diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dcf8d95dfa..1692330022 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,8 +68,8 @@ jobs: ci_run zk db basic-setup ci_run zk run yarn - - name: liquidity-token - run: docker-compose -f docker-compose-runner.yml restart dev-liquidity-token-watcher + - name: restart dev-liquidity-token-watcher and dev-ticker + run: docker-compose -f docker-compose-runner.yml restart dev-liquidity-token-watcher dev-ticker - name: contracts-unit-tests run: ci_run zk test contracts @@ -105,8 +105,8 @@ jobs: ci_run zk dummy-prover enable --no-redeploy ci_run zk init - - name: liquidity-token - run: docker-compose -f docker-compose-runner.yml restart dev-liquidity-token-watcher + - name: restart dev-liquidity-token-watcher and dev-ticker + run: docker-compose -f docker-compose-runner.yml restart dev-liquidity-token-watcher dev-ticker - name: run-services run: | diff --git a/core/bin/zksync_api/src/bin/dev-ticker-server.rs b/core/bin/zksync_api/src/bin/dev-ticker-server.rs index 7cef170ef8..5b22496fb7 100644 --- a/core/bin/zksync_api/src/bin/dev-ticker-server.rs +++ b/core/bin/zksync_api/src/bin/dev-ticker-server.rs @@ -9,9 +9,11 @@ use bigdecimal::BigDecimal; use chrono::{SecondsFormat, Utc}; use serde::{Deserialize, Serialize}; use serde_json::json; +use std::{collections::HashMap, fs::read_to_string, path::Path}; use std::{convert::TryFrom, time::Duration}; use structopt::StructOpt; use zksync_crypto::rand::{thread_rng, Rng}; +use zksync_types::Address; #[derive(Debug, Serialize, Deserialize)] struct CoinMarketCapTokenQuery { @@ -20,7 +22,7 @@ struct CoinMarketCapTokenQuery { macro_rules! make_sloppy { ($f: ident) => {{ - |query| async { + |query, data| async { if thread_rng().gen_range(0, 100) < 5 { vlog::debug!("`{}` has been errored", stringify!($f)); return Ok(HttpResponse::InternalServerError().finish()); @@ -42,7 +44,7 @@ macro_rules! make_sloppy { ); tokio::time::delay_for(duration).await; - let resp = $f(query).await; + let resp = $f(query, data).await; resp } }}; @@ -50,6 +52,7 @@ macro_rules! make_sloppy { async fn handle_coinmarketcap_token_price_query( query: web::Query, + _data: web::Data>, ) -> Result { let symbol = query.symbol.clone(); let base_price = match symbol.as_str() { @@ -82,36 +85,62 @@ async fn handle_coinmarketcap_token_price_query( Ok(HttpResponse::Ok().json(resp)) } -async fn handle_coingecko_token_list(_req: HttpRequest) -> Result { - let resp = json!([ - {"id": "ethereum", "symbol": "eth", "name": "Ethereum"}, - {"id": "dai", "symbol":"dai", "name": "Dai"}, - {"id": "glm", "symbol":"glm", "name": "Golem"}, - {"id": "tglm", "symbol":"tglm", "name": "Golem"}, - {"id": "usdc", "symbol":"usdc", "name": "usdc"}, - {"id": "usdt", "symbol":"usdt", "name": "usdt"}, - {"id": "tusd", "symbol":"tusd", "name": "tusd"}, - {"id": "link", "symbol":"link", "name": "link"}, - {"id": "ht", "symbol":"ht", "name": "ht"}, - {"id": "omg", "symbol":"omg", "name": "omg"}, - {"id": "trb", "symbol":"trb", "name": "trb"}, - {"id": "zrx", "symbol":"zrx", "name": "zrx"}, - {"id": "rep", "symbol":"rep", "name": "rep"}, - {"id": "storj", "symbol":"storj", "name": "storj"}, - {"id": "nexo", "symbol":"nexo", "name": "nexo"}, - {"id": "mco", "symbol":"mco", "name": "mco"}, - {"id": "knc", "symbol":"knc", "name": "knc"}, - {"id": "lamb", "symbol":"lamb", "name": "lamb"}, - {"id": "xem", "symbol":"xem", "name": "xem"}, - {"id": "phnx", "symbol":"phnx", "name": "Golem"}, - {"id": "basic-attention-token", "symbol": "bat", "name": "Basic Attention Token"}, - {"id": "wrapped-bitcoin", "symbol": "wbtc", "name": "Wrapped Bitcoin"}, - ]); +#[derive(Debug, Deserialize)] +struct Token { + pub address: Address, + pub decimals: u8, + pub symbol: String, +} - Ok(HttpResponse::Ok().json(resp)) +#[derive(Serialize, Deserialize, Clone, Debug)] +struct TokenData { + id: String, + symbol: String, + name: String, + platforms: HashMap, +} + +fn load_tokens(path: impl AsRef) -> Vec { + if let Ok(text) = read_to_string(path) { + let tokens: Vec = serde_json::from_str(&text).unwrap(); + let tokens_data: Vec = tokens + .into_iter() + .map(|token| { + let symbol = token.symbol.to_lowercase(); + let mut platforms = HashMap::new(); + platforms.insert(String::from("ethereum"), token.address); + let id = match symbol.as_str() { + "eth" => String::from("ethereum"), + "wbtc" => String::from("wrapped-bitcoin"), + "bat" => String::from("basic-attention-token"), + _ => symbol.clone(), + }; + + TokenData { + id, + symbol: symbol.clone(), + name: symbol, + platforms, + } + }) + .collect(); + tokens_data + } else { + Vec::new() + } +} + +async fn handle_coingecko_token_list( + _req: HttpRequest, + data: web::Data>, +) -> Result { + Ok(HttpResponse::Ok().json((*data.into_inner()).clone())) } -async fn handle_coingecko_token_price_query(req: HttpRequest) -> Result { +async fn handle_coingecko_token_price_query( + req: HttpRequest, + _data: web::Data>, +) -> Result { let coin_id = req.match_info().get("coin_id"); let base_price = match coin_id { Some("ethereum") => BigDecimal::from(200), @@ -133,8 +162,17 @@ async fn handle_coingecko_token_price_query(req: HttpRequest) -> Result actix_web::Scope { + let localhost_tokens = load_tokens(&"etc/tokens/localhost.json"); + let rinkeby_tokens = load_tokens(&"etc/tokens/rinkeby.json"); + let ropsten_tokens = load_tokens(&"etc/tokens/ropsten.json"); + let data: Vec = localhost_tokens + .into_iter() + .chain(rinkeby_tokens.into_iter()) + .chain(ropsten_tokens.into_iter()) + .collect(); if sloppy_mode { web::scope("/") + .data(data) .route( "/cryptocurrency/quotes/latest", web::get().to(make_sloppy!(handle_coinmarketcap_token_price_query)), @@ -149,6 +187,7 @@ fn main_scope(sloppy_mode: bool) -> actix_web::Scope { ) } else { web::scope("/") + .data(data) .route( "/cryptocurrency/quotes/latest", web::get().to(handle_coinmarketcap_token_price_query), diff --git a/core/bin/zksync_api/src/fee_ticker/tests.rs b/core/bin/zksync_api/src/fee_ticker/tests.rs index a296964a41..e9be550d79 100644 --- a/core/bin/zksync_api/src/fee_ticker/tests.rs +++ b/core/bin/zksync_api/src/fee_ticker/tests.rs @@ -191,12 +191,12 @@ struct ErrorTickerApi; #[async_trait::async_trait] impl TokenPriceAPI for ErrorTickerApi { - async fn get_price(&self, _token_symbol: &str) -> Result { + async fn get_price(&self, _token: &Token) -> Result { Err(PriceError::token_not_found("Wrong token")) } } -fn run_server() -> (String, AbortHandle) { +fn run_server(token_address: Address) -> (String, AbortHandle) { let mut url = None; let mut server = None; for i in 9000..9999 { @@ -210,10 +210,15 @@ fn run_server() -> (String, AbortHandle) { HttpResponse::MethodNotAllowed() })), ) - .service(web::resource("/api/v3/coins/list").to(|| { + .service(web::resource("/api/v3/coins/list").to(move || { + let mut platforms = HashMap::new(); + platforms.insert( + String::from("ethereum"), + serde_json::Value::String(serde_json::to_string(&token_address).unwrap()), + ); HttpResponse::Ok().json(CoinGeckoTokenList(vec![CoinGeckoTokenInfo { - id: "DAI".to_string(), - symbol: "DAI".to_string(), + id: "dai".to_string(), + platforms, }])) })) }) @@ -387,7 +392,13 @@ fn test_zero_price_token_fee() { #[ignore] // It's ignore because we can't initialize coingecko in current way with block async fn test_error_coingecko_api() { - let (address, handler) = run_server(); + let token = Token { + id: TokenId(1), + address: Address::random(), + symbol: String::from("DAI"), + decimals: 18, + }; + let (address, handler) = run_server(token.address); let client = reqwest::ClientBuilder::new() .timeout(CONNECTION_TIMEOUT) .connect_timeout(CONNECTION_TIMEOUT) @@ -402,20 +413,25 @@ async fn test_error_coingecko_api() { FakeTokenWatcher, ); let connection_pool = ConnectionPool::new(Some(1)); - connection_pool - .access_storage() - .await - .unwrap() - .tokens_schema() - .update_historical_ticker_price( - TokenId(1), - TokenPrice { - usd_price: big_decimal_to_ratio(&BigDecimal::from(10)).unwrap(), - last_updated: chrono::offset::Utc::now(), - }, - ) - .await - .unwrap(); + { + let mut storage = connection_pool.access_storage().await.unwrap(); + storage + .tokens_schema() + .store_token(token.clone()) + .await + .unwrap(); + storage + .tokens_schema() + .update_historical_ticker_price( + token.id, + TokenPrice { + usd_price: big_decimal_to_ratio(&BigDecimal::from(10)).unwrap(), + last_updated: chrono::offset::Utc::now(), + }, + ) + .await + .unwrap(); + } let ticker_api = TickerApi::new(connection_pool, coingecko); let config = get_test_ticker_config(); @@ -430,13 +446,13 @@ async fn test_error_coingecko_api() { ticker .get_fee_from_ticker_in_wei( TxFeeTypes::FastWithdraw, - TokenId(1).into(), + token.id.into(), Address::default(), ) .await .unwrap(); ticker - .get_token_price(TokenId(1).into(), TokenPriceRequestType::USDForOneWei) + .get_token_price(token.id.into(), TokenPriceRequestType::USDForOneWei) .await .unwrap(); } diff --git a/core/bin/zksync_api/src/fee_ticker/ticker_api/coingecko.rs b/core/bin/zksync_api/src/fee_ticker/ticker_api/coingecko.rs index e6b625daab..f6a0627c79 100644 --- a/core/bin/zksync_api/src/fee_ticker/ticker_api/coingecko.rs +++ b/core/bin/zksync_api/src/fee_ticker/ticker_api/coingecko.rs @@ -7,21 +7,22 @@ use num::BigUint; use reqwest::Url; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::str::FromStr; use std::time::Instant; -use zksync_types::TokenPrice; -use zksync_utils::UnsignedRatioSerializeAsDecimal; +use zksync_types::{Address, Token, TokenPrice}; +use zksync_utils::{remove_prefix, UnsignedRatioSerializeAsDecimal}; #[derive(Debug, Clone)] pub struct CoinGeckoAPI { base_url: Url, client: reqwest::Client, - token_ids: HashMap, + token_ids: HashMap, } impl CoinGeckoAPI { pub fn new(client: reqwest::Client, base_url: Url) -> anyhow::Result { let token_list_url = base_url - .join("api/v3/coins/list") + .join("api/v3/coins/list?include_platform=true") .expect("failed to join URL path"); let token_list = reqwest::blocking::get(token_list_url) @@ -30,8 +31,19 @@ impl CoinGeckoAPI { let mut token_ids = HashMap::new(); for token in token_list.0 { - token_ids.insert(token.symbol, token.id); + if let Some(address_value) = token.platforms.get("ethereum") { + if let Some(address_str) = address_value.as_str() { + let address_str = remove_prefix(address_str); + if let Ok(address) = Address::from_str(address_str) { + token_ids.insert(address, token.id); + } + } + } } + + // Add ETH manually because coingecko API doesn't return address for it. + token_ids.insert(Address::default(), String::from("ethereum")); + Ok(Self { base_url, client, @@ -42,21 +54,15 @@ impl CoinGeckoAPI { #[async_trait] impl TokenPriceAPI for CoinGeckoAPI { - async fn get_price(&self, token_symbol: &str) -> Result { + async fn get_price(&self, token: &Token) -> Result { let start = Instant::now(); - let token_lowercase_symbol = token_symbol.to_lowercase(); - let token_id = self - .token_ids - .get(&token_lowercase_symbol) - .or_else(|| self.token_ids.get(token_symbol)) - .unwrap_or(&token_lowercase_symbol); - // TODO ZKS-595. Uncomment this code - // .ok_or_else(|| { - // PriceError::token_not_found(format!( - // "Token '{}' is not listed on CoinGecko", - // token_symbol - // )) - // })?; + let token_symbol = token.symbol.as_str(); + let token_id = self.token_ids.get(&token.address).ok_or_else(|| { + PriceError::token_not_found(format!( + "Token '{}, {:?}' is not listed on CoinGecko", + token.symbol, token.address + )) + })?; let market_chart_url = self .base_url @@ -117,7 +123,7 @@ impl TokenPriceAPI for CoinGeckoAPI { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CoinGeckoTokenInfo { pub(crate) id: String, - pub(crate) symbol: String, + pub(crate) platforms: HashMap, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -137,6 +143,7 @@ pub struct CoinGeckoMarketChart { #[cfg(test)] mod tests { use super::*; + use zksync_types::TokenId; use zksync_utils::parse_env; #[tokio::test] @@ -144,7 +151,8 @@ mod tests { let ticker_url = parse_env("FEE_TICKER_COINGECKO_BASE_URL"); let client = reqwest::Client::new(); let api = CoinGeckoAPI::new(client, ticker_url).unwrap(); - api.get_price("ETH") + let token = Token::new(TokenId(0), Default::default(), "ETH", 18); + api.get_price(&token) .await .expect("Failed to get data from ticker"); } diff --git a/core/bin/zksync_api/src/fee_ticker/ticker_api/coinmarkercap.rs b/core/bin/zksync_api/src/fee_ticker/ticker_api/coinmarkercap.rs index a263a8bbbe..2eede509ef 100644 --- a/core/bin/zksync_api/src/fee_ticker/ticker_api/coinmarkercap.rs +++ b/core/bin/zksync_api/src/fee_ticker/ticker_api/coinmarkercap.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; // Workspace deps use super::{TokenPriceAPI, REQUEST_TIMEOUT}; use crate::fee_ticker::PriceError; -use zksync_types::{TokenLike, TokenPrice}; +use zksync_types::{Token, TokenLike, TokenPrice}; use zksync_utils::UnsignedRatioSerializeAsDecimal; #[derive(Debug)] @@ -26,7 +26,8 @@ impl CoinMarketCapAPI { #[async_trait] impl TokenPriceAPI for CoinMarketCapAPI { - async fn get_price(&self, token_symbol: &str) -> Result { + async fn get_price(&self, token: &Token) -> Result { + let token_symbol = token.symbol.as_str(); let request_url = self .base_url .join(&format!( @@ -84,6 +85,7 @@ pub(super) struct CoinmarketCapResponse { mod test { use super::*; use std::str::FromStr; + use zksync_types::TokenId; use zksync_utils::parse_env; #[test] @@ -97,8 +99,9 @@ mod test { let ticker_url = parse_env("FEE_TICKER_COINMARKETCAP_BASE_URL"); let client = reqwest::Client::new(); let api = CoinMarketCapAPI::new(client, ticker_url); + let token = Token::new(TokenId(0), Default::default(), "ETH", 18); runtime - .block_on(api.get_price("ETH")) + .block_on(api.get_price(&token)) .expect("Failed to get data from ticker"); } diff --git a/core/bin/zksync_api/src/fee_ticker/ticker_api/mod.rs b/core/bin/zksync_api/src/fee_ticker/ticker_api/mod.rs index 97666c71b5..be6a0d3104 100644 --- a/core/bin/zksync_api/src/fee_ticker/ticker_api/mod.rs +++ b/core/bin/zksync_api/src/fee_ticker/ticker_api/mod.rs @@ -25,7 +25,7 @@ pub const CONNECTION_TIMEOUT: Duration = Duration::from_millis(700); #[async_trait] pub trait TokenPriceAPI { - async fn get_price(&self, token_symbol: &str) -> Result; + async fn get_price(&self, token: &Token) -> Result; } /// Api responsible for querying for TokenPrices @@ -232,7 +232,7 @@ impl FeeTickerAPI for TickerApi { return Ok(cached_value); } - let api_price = self.token_price_api.get_price(&token.symbol).await; + let api_price = self.token_price_api.get_price(&token).await; match api_price { Ok(api_price) => { diff --git a/docker-compose-runner.yml b/docker-compose-runner.yml index 513cbf65f6..eb7c0513ff 100644 --- a/docker-compose-runner.yml +++ b/docker-compose-runner.yml @@ -12,6 +12,8 @@ services: dev-ticker: image: "matterlabs/dev-ticker:latest" + volumes: + - ./etc/tokens/:/etc/tokens dev-liquidity-token-watcher: image: "matterlabs/dev-liquidity-token-watcher:latest" diff --git a/docker-compose.yml b/docker-compose.yml index 96b2a4662a..e560a21a04 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,8 @@ services: image: "matterlabs/dev-ticker:latest" ports: - "9876:9876" + volumes: + - ./etc/tokens/:/etc/tokens tesseracts: image: "adria0/tesseracts" command: --cfg /tesseracts.toml -vvv diff --git a/docker/dev-ticker/Dockerfile b/docker/dev-ticker/Dockerfile index da146d226a..d1bb4dcda5 100644 --- a/docker/dev-ticker/Dockerfile +++ b/docker/dev-ticker/Dockerfile @@ -15,4 +15,5 @@ RUN apt install openssl -y EXPOSE 9876 ENV RUST_LOG info COPY --from=builder /usr/src/zksync/target/release/dev-ticker-server /bin/ +COPY --from=builder /usr/src/zksync/etc/tokens /etc/tokens ENTRYPOINT ["dev-ticker-server"] diff --git a/infrastructure/zk/src/run/run.ts b/infrastructure/zk/src/run/run.ts index 18bbd670d5..93452a11b5 100644 --- a/infrastructure/zk/src/run/run.ts +++ b/infrastructure/zk/src/run/run.ts @@ -22,6 +22,7 @@ export async function deployERC20(command: 'dev' | 'new', name?: string, symbol? ]' > ./etc/tokens/localhost.json`); if (!process.env.CI) { await docker.restart('dev-liquidity-token-watcher'); + await docker.restart('dev-ticker'); } } else if (command == 'new') { await utils.spawn(