From 375c7700e7d856e659c925017f3a4f94f6a142d4 Mon Sep 17 00:00:00 2001 From: Juraj Piar Date: Fri, 15 Sep 2023 12:11:53 +0100 Subject: [PATCH] feat(ticker): translates addresses in client --- .../src/bin/providers/dev_price_provider.rs | 89 ++----------------- .../src/fee_ticker/ticker_api/coingecko.rs | 71 ++++++++++++++- etc/tokens/testnet.json | 4 +- 3 files changed, 77 insertions(+), 87 deletions(-) diff --git a/core/bin/zksync_api/src/bin/providers/dev_price_provider.rs b/core/bin/zksync_api/src/bin/providers/dev_price_provider.rs index 06470a3f3..4ff369c48 100644 --- a/core/bin/zksync_api/src/bin/providers/dev_price_provider.rs +++ b/core/bin/zksync_api/src/bin/providers/dev_price_provider.rs @@ -3,15 +3,11 @@ //! Implements coinmarketcap API for tokens deployed using `deploy-dev-erc20` //! Prices are randomly distributed around base values estimated from real world prices. -use actix_web::{web, HttpRequest, HttpResponse, Result, Scope}; +use actix_web::{web, HttpRequest, HttpResponse, Result}; use bigdecimal::BigDecimal; use chrono::{SecondsFormat, Utc}; -use jsonrpc_ws_server::ws::Handler; -use reqwest::Client; use serde::{Deserialize, Serialize}; -use serde_json::{json, Value}; -use std::sync::{Arc, Mutex}; -use std::time::Instant; +use serde_json::json; use std::{collections::HashMap, fs::read_to_string, path::Path}; use std::{convert::TryFrom, time::Duration}; use zksync_crypto::rand::{thread_rng, Rng}; @@ -22,77 +18,6 @@ struct CoinMarketCapTokenQuery { symbol: String, } -struct Cache { - data: T, - last_fetched: Instant, -} - -#[derive(Clone)] -struct ProxyState { - cache: Arc>>>, -} - -async fn fetch_coins_list(data: web::Data, path: web::Path<(bool,)>) -> HttpResponse { - let include_platform = path.0.clone(); - let url = format!( - "https://api.coingecko.com/api/v3/coins/list?include_platform={}", - include_platform - ); - proxy_request(&url, &data.cache).await -} - -async fn fetch_market_chart( - data: web::Data, - path: web::Path<(String,)>, -) -> HttpResponse { - let coin = path.0.clone(); - let url = format!( - "https://api.coingecko.com/api/v3/coins/{}/market_chart", - coin - ); - proxy_request(&url, &data.cache).await -} - -async fn fetch_coin_market_cap(data: web::Data) -> HttpResponse { - proxy_request( - "https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest", - &data.cache, - ) - .await -} - -async fn proxy_request(url: &str, cache: &Mutex>>) -> HttpResponse { - let mut lock = cache.lock().unwrap(); - - // Check cache first - if let Some(cached) = lock.get(url) { - if cached.last_fetched.elapsed() < Duration::from_secs(5) { - // TODO: configure timeout (or use existing one) - return HttpResponse::Ok().json(&cached.data); - } - } - - // Fetch data if not in cache or stale - let client = Client::new(); - match client.get(url).send().await { - Ok(response) => match response.json::().await { - Ok(data) => { - // Cache the fetched data - lock.insert( - url.to_string(), - Cache { - data: data.clone(), - last_fetched: Instant::now(), - }, - ); - HttpResponse::Ok().json(data) - } - Err(_) => HttpResponse::InternalServerError().finish(), - }, - Err(_) => HttpResponse::InternalServerError().finish(), - } -} - macro_rules! make_sloppy { ($f: ident) => {{ |query, data| async { @@ -270,17 +195,19 @@ pub fn create_price_service(sloppy_mode: bool) -> actix_web::Scope { web::get().to(make_sloppy!(handle_coingecko_token_price_query)), ) } else { - // TODO: allow non-sloppy mode for dev env (ei move the proxy to its own service) web::scope("") .app_data(web::Data::new(data)) .route( "/cryptocurrency/quotes/latest", - web::get().to(fetch_coin_market_cap), + web::get().to(handle_coinmarketcap_token_price_query), + ) + .route( + "/api/v3/coins/list", + web::get().to(handle_coingecko_token_list), ) - .route("/api/v3/coins/list", web::get().to(fetch_coins_list)) .route( "/api/v3/coins/{coin_id}/market_chart", - web::get().to(fetch_market_chart), + web::get().to(handle_coingecko_token_price_query), ) } } 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 3b7b2d45b..ea7b1b3f0 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 @@ -6,10 +6,12 @@ use num::rational::Ratio; use num::BigUint; use reqwest::Url; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::fs::File; use std::str::FromStr; use std::time::Instant; -use zksync_types::{Address, Token, TokenPrice}; +use std::{collections::HashMap, path::Path}; +use zksync_types::Token; +use zksync_types::{network::Network, Address, TokenPrice}; use zksync_utils::{remove_prefix, UnsignedRatioSerializeAsDecimal}; #[derive(Debug, Clone)] @@ -17,10 +19,56 @@ pub struct CoinGeckoAPI { base_url: Url, client: reqwest::Client, token_ids: HashMap, + tnet_to_mnet_address_mapping: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TokenInfo { + /// Address (prefixed with 0x) + pub address: Address, + /// Powers of 10 in 1.0 token (18 for default RBTC-like tokens) + pub decimals: u8, + /// Token symbol + pub symbol: String, + pub name: String, +} + +impl TokenInfo { + pub fn from_json(reader: R) -> anyhow::Result> { + let tokens: Vec = serde_json::from_reader(reader)?; + Ok(tokens) + } } impl CoinGeckoAPI { pub async fn new(client: reqwest::Client, base_url: Url) -> anyhow::Result { + // If network is testner + let zksync_config = zksync_config::ZkSyncConfig::from_env(); + let network = zksync_config.chain.eth.network; + + // Get tokens from etc/tokens/testnet.json and etc/tokens/mainnet.json + let address_mapping = if network == Network::Testnet { + let testnet_tokens_file = Path::new("etc/tokens/testnet.json"); + let mainnet_tokens_file = Path::new("etc/tokens/mainnet.json"); + + let testnet_tokens_file = File::open(testnet_tokens_file)?; + let mainnet_tokens_file = File::open(mainnet_tokens_file)?; + + let testnet_tokens: Vec = TokenInfo::from_json(testnet_tokens_file)?; + let mainnet_tokens: Vec = TokenInfo::from_json(mainnet_tokens_file)?; + + let mut address_mapping = HashMap::new(); + + for (testnet_token, mainnet_token) in testnet_tokens.iter().zip(mainnet_tokens.iter()) { + address_mapping.insert(testnet_token.address, mainnet_token.address); + } + + address_mapping + } else { + HashMap::new() + }; + // create a testnet to mainnet address mapping + let token_list_url = base_url .join("api/v3/coins/list?include_platform=true") .expect("failed to join URL path"); @@ -50,6 +98,7 @@ impl CoinGeckoAPI { base_url, client, token_ids, + tnet_to_mnet_address_mapping: address_mapping, }) } } @@ -57,11 +106,25 @@ impl CoinGeckoAPI { #[async_trait] impl TokenPriceAPI for CoinGeckoAPI { async fn get_price(&self, token: &Token) -> Result { + let token_address = { + let zksync_config = zksync_config::ZkSyncConfig::from_env(); + let network = zksync_config.chain.eth.network; + + if network == Network::Testnet { + self.tnet_to_mnet_address_mapping + .get(&token.address) + .copied() + .unwrap_or(token.address) + } else { + token.address + } + }; + let start = Instant::now(); - let token_id = self.token_ids.get(&token.address).ok_or_else(|| { + 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 + token.symbol, token_address )) })?; diff --git a/etc/tokens/testnet.json b/etc/tokens/testnet.json index 6ccbcd0e9..54953060f 100644 --- a/etc/tokens/testnet.json +++ b/etc/tokens/testnet.json @@ -2,13 +2,13 @@ { "address": "0x19f64674D8a5b4e652319F5e239EFd3bc969a1FE", "decimals": 18, - "symbol": "RIF", + "symbol": "TRIF", "name": "RSK Infrastructure Framework" }, { "address": "0xC3De9f38581F83e281F260D0ddBAac0E102Ff9F8", "decimals": 18, - "symbol": "rDOC", + "symbol": "RDOC", "name": "RIF Dollar on Chain" } ]