Skip to content

Commit

Permalink
feat(ticker): separates dev and proxy tickers
Browse files Browse the repository at this point in the history
  • Loading branch information
jurajpiar committed Sep 25, 2023
1 parent ede4006 commit 472a78e
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 237 deletions.
190 changes: 20 additions & 170 deletions core/bin/zksync_api/src/bin/dev_ticker_server.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -33,168 +18,33 @@ struct FeeTickerOpts {
sloppy: bool,
}

struct ResponseCache<T> {
data: T,
last_fetched: Instant,
}

#[derive(Clone)]
struct ProxyState {
testnet_to_mainnet_address_mapping: HashMap<Address, Address>,
cache: Arc<Mutex<HashMap<String, ResponseCache<Value>>>>,
}

fn load_tokens(path: impl AsRef<Path>) -> Result<Vec<TokenInfo>, serde_json::Error> {
let tokens = serde_json::from_str(&read_to_string(path).unwrap());

tokens
}

async fn proxy_request(
url: &str,
cache: &Mutex<HashMap<String, ResponseCache<Value>>>,
) -> 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::<Value>().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<ProxyState>, 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<ProxyState>,
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<Address, Address> = mainnet_tokens.iter().fold(
HashMap::<Address, Address>::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<HttpResponse> {
// }

// 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 {
vlog::info!("Fee ticker server will run in a sloppy mode.");
}

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()
Expand Down
1 change: 1 addition & 0 deletions core/bin/zksync_api/src/bin/providers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
pub mod dev_liquidity_provider;
pub mod dev_price_provider;
pub mod proxy_price_provider;
116 changes: 116 additions & 0 deletions core/bin/zksync_api/src/bin/providers/proxy_price_provider.rs
Original file line number Diff line number Diff line change
@@ -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<T> {
data: T,
last_fetched: Instant,
}

#[derive(Clone)]
struct ProxyState {
cache: Arc<Mutex<HashMap<String, ResponseCache<Value>>>>,
}

async fn proxy_request(
url: &str,
cache: &Mutex<HashMap<String, ResponseCache<Value>>>,
) -> 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::<Value>().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<ProxyState>, _: web::Path<(bool,)>) -> HttpResponse {
let rootstock_platform: HashMap<String, Option<String>> = 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<ProxyState>,
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),
)
}
Loading

0 comments on commit 472a78e

Please sign in to comment.