Skip to content

Commit

Permalink
ROLLUP-379: modifies dev ticker to proxy to coingecko (#101)
Browse files Browse the repository at this point in the history
* feat(ticker): modifies dev ticker to proxy to coingecko

feat(ticker): separates dev and proxy tickers

feat(ticker): adds proxy liquidity

* chore: remove debug println

* build: install sscache with '--locked' to avoid build error

---------

Co-authored-by: Antonio Morrone <[email protected]>
  • Loading branch information
jurajpiar and antomor authored Oct 16, 2023
1 parent fe61bdd commit 3e1c16f
Show file tree
Hide file tree
Showing 20 changed files with 601 additions and 121 deletions.
43 changes: 37 additions & 6 deletions core/bin/zksync_api/src/bin/dev_ticker_server.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
use actix_cors::Cors;
use actix_web::{middleware, App, HttpServer};
use actix_web::{middleware, web, 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,
proxy_utils::ProxyState,
};
use std::{
collections::HashMap,
fs::read_to_string,
path::{Path, PathBuf},
};
use structopt::StructOpt;
use tokio::sync::Mutex;
use zksync_config::ZkSyncConfig;
use zksync_types::{network::Network, TokenInfo};
use zksync_utils::parse_env;

mod providers;

Expand All @@ -18,21 +28,42 @@ struct FeeTickerOpts {
sloppy: bool,
}

fn load_tokens(path: impl AsRef<Path>) -> Result<Vec<TokenInfo>, serde_json::Error> {
let mut full_path = parse_env::<PathBuf>("ZKSYNC_HOME");
full_path.push(path);
serde_json::from_str(&read_to_string(full_path).unwrap())
}

#[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.");
}

let shared_data = web::Data::new(ProxyState {
cache: std::sync::Arc::new(Mutex::new(HashMap::new())),
});

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
.app_data(shared_data.clone())
.service(proxy_price_provider::create_price_service())
.service(proxy_liquidity_provider::config_liquidity_app()),
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
6 changes: 6 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,8 @@
pub mod dev_liquidity_provider;
pub mod dev_price_provider;
pub mod proxy_liquidity_provider;
pub mod proxy_price_provider;
pub mod proxy_utils;

#[cfg(test)]
pub(crate) mod test_utils;
158 changes: 158 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,158 @@
use std::str::FromStr;

use actix_web::{web, FromRequest, HttpResponse, Result, Scope};
use zksync_api::fee_ticker::CoinGeckoTypes::AssetPlatform;
use zksync_config::ETHClientConfig;
use zksync_types::Address;
use zksync_utils::remove_prefix;

use crate::load_tokens;

use super::proxy_utils::{cache_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";
pub(crate) 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),
}]))
}

async fn handle_get_coin_contract(request: web::HttpRequest) -> HttpResponse {
let mainnet_tokens = load_tokens("etc/tokens/mainnet.json").unwrap();
let testnet_tokens = load_tokens("etc/tokens/testnet.json").unwrap();
let data: &web::Data<ProxyState> = request.app_data().unwrap();
let path = web::Path::<(String, String)>::extract(&request)
.await
.unwrap();

let (_, contract_address) = path.into_inner();
let testnet_token_address = Address::from_str(remove_prefix(&contract_address)).unwrap();

let testnet_token = testnet_tokens
.iter()
.find(|token| token.address.eq(&testnet_token_address));
let mainnet_token = match testnet_token {
Some(testnet_token) => 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 query = request.query_string();
let forward_url = format!(
"{}{}/coins/{}/contract/{:#x}?{}",
API_URL,
API_PATH,
ROOTSTOCK_PLATFORM_ID,
match mainnet_token {
Some(token) => token.address,
None => testnet_token_address,
},
query
);

cache_proxy_request(&reqwest::Client::new(), &forward_url, &data.cache).await
}

pub(crate) fn config_liquidity_app() -> Scope {
web::scope("")
.service(web::resource("/asset_platforms").route(web::get().to(handle_get_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)),
),
),
),
)
}

#[cfg(test)]
mod handle_get_coin_contract_tests {
use std::collections::HashMap;

use super::*;
use actix_web::{test, App};
use tokio::sync::Mutex;
use zksync_api::fee_ticker::CoinGeckoTypes::ContractSimplified;
use zksync_types::TokenInfo;

#[actix_web::test]
async fn returns_mainnet_token() {
let testnet_token = TokenInfo {
// Testnet RIF token address
address: Address::from_str("0x19f64674D8a5b4e652319F5e239EFd3bc969a1FE").unwrap(),
decimals: 0,
symbol: "tRIF".to_string(),
};

let expected_uri = format!(
"/coins/{}/contract/{:#x}",
TESTNET_PLATFORM_ID, testnet_token.address
);

let request = test::TestRequest::get().uri(&expected_uri.clone());

let test_app = test::init_service(
#[allow(deprecated)]
// Allowed deprecated .data function as .app_data is not working inside the test service
App::new()
.data(ProxyState {
cache: std::sync::Arc::new(Mutex::new(HashMap::new())),
})
.configure(|cfg| {
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)),
),
),
),
);
})
.service(
web::resource("/{contract_address}")
.route(web::get().to(handle_get_coin_contract)),
),
)
.await;

let response = test::call_service(&test_app, request.to_request()).await;
assert!(response.status().is_success());

let body = response.into_body();
let bytes = actix_web::body::to_bytes(body).await.unwrap();
let result = String::from_utf8(bytes.to_vec()).unwrap();

let ContractSimplified {
liquidity_score,
market_data,
} = serde_json::from_str(&result).unwrap();

assert!(
liquidity_score > 0.0,
"Liquidity score is not greater than 0"
);
assert!(
market_data.total_volume.usd.is_some() && market_data.total_volume.usd.unwrap() > 0.0,
"Total volume in USD is not greater than 0"
);
}
}
Loading

0 comments on commit 3e1c16f

Please sign in to comment.