From e0c8c1ba3da0a65a3dd2681d3b7802c1c09cc4a1 Mon Sep 17 00:00:00 2001 From: Juraj Piar Date: Fri, 15 Sep 2023 10:43:48 +0100 Subject: [PATCH] feat(ticker): modifies dev ticker to proxy to coingecko --- .../src/bin/providers/dev_price_provider.rs | 92 +++++++++++++++++-- 1 file changed, 84 insertions(+), 8 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 2dd68aa87..06470a3f3 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,11 +3,15 @@ //! 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}; +use actix_web::{web, HttpRequest, HttpResponse, Result, Scope}; use bigdecimal::BigDecimal; use chrono::{SecondsFormat, Utc}; +use jsonrpc_ws_server::ws::Handler; +use reqwest::Client; use serde::{Deserialize, Serialize}; -use serde_json::json; +use serde_json::{json, Value}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; use std::{collections::HashMap, fs::read_to_string, path::Path}; use std::{convert::TryFrom, time::Duration}; use zksync_crypto::rand::{thread_rng, Rng}; @@ -18,6 +22,77 @@ 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 { @@ -176,6 +251,9 @@ pub fn create_price_service(sloppy_mode: bool) -> actix_web::Scope { .into_iter() .chain(testnet_tokens.into_iter()) .collect(); + + // When sloppy mode is enabled, we are more tolerant of errors and we use + // a fake API that returns random prices. This is useful for testing. if sloppy_mode { web::scope("") .app_data(web::Data::new(data)) @@ -192,19 +270,17 @@ 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(handle_coinmarketcap_token_price_query), - ) - .route( - "/api/v3/coins/list", - web::get().to(handle_coingecko_token_list), + web::get().to(fetch_coin_market_cap), ) + .route("/api/v3/coins/list", web::get().to(fetch_coins_list)) .route( "/api/v3/coins/{coin_id}/market_chart", - web::get().to(handle_coingecko_token_price_query), + web::get().to(fetch_market_chart), ) } }