Skip to content

Commit

Permalink
Merge #1709
Browse files Browse the repository at this point in the history
1709: Use addresses of tokens in Coingecko r=Deniallugo a=perekopskiy



Co-authored-by: perekopskiy <[email protected]>
  • Loading branch information
bors-matterlabs-dev[bot] and perekopskiy authored Jun 23, 2021
2 parents f3aadb3 + 3b1b36b commit 5758051
Show file tree
Hide file tree
Showing 10 changed files with 153 additions and 81 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ jobs:
ci_run zk db basic-setup
ci_run zk run yarn
- name: liquidity-token
run: docker-compose -f docker-compose-runner.yml restart dev-liquidity-token-watcher
- name: restart dev-liquidity-token-watcher and dev-ticker
run: docker-compose -f docker-compose-runner.yml restart dev-liquidity-token-watcher dev-ticker

- name: contracts-unit-tests
run: ci_run zk test contracts
Expand Down Expand Up @@ -105,8 +105,8 @@ jobs:
ci_run zk dummy-prover enable --no-redeploy
ci_run zk init
- name: liquidity-token
run: docker-compose -f docker-compose-runner.yml restart dev-liquidity-token-watcher
- name: restart dev-liquidity-token-watcher and dev-ticker
run: docker-compose -f docker-compose-runner.yml restart dev-liquidity-token-watcher dev-ticker

