Skip to content

Commit

Permalink
feat(ticker): modifies dev ticker to proxy to coingecko
Browse files Browse the repository at this point in the history
feat(ticker): separates dev and proxy tickers

feat(ticker): adds proxy liquidity

refactor(ticker): url and path to constants

fix(ticker): trimms testnet prefix in loaded token symbol
  • Loading branch information
jurajpiar committed Sep 26, 2023
1 parent a3fdb5c commit 02b889b
Show file tree
Hide file tree
Showing 16 changed files with 391 additions and 100 deletions.
160 changes: 129 additions & 31 deletions Cargo.lock

Large diffs are not rendered by default.

22 changes: 17 additions & 5 deletions core/bin/zksync_api/src/bin/dev_ticker_server.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use actix_cors::Cors;
use actix_web::{middleware, App, HttpServer};
use providers::{
dev_liquidity_provider::config_liquidity_app, dev_price_provider::create_price_service,
dev_liquidity_provider, dev_price_provider, proxy_liquidity_provider, proxy_price_provider,
};
use structopt::StructOpt;
use zksync_config::ZkSyncConfig;
use zksync_types::network::Network;

mod providers;

Expand All @@ -21,18 +23,28 @@ struct FeeTickerOpts {
#[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(opts.sloppy))
.wrap(middleware::Logger::default());
match network {
Network::Testnet => base_app
.configure(proxy_liquidity_provider::config_liquidity_app)
.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
66 changes: 9 additions & 57 deletions core/bin/zksync_api/src/bin/providers/dev_price_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@
use actix_web::{web, HttpRequest, HttpResponse, Result};
use bigdecimal::BigDecimal;
use chrono::{SecondsFormat, Utc};
use chrono::Utc;
use serde::{Deserialize, Serialize};
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};
use zksync_types::Address;

use super::proxy_utils::API_PATH;

#[derive(Debug, Serialize, Deserialize)]
struct CoinMarketCapTokenQuery {
symbol: String,
Expand Down Expand Up @@ -48,45 +50,6 @@ macro_rules! make_sloppy {
}};
}

async fn handle_coinmarketcap_token_price_query(
query: web::Query<CoinMarketCapTokenQuery>,
_data: web::Data<Vec<TokenData>>,
) -> Result<HttpResponse> {
let symbol = query.symbol.clone();
let base_price = match symbol.as_str() {
"RBTC" => BigDecimal::from(1800),
"wBTC" => BigDecimal::from(9000),
// Even though these tokens have their base price equal to
// the default one, we still keep them here so that in the future it would
// be easier to change the default price without affecting the important tokens
"DAI" => BigDecimal::from(1),
"tGLM" => BigDecimal::from(1),
"GLM" => BigDecimal::from(1),

"RIF" => BigDecimal::try_from(0.053533).unwrap(),
_ => BigDecimal::from(1),
};
let random_multiplier = thread_rng().gen_range(0.9, 1.1);

let price = base_price * BigDecimal::try_from(random_multiplier).unwrap();

let last_updated = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let resp = json!({
"data": {
symbol: {
"quote": {
"USD": {
"price": price.to_string(),
"last_updated": last_updated
}
}
}
}
});
vlog::info!("1.0 {} = {} USD", query.symbol, price);
Ok(HttpResponse::Ok().json(resp))
}

#[derive(Debug, Deserialize)]
struct Token {
pub address: Address,
Expand Down Expand Up @@ -177,33 +140,22 @@ pub fn create_price_service(sloppy_mode: bool) -> actix_web::Scope {
.chain(testnet_tokens.into_iter())
.collect();
if sloppy_mode {
web::scope("")
web::scope(API_PATH)
.app_data(web::Data::new(data))
.route(
"/cryptocurrency/quotes/latest",
web::get().to(make_sloppy!(handle_coinmarketcap_token_price_query)),
)
.route(
"/api/v3/coins/list",
"/coins/list",
web::get().to(make_sloppy!(handle_coingecko_token_list)),
)
.route(
"/api/v3/coins/{coin_id}/market_chart",
"/coins/{coin_id}/market_chart",
web::get().to(make_sloppy!(handle_coingecko_token_price_query)),
)
} else {
web::scope("")
web::scope(API_PATH)
.app_data(web::Data::new(data))
.route("/coins/list", web::get().to(handle_coingecko_token_list))
.route(
"/cryptocurrency/quotes/latest",
web::get().to(handle_coinmarketcap_token_price_query),
)
.route(
"/api/v3/coins/list",
web::get().to(handle_coingecko_token_list),
)
.route(
"/api/v3/coins/{coin_id}/market_chart",
"/coins/{coin_id}/market_chart",
web::get().to(handle_coingecko_token_price_query),
)
}
Expand Down
3 changes: 3 additions & 0 deletions core/bin/zksync_api/src/bin/providers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
pub mod dev_liquidity_provider;
pub mod dev_price_provider;
pub mod proxy_liquidity_provider;
pub mod proxy_price_provider;
mod proxy_utils;
91 changes: 91 additions & 0 deletions core/bin/zksync_api/src/bin/providers/proxy_liquidity_provider.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
use std::{collections::HashMap, fs::read_to_string, path::Path, str::FromStr};

use actix_web::{web, HttpResponse, Result};
use tokio::sync::Mutex;
use zksync_api::fee_ticker::CoinGeckoTypes::AssetPlatform;
use zksync_config::ETHClientConfig;
use zksync_types::{Address, TokenInfo};
use zksync_utils::remove_prefix;

use super::proxy_utils::{proxy_request, ProxyState, API_PATH, API_URL};

const TESTNET_PLATFORM_ID: &str = "testnet";
const TESTNET_PLATFORM_NAME: &str = "Rootstock Testnet";
const TESTNET_PLATFORM_SHORTNAME: &str = "testnet";
const ROOTSTOCK_PLATFORM_ID: &str = "rootstock";

async fn handle_get_asset_platforms() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json(vec![AssetPlatform {
id: String::from(TESTNET_PLATFORM_ID),
chain_identifier: Some(ETHClientConfig::from_env().chain_id as i64),
name: String::from(TESTNET_PLATFORM_NAME),
shortname: String::from(TESTNET_PLATFORM_SHORTNAME),
}]))
}

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

