Skip to content

Commit

Permalink
feat(ticker): translates addresses in client
Browse files Browse the repository at this point in the history
  • Loading branch information
jurajpiar committed Sep 15, 2023
1 parent e0c8c1b commit 375c770
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 87 deletions.
89 changes: 8 additions & 81 deletions core/bin/zksync_api/src/bin/providers/dev_price_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -22,77 +18,6 @@ struct CoinMarketCapTokenQuery {
symbol: String,
}

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

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

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 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<ProxyState>) -> HttpResponse {
proxy_request(
"https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest",
&data.cache,
)
.await
}

async fn proxy_request(url: &str, cache: &Mutex<HashMap<String, Cache<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(),
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 {
Expand Down Expand Up @@ -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),
)
}
}
71 changes: 67 additions & 4 deletions core/bin/zksync_api/src/fee_ticker/ticker_api/coingecko.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,69 @@ 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)]
pub struct CoinGeckoAPI {
base_url: Url,
client: reqwest::Client,
token_ids: HashMap<Address, String>,
tnet_to_mnet_address_mapping: HashMap<Address, Address>,
}

#[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<R: std::io::Read>(reader: R) -> anyhow::Result<Vec<Self>> {
let tokens: Vec<Self> = serde_json::from_reader(reader)?;
Ok(tokens)
}
}

impl CoinGeckoAPI {
pub async fn new(client: reqwest::Client, base_url: Url) -> anyhow::Result<Self> {
// 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> = TokenInfo::from_json(testnet_tokens_file)?;
let mainnet_tokens: Vec<TokenInfo> = 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");
Expand Down Expand Up @@ -50,18 +98,33 @@ impl CoinGeckoAPI {
base_url,
client,
token_ids,
tnet_to_mnet_address_mapping: address_mapping,
})
}
}

#[async_trait]
impl TokenPriceAPI for CoinGeckoAPI {
async fn get_price(&self, token: &Token) -> Result<TokenPrice, PriceError> {
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
))
})?;

Expand Down
4 changes: 2 additions & 2 deletions etc/tokens/testnet.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]

0 comments on commit 375c770

Please sign in to comment.