- name: run-services
run: |
Expand Down
97 changes: 68 additions & 29 deletions core/bin/zksync_api/src/bin/dev-ticker-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ use bigdecimal::BigDecimal;
use chrono::{SecondsFormat, 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 structopt::StructOpt;
use zksync_crypto::rand::{thread_rng, Rng};
use zksync_types::Address;

#[derive(Debug, Serialize, Deserialize)]
struct CoinMarketCapTokenQuery {
Expand All @@ -20,7 +22,7 @@ struct CoinMarketCapTokenQuery {

macro_rules! make_sloppy {
($f: ident) => {{
|query| async {
|query, data| async {
if thread_rng().gen_range(0, 100) < 5 {
vlog::debug!("`{}` has been errored", stringify!($f));
return Ok(HttpResponse::InternalServerError().finish());
Expand All @@ -42,14 +44,15 @@ macro_rules! make_sloppy {
);
tokio::time::delay_for(duration).await;

let resp = $f(query).await;
let resp = $f(query, data).await;
resp
}
}};
}

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() {
Expand Down Expand Up @@ -82,36 +85,62 @@ async fn handle_coinmarketcap_token_price_query(
Ok(HttpResponse::Ok().json(resp))
}

async fn handle_coingecko_token_list(_req: HttpRequest) -> Result<HttpResponse> {
let resp = json!([
{"id": "ethereum", "symbol": "eth", "name": "Ethereum"},
{"id": "dai", "symbol":"dai", "name": "Dai"},
{"id": "glm", "symbol":"glm", "name": "Golem"},
{"id": "tglm", "symbol":"tglm", "name": "Golem"},
{"id": "usdc", "symbol":"usdc", "name": "usdc"},
{"id": "usdt", "symbol":"usdt", "name": "usdt"},
{"id": "tusd", "symbol":"tusd", "name": "tusd"},
{"id": "link", "symbol":"link", "name": "link"},
{"id": "ht", "symbol":"ht", "name": "ht"},
{"id": "omg", "symbol":"omg", "name": "omg"},
{"id": "trb", "symbol":"trb", "name": "trb"},
{"id": "zrx", "symbol":"zrx", "name": "zrx"},
{"id": "rep", "symbol":"rep", "name": "rep"},
{"id": "storj", "symbol":"storj", "name": "storj"},
{"id": "nexo", "symbol":"nexo", "name": "nexo"},
{"id": "mco", "symbol":"mco", "name": "mco"},
{"id": "knc", "symbol":"knc", "name": "knc"},
{"id": "lamb", "symbol":"lamb", "name": "lamb"},
{"id": "xem", "symbol":"xem", "name": "xem"},
{"id": "phnx", "symbol":"phnx", "name": "Golem"},
{"id": "basic-attention-token", "symbol": "bat", "name": "Basic Attention Token"},
{"id": "wrapped-bitcoin", "symbol": "wbtc", "name": "Wrapped Bitcoin"},
]);
#[derive(Debug, Deserialize)]
struct Token {
pub address: Address,
pub decimals: u8,
pub symbol: String,
}

Ok(HttpResponse::Ok().json(resp))
#[derive(Serialize, Deserialize, Clone, Debug)]
struct TokenData {
id: String,
symbol: String,
name: String,
platforms: HashMap<String, Address>,
}

fn load_tokens(path: impl AsRef<Path>) -> Vec<TokenData> {
if let Ok(text) = read_to_string(path) {
let tokens: Vec<Token> = serde_json::from_str(&text).unwrap();
let tokens_data: Vec<TokenData> = tokens
.into_iter()
.map(|token| {
let symbol = token.symbol.to_lowercase();
let mut platforms = HashMap::new();
platforms.insert(String::from("ethereum"), token.address);
let id = match symbol.as_str() {
"eth" => String::from("ethereum"),
"wbtc" => String::from("wrapped-bitcoin"),
"bat" => String::from("basic-attention-token"),
_ => symbol.clone(),
};

TokenData {
id,
symbol: symbol.clone(),
name: symbol,
platforms,
}
})
.collect();
tokens_data
} else {
Vec::new()
}
}

async fn handle_coingecko_token_list(
_req: HttpRequest,
data: web::Data<Vec<TokenData>>,
) -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json((*data.into_inner()).clone()))
}

async fn handle_coingecko_token_price_query(req: HttpRequest) -> Result<HttpResponse> {
async fn handle_coingecko_token_price_query(
req: HttpRequest,
_data: web::Data<Vec<TokenData>>,
) -> Result<HttpResponse> {
let coin_id = req.match_info().get("coin_id");
let base_price = match coin_id {
Some("ethereum") => BigDecimal::from(200),
Expand All @@ -133,8 +162,17 @@ async fn handle_coingecko_token_price_query(req: HttpRequest) -> Result<HttpResp
}

fn main_scope(sloppy_mode: bool) -> actix_web::Scope {
let localhost_tokens = load_tokens(&"etc/tokens/localhost.json");
let rinkeby_tokens = load_tokens(&"etc/tokens/rinkeby.json");
let ropsten_tokens = load_tokens(&"etc/tokens/ropsten.json");
let data: Vec<TokenData> = localhost_tokens
.into_iter()
.chain(rinkeby_tokens.into_iter())
.chain(ropsten_tokens.into_iter())
.collect();
if sloppy_mode {
web::scope("/")
.data(data)
.route(
"/cryptocurrency/quotes/latest",
web::get().to(make_sloppy!(handle_coinmarketcap_token_price_query)),
Expand All @@ -149,6 +187,7 @@ fn main_scope(sloppy_mode: bool) -> actix_web::Scope {
)
} else {
web::scope("/")
.data(data)
.route(
"/cryptocurrency/quotes/latest",
web::get().to(handle_coinmarketcap_token_price_query),
Expand Down
60 changes: 38 additions & 22 deletions core/bin/zksync_api/src/fee_ticker/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,12 +191,12 @@ struct ErrorTickerApi;

#[async_trait::async_trait]
impl TokenPriceAPI for ErrorTickerApi {
async fn get_price(&self, _token_symbol: &str) -> Result<TokenPrice, PriceError> {
async fn get_price(&self, _token: &Token) -> Result<TokenPrice, PriceError> {
Err(PriceError::token_not_found("Wrong token"))
}
}

fn run_server() -> (String, AbortHandle) {
fn run_server(token_address: Address) -> (String, AbortHandle) {
let mut url = None;
let mut server = None;
for i in 9000..9999 {
Expand All @@ -210,10 +210,15 @@ fn run_server() -> (String, AbortHandle) {
HttpResponse::MethodNotAllowed()
})),
)
.service(web::resource("/api/v3/coins/list").to(|| {
.service(web::resource("/api/v3/coins/list").to(move || {
let mut platforms = HashMap::new();
platforms.insert(
String::from("ethereum"),
serde_json::Value::String(serde_json::to_string(&token_address).unwrap()),
);
HttpResponse::Ok().json(CoinGeckoTokenList(vec![CoinGeckoTokenInfo {
id: "DAI".to_string(),
symbol: "DAI".to_string(),
id: "dai".to_string(),
platforms,
}]))
}))
})
Expand Down Expand Up @@ -387,7 +392,13 @@ fn test_zero_price_token_fee() {
#[ignore]
// It's ignore because we can't initialize coingecko in current way with block
async fn test_error_coingecko_api() {
let (address, handler) = run_server();
let token = Token {
id: TokenId(1),
address: Address::random(),
symbol: String::from("DAI"),
decimals: 18,
};
let (address, handler) = run_server(token.address);
let client = reqwest::ClientBuilder::new()
.timeout(CONNECTION_TIMEOUT)
.connect_timeout(CONNECTION_TIMEOUT)
Expand All @@ -402,20 +413,25 @@ async fn test_error_coingecko_api() {
FakeTokenWatcher,
);
let connection_pool = ConnectionPool::new(Some(1));
connection_pool
.access_storage()
.await
.unwrap()
.tokens_schema()
.update_historical_ticker_price(
TokenId(1),
TokenPrice {
usd_price: big_decimal_to_ratio(&BigDecimal::from(10)).unwrap(),
last_updated: chrono::offset::Utc::now(),
},
)
.await
.unwrap();
{
let mut storage = connection_pool.access_storage().await.unwrap();
storage
.tokens_schema()
.store_token(token.clone())
.await
.unwrap();
storage
.tokens_schema()
.update_historical_ticker_price(
token.id,
TokenPrice {
usd_price: big_decimal_to_ratio(&BigDecimal::from(10)).unwrap(),
last_updated: chrono::offset::Utc::now(),
},
)
.await
.unwrap();
}
let ticker_api = TickerApi::new(connection_pool, coingecko);

let config = get_test_ticker_config();
Expand All @@ -430,13 +446,13 @@ async fn test_error_coingecko_api() {
ticker
.get_fee_from_ticker_in_wei(
TxFeeTypes::FastWithdraw,
TokenId(1).into(),
token.id.into(),
Address::default(),
)
.await
.unwrap();
ticker
.get_token_price(TokenId(1).into(), TokenPriceRequestType::USDForOneWei)
.get_token_price(token.id.into(), TokenPriceRequestType::USDForOneWei)
.await
.unwrap();
}
Expand Down
50 changes: 29 additions & 21 deletions core/bin/zksync_api/src/fee_ticker/ticker_api/coingecko.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ use num::BigUint;
use reqwest::Url;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::str::FromStr;
use std::time::Instant;
use zksync_types::TokenPrice;
use zksync_utils::UnsignedRatioSerializeAsDecimal;
use zksync_types::{Address, Token, TokenPrice};
use zksync_utils::{remove_prefix, UnsignedRatioSerializeAsDecimal};

#[derive(Debug, Clone)]
pub struct CoinGeckoAPI {
base_url: Url,
client: reqwest::Client,
token_ids: HashMap<String, String>,
token_ids: HashMap<Address, String>,
}

impl CoinGeckoAPI {
pub fn new(client: reqwest::Client, base_url: Url) -> anyhow::Result<Self> {
let token_list_url = base_url
.join("api/v3/coins/list")
.join("api/v3/coins/list?include_platform=true")
.expect("failed to join URL path");

let token_list = reqwest::blocking::get(token_list_url)
Expand All @@ -30,8 +31,19 @@ impl CoinGeckoAPI {

let mut token_ids = HashMap::new();
for token in token_list.0 {
token_ids.insert(token.symbol, token.id);
if let Some(address_value) = token.platforms.get("ethereum") {
if let Some(address_str) = address_value.as_str() {
let address_str = remove_prefix(address_str);
if let Ok(address) = Address::from_str(address_str) {
token_ids.insert(address, token.id);
}
}
}
}

// Add ETH manually because coingecko API doesn't return address for it.
token_ids.insert(Address::default(), String::from("ethereum"));

Ok(Self {
base_url,
client,
Expand All @@ -42,21 +54,15 @@ impl CoinGeckoAPI {

#[async_trait]
impl TokenPriceAPI for CoinGeckoAPI {
async fn get_price(&self, token_symbol: &str) -> Result<TokenPrice, PriceError> {
async fn get_price(&self, token: &Token) -> Result<TokenPrice, PriceError> {
let start = Instant::now();
let token_lowercase_symbol = token_symbol.to_lowercase();
let token_id = self
.token_ids
.get(&token_lowercase_symbol)
.or_else(|| self.token_ids.get(token_symbol))
.unwrap_or(&token_lowercase_symbol);
// TODO ZKS-595. Uncomment this code
// .ok_or_else(|| {
// PriceError::token_not_found(format!(
// "Token '{}' is not listed on CoinGecko",
// token_symbol
// ))
// })?;
let token_symbol = token.symbol.as_str();
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
))
})?;

let market_chart_url = self
.base_url
Expand Down Expand Up @@ -117,7 +123,7 @@ impl TokenPriceAPI for CoinGeckoAPI {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CoinGeckoTokenInfo {
pub(crate) id: String,
pub(crate) symbol: String,
pub(crate) platforms: HashMap<String, serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -137,14 +143,16 @@ pub struct CoinGeckoMarketChart {
#[cfg(test)]
mod tests {
use super::*;
use zksync_types::TokenId;
use zksync_utils::parse_env;

#[tokio::test]
async fn test_coingecko_api() {
let ticker_url = parse_env("FEE_TICKER_COINGECKO_BASE_URL");
let client = reqwest::Client::new();
let api = CoinGeckoAPI::new(client, ticker_url).unwrap();
api.get_price("ETH")
let token = Token::new(TokenId(0), Default::default(), "ETH", 18);
api.get_price(&token)
.await
.expect("Failed to get data from ticker");
}
Expand Down
Loading

0 comments on commit 5758051

Please sign in to comment.