async fn handle_get_coin_contract(
path: web::Path<(String, String)>,
data: web::Data<AppState>,
) -> HttpResponse {
let (_, contract_address) = path.into_inner();
let testnet_token_address = Address::from_str(remove_prefix(&contract_address)).unwrap();

let testnet_token = data
.testnet_tokens
.iter()
.find(|token| token.address.eq(&testnet_token_address));
let mainnet_token = match testnet_token {
Some(testnet_token) => data.mainnet_tokens.iter().find(|token| {
let mainnet_symbol = token.symbol.to_uppercase();
let testnet_symbol = testnet_token.symbol.to_uppercase();

mainnet_symbol.eq(match testnet_symbol.len().gt(&mainnet_symbol.len()) {
true => testnet_symbol.trim_start_matches('T'),
false => &testnet_symbol,
})
}),
None => None,
};

let url = format!(
"{}{}/coins/{}/market_chart/{}",
API_URL,
API_PATH,
ROOTSTOCK_PLATFORM_ID,
match mainnet_token {
Some(token) => token.address,
None => testnet_token_address,
},
);

proxy_request(&url, &data.proxy_state.cache).await
}

struct AppState {
mainnet_tokens: Vec<TokenInfo>,
testnet_tokens: Vec<TokenInfo>,
proxy_state: ProxyState,
}

pub fn config_liquidity_app(cfg: &mut web::ServiceConfig) {
let shared_data = AppState {
mainnet_tokens: load_tokens("etc/tokens/mainnet.json").unwrap(),
testnet_tokens: load_tokens("etc/tokens/testnet.json").unwrap(),
proxy_state: ProxyState {
cache: std::sync::Arc::new(Mutex::new(HashMap::new())),
},
};
cfg.app_data(web::Data::new(shared_data));
cfg.service(web::resource("/asset_platforms").route(web::get().to(handle_get_asset_platforms)));
cfg.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)),
),
)),
);
}
57 changes: 57 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,57 @@
use std::collections::HashMap;
use tokio::sync::Mutex;

use actix_web::{web, HttpResponse, Scope};
use zksync_api::fee_ticker::CoinGeckoTypes::CoinsListItem;

use super::proxy_utils::{proxy_request, ProxyState, API_PATH, API_URL};

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!("{}{}/coins/{}/market_chart", API_URL, API_PATH, coin_id);

proxy_request(&url, &data.cache).await
}

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(API_PATH)
.app_data(web::Data::new(shared_data))
.route("/coins/list", web::get().to(fetch_coins_list))
.route(
"/coins/{coin_id}/market_chart",
web::get().to(fetch_market_chart),
)
}
60 changes: 60 additions & 0 deletions core/bin/zksync_api/src/bin/providers/proxy_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use std::{
collections::HashMap,
sync::Arc,
time::{Duration, Instant},
};
use tokio::sync::Mutex;

use actix_web::HttpResponse;
use serde_json::Value;
use zksync_config::DevTickerConfig;

pub(crate) const API_URL: &str = "https://api.coingecko.com";
pub(crate) const API_PATH: &str = "/api/v3";

pub(crate) struct ResponseCache<T> {
data: T,
last_fetched: Instant,
}

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

pub(crate) 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(DevTickerConfig::from_env().proxy_cache_timout as u64)
{
// 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(),
}
}
13 changes: 13 additions & 0 deletions core/bin/zksync_api/src/fee_ticker/validator/types.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

// --------- locally simplified Contract struct for retreiving market data only
Expand All @@ -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<HashMap<String, Option<String>>>,
}
Loading

0 comments on commit 02b889b

Please sign in to comment.