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 25955ac03..3be5b5f33 100644 --- a/core/bin/zksync_api/src/bin/dev_ticker_server.rs +++ b/core/bin/zksync_api/src/bin/dev_ticker_server.rs @@ -1,24 +1,9 @@ -use std::{ - collections::HashMap, - fs::read_to_string, - path::Path, - str::FromStr, - sync::{Arc, Mutex}, - time::{Duration, Instant}, -}; - use actix_cors::Cors; -use actix_web::{middleware, web, App, HttpResponse, HttpServer, Scope}; -use itertools::Itertools; -use regex::Replacer; -use reqwest::Client; -use serde_json::Value; -// use providers::{ -// dev_liquidity_provider::config_liquidity_app, dev_price_provider::create_price_service, -// }; +use actix_web::{middleware, App, HttpServer}; +use providers::{dev_liquidity_provider, dev_price_provider, proxy_price_provider}; use structopt::StructOpt; -use web3::signing::Key; -use zksync_types::{Address, TokenInfo}; +use zksync_config::ZkSyncConfig; +use zksync_types::network::Network; mod providers; @@ -33,156 +18,10 @@ struct FeeTickerOpts { sloppy: bool, } -struct ResponseCache { - data: T, - last_fetched: Instant, -} - -#[derive(Clone)] -struct ProxyState { - testnet_to_mainnet_address_mapping: HashMap, - cache: Arc>>>, -} - -fn load_tokens(path: impl AsRef) -> Result, serde_json::Error> { - let tokens = serde_json::from_str(&read_to_string(path).unwrap()); - - tokens -} - -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(), - ResponseCache { - data: data.clone(), - last_fetched: Instant::now(), - }, - ); - HttpResponse::Ok().json(data) - } - Err(_) => HttpResponse::InternalServerError().finish(), - }, - Err(_) => HttpResponse::InternalServerError().finish(), - } -} - -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 token_address = path.0.clone(); - let testnet_address = Address::from_str(&token_address).unwrap(); // TODO: should handle the error - let mainnet_address: Option<&zksync_types::H160> = data - .testnet_to_mainnet_address_mapping - .get(&testnet_address); - let url = format!( - "https://api.coingecko.com/api/v3/coins/{}/market_chart", - match mainnet_address { - None => testnet_address, - Some(address) => *address, - } - ); - - proxy_request(&url, &data.cache).await -} - -fn create_price_service() -> Scope { - let mainnet_tokens = load_tokens("etc/tokens/mainnet.json").unwrap(); - let testnet_tokens = load_tokens("etc/tokens/testnet.json").unwrap(); - - let testnet_to_mainnet_address_mapping: HashMap = mainnet_tokens.iter().fold( - HashMap::::new(), - |mut acc, mainnet_token| { - let mainnet_symbol: &str = &mainnet_token.symbol; - let testnet_token = testnet_tokens - .iter() - .find(|testnet_token: &&TokenInfo| { - let testnet_symbol: &str = &testnet_token.symbol.to_uppercase(); - let mut prefixed_mainnet_symbol: String = "T".to_owned(); - prefixed_mainnet_symbol.push_str(testnet_symbol); - - testnet_symbol.eq(mainnet_symbol) - || testnet_symbol - .to_owned() - .to_uppercase() - .eq(&prefixed_mainnet_symbol) - }) - .unwrap(); - - acc.insert(testnet_token.address, mainnet_token.address); - - acc - }, - ); - - let shared_data = web::Data::new(ProxyState { - testnet_to_mainnet_address_mapping, - cache: std::sync::Arc::new(Mutex::new(HashMap::new())), - }); - - web::scope("") - .app_data(web::Data::new(shared_data)) - .route( - "/cryptocurrency/quotes/latest", - web::get().to(|| HttpResponse::NotImplemented()), - ) - .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), - ) -} - -// fn forward_asset_platforms() -> Result { -// } - -// fn config_liquidity_app(cfg: &mut web::ServiceConfig) { - -// cfg.service(web::resource("/asset_platforms").route(web::get().to(forward_asset_platforms))) -// .service( -// web::scope("/coins").service( -// web::scope("/{platform_id}").service( -// web::scope("/contract").service( -// web::resource("/{contract_address}") -// .route(web::get().to(handle_get_coin_contract)), -// ), -// ), -// ), -// ); -// } - -// FIXME: don't forget to COPY out the proxy changes to its own PROXY TICKER and REVERT this file to the original DEV TICEKR. this will require some DEPLOYMENT changes in TESTNET #[actix_web::main] async fn main() -> std::io::Result<()> { let _vlog_guard = vlog::init(); + let network = ZkSyncConfig::from_env().chain.eth.network; let opts = FeeTickerOpts::from_args(); if opts.sloppy { @@ -190,11 +29,22 @@ async fn main() -> std::io::Result<()> { } HttpServer::new(move || { - App::new() + let base_app = App::new() .wrap(Cors::default().send_wildcard().max_age(3600)) - .wrap(middleware::Logger::default()) - // .configure(config_liquidity_app) - .service(create_price_service()) + .wrap(middleware::Logger::default()); + match network { + Network::Testnet => { + base_app + // .configure(config_liquidity_app) // TODO: implement proxy + .service(proxy_price_provider::create_price_service()) + } + Network::Mainnet => { + panic!("{}", "Not meant to be running against mainnet!".to_string()) + } + _ => base_app + .configure(dev_liquidity_provider::config_liquidity_app) + .service(dev_price_provider::create_price_service(opts.sloppy)), + } }) .bind("0.0.0.0:9876") .unwrap() diff --git a/core/bin/zksync_api/src/bin/providers/mod.rs b/core/bin/zksync_api/src/bin/providers/mod.rs index 5f387b961..6f927fa13 100644 --- a/core/bin/zksync_api/src/bin/providers/mod.rs +++ b/core/bin/zksync_api/src/bin/providers/mod.rs @@ -1,2 +1,3 @@ pub mod dev_liquidity_provider; pub mod dev_price_provider; +pub mod proxy_price_provider; diff --git a/core/bin/zksync_api/src/bin/providers/proxy_price_provider.rs b/core/bin/zksync_api/src/bin/providers/proxy_price_provider.rs new file mode 100644 index 000000000..b7ee87c00 --- /dev/null +++ b/core/bin/zksync_api/src/bin/providers/proxy_price_provider.rs @@ -0,0 +1,116 @@ +use std::{ + collections::HashMap, + sync::Arc, + time::{Duration, Instant}, +}; +use tokio::sync::Mutex; + +use actix_web::{web, HttpResponse, Scope}; +use serde_json::Value; +use zksync_api::fee_ticker::CoinGeckoTypes::CoinsListItem; + +struct ResponseCache { + data: T, + last_fetched: Instant, +} + +#[derive(Clone)] +struct ProxyState { + cache: Arc>>>, +} + +async fn proxy_request( + url: &str, + cache: &Mutex>>, +) -> HttpResponse { + let mut lock = cache.lock().await; + + // 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 + + match reqwest::get(url).await { + Ok(response) => match response.json::().await { + Ok(data) => { + // Cache the fetched data + lock.insert( + url.to_string(), + ResponseCache { + data: data.clone(), + last_fetched: Instant::now(), + }, + ); + HttpResponse::Ok().json(data) + } + Err(_) => HttpResponse::InternalServerError().finish(), + }, + Err(_) => HttpResponse::InternalServerError().finish(), + } +} + +const RIF_TOKEN_TESTNET_ADDRESS: &str = "0x19f64674D8a5b4e652319F5e239EFd3bc969a1FE"; + +async fn fetch_coins_list(_: web::Data, _: web::Path<(bool,)>) -> HttpResponse { + let rootstock_platform: HashMap> = vec![( + "rootstock".to_string(), + Some(RIF_TOKEN_TESTNET_ADDRESS.to_string()), + )] + .into_iter() + .collect(); + let rif_token = CoinsListItem { + id: "rif-token".to_string(), + platforms: Some(rootstock_platform.clone()), + name: "RIF Token".to_string(), + symbol: "RIF".to_string(), + }; + let rbtc = CoinsListItem { + id: "rootstock".to_string(), + symbol: "rbtc".to_string(), + name: "Rootstock RSK".to_string(), + platforms: Some(rootstock_platform), + }; + let coin_list: &[CoinsListItem] = &[rif_token, rbtc]; + + HttpResponse::Ok().json(coin_list) +} + +async fn fetch_market_chart( + data: web::Data, + path: web::Path<(String,)>, +) -> HttpResponse { + let (coin_id,) = path.into_inner(); + let url = format!( + "https://api.coingecko.com/api/v3/coins/{}/market_chart", + coin_id + ); + + proxy_request(&url, &data.cache).await +} + +fn fetch_coinmarketcap_price() -> HttpResponse { + HttpResponse::NotImplemented().json("{}") +} + +pub(crate) fn create_price_service() -> Scope { + let shared_data = web::Data::new(ProxyState { + cache: std::sync::Arc::new(Mutex::new(HashMap::new())), + }); + + web::scope("") + .app_data(web::Data::new(shared_data)) + .route( + "/cryptocurrency/quotes/latest", + web::get().to(fetch_coinmarketcap_price), + ) + .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), + ) +} 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 ea7b1b3f0..3b7b2d45b 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,12 +6,10 @@ use num::rational::Ratio; use num::BigUint; use reqwest::Url; use serde::{Deserialize, Serialize}; -use std::fs::File; +use std::collections::HashMap; use std::str::FromStr; use std::time::Instant; -use std::{collections::HashMap, path::Path}; -use zksync_types::Token; -use zksync_types::{network::Network, Address, TokenPrice}; +use zksync_types::{Address, Token, TokenPrice}; use zksync_utils::{remove_prefix, UnsignedRatioSerializeAsDecimal}; #[derive(Debug, Clone)] @@ -19,56 +17,10 @@ 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"); @@ -98,7 +50,6 @@ impl CoinGeckoAPI { base_url, client, token_ids, - tnet_to_mnet_address_mapping: address_mapping, }) } } @@ -106,25 +57,11 @@ 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/core/bin/zksync_api/src/fee_ticker/validator/types.rs b/core/bin/zksync_api/src/fee_ticker/validator/types.rs index f81d8a63d..a13e75da1 100644 --- a/core/bin/zksync_api/src/fee_ticker/validator/types.rs +++ b/core/bin/zksync_api/src/fee_ticker/validator/types.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; // --------- locally simplified Contract struct for retreiving market data only @@ -24,3 +26,14 @@ pub struct AssetPlatform { pub name: String, pub shortname: String, } + +// --------------------------------------------- +// /coins/list +// --------------------------------------------- +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CoinsListItem { + pub id: String, + pub symbol: String, + pub name: String, + pub platforms: Option>>, +}