From f33a835b5aeb5a3dbdd840b7f0732cc6d76b1e2c Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 25 Sep 2024 21:58:55 +0900 Subject: [PATCH 1/7] feat: adds cmc api --- core/lib/external_price_api/src/cmc_api.rs | 531 +++++++++++++++++++++ core/lib/external_price_api/src/lib.rs | 1 + 2 files changed, 532 insertions(+) create mode 100644 core/lib/external_price_api/src/cmc_api.rs diff --git a/core/lib/external_price_api/src/cmc_api.rs b/core/lib/external_price_api/src/cmc_api.rs new file mode 100644 index 00000000000..99b09e78c15 --- /dev/null +++ b/core/lib/external_price_api/src/cmc_api.rs @@ -0,0 +1,531 @@ +use std::{collections::HashMap, str::FromStr}; + +use async_trait::async_trait; +use chrono::Utc; +use serde::Deserialize; +use tokio::sync::RwLock; +use url::Url; +use zksync_config::configs::ExternalPriceApiClientConfig; +use zksync_types::{base_token_ratio::BaseTokenAPIRatio, Address}; + +use crate::{utils::get_fraction, PriceAPIClient}; + +const CMC_AUTH_HEADER: &str = "x-cmc_pro_api_key"; +const DEFAULT_CMC_API_URL: &str = "https://pro-api.coinmarketcap.com"; +// it's safe to have id hardcoded as they are stable as claimed by CMC +// const CMC_ETH_ID: i32 = 1027; +const SUPPORT_TOKENS_ONLY_ON_CMC_PLATFORM_ID: i32 = 1; // 1 = Ethereum + +#[derive(Debug)] +pub struct CMCPriceAPIClient { + base_url: Url, + client: reqwest::Client, + cache_token_id_by_address: RwLock>, +} + +impl CMCPriceAPIClient { + pub fn new(config: ExternalPriceApiClientConfig) -> Self { + let client = if let Some(api_key) = &config.api_key { + use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; + + let mut headers = HeaderMap::new(); + headers.insert( + HeaderName::from_static(CMC_AUTH_HEADER), + HeaderValue::from_str(api_key).expect("Failed to create header value"), + ); + + reqwest::Client::builder().default_headers(headers) + } else { + reqwest::Client::builder() + } + .timeout(config.client_timeout()) + .build() + .expect("Failed to build reqwest client"); + + let base_url = config.base_url.unwrap_or(DEFAULT_CMC_API_URL.to_string()); + + Self { + base_url: Url::parse(&base_url).expect("Failed to parse CoinMarketCap API URL"), + client, + cache_token_id_by_address: RwLock::new(HashMap::new()), + } + } + + fn get(&self, path: &str) -> reqwest::RequestBuilder { + self.client + .get(self.base_url.join(path).expect("Failed to join URL path")) + } + + async fn get_token_id(&self, address: Address) -> anyhow::Result { + if let Some(x) = self.cache_token_id_by_address.read().await.get(&address) { + return Ok(*x); + } + // drop read lock + + let response = self.get("/v1/cryptocurrency/map").send().await?; + let status = response.status(); + if !status.is_success() { + return Err(anyhow::anyhow!( + "Http error while fetching token id. Status: {status}, token: {address}, msg: {}", + response.text().await.unwrap_or_default(), + )); + } + + let parsed = response.json::().await?; + for token_info in parsed.data { + if let Some(platform) = token_info.platform { + if platform.id == SUPPORT_TOKENS_ONLY_ON_CMC_PLATFORM_ID + && Address::from_str(&platform.token_address).is_ok_and(|a| a == address) + { + self.cache_token_id_by_address + .write() + .await + .insert(address, token_info.id); + return Ok(token_info.id); + } + } + } + + Err(anyhow::anyhow!("Token ID not found for address {address}")) + } + + async fn get_token_price_by_address(&self, address: Address) -> anyhow::Result { + let id = self.get_token_id(address).await?; + self.get_token_price_by_id(id).await + } + + async fn get_token_price_by_id(&self, id: i32) -> anyhow::Result { + let response = self + .get("/v2/cryptocurrency/quotes/latest") + .query(&[("id", id)]) + .send() + .await?; + + let status = response.status(); + if !status.is_success() { + return Err(anyhow::anyhow!( + "Http error while fetching token price. Status: {status}, token: {id}, msg: {}", + response.text().await.unwrap_or("".to_string()) + )); + } + + response + .json::() + .await? + .data + .get(&id) + .and_then(|data| data.quote.get("USD")) + .map(|mq| mq.price) + .ok_or_else(|| anyhow::anyhow!("Price not found for token: {id}")) + } +} + +#[derive(Debug, Deserialize)] +struct V2CryptocurrencyQuotesLatestResponse { + data: HashMap, +} + +#[derive(Debug, Deserialize)] +struct CryptocurrencyQuoteObject { + // #[serde(flatten)] + // cryptocurrency_object: CryptocurrencyObject, + quote: HashMap, +} + +#[derive(Debug, Deserialize)] +struct MarketQuote { + price: f64, + // last_updated: chrono::DateTime, // TODO: Recency? +} + +#[derive(Debug, Deserialize)] +struct V1CryptocurrencyMapResponse { + data: Vec, +} + +#[derive(Debug, Deserialize)] +struct CryptocurrencyObject { + id: i32, + // name: String, + // symbol: String, + // slug: String, + // is_active: u8, // TODO: This field is available, should we at least emit a warning if the listing is not marked as active? + platform: Option, +} + +#[derive(Debug, Deserialize)] +struct CryptocurrencyPlatform { + id: i32, + // name: String, + token_address: String, +} + +#[async_trait] +impl PriceAPIClient for CMCPriceAPIClient { + async fn fetch_ratio(&self, token_address: Address) -> anyhow::Result { + let base_token_in_eth = self.get_token_price_by_address(token_address).await?; + let (numerator, denominator) = get_fraction(base_token_in_eth); + + return Ok(BaseTokenAPIRatio { + numerator, + denominator, + ratio_timestamp: Utc::now(), // TODO: Should this be now (as written), or should it be the time returned by the API? + }); + } +} + +#[cfg(test)] +mod irl_tests { + use super::*; + + #[tokio::test] + #[ignore = "run manually; specify CoinMarketCap API key in env var CMC_API_KEY"] + async fn test() { + let client = CMCPriceAPIClient::new(ExternalPriceApiClientConfig { + api_key: Some(std::env::var("CMC_API_KEY").unwrap()), + base_url: None, + client_timeout_ms: 5000, + source: String::new(), + forced: None, + }); + + let tether: Address = "0xdac17f958d2ee523a2206206994597c13d831ec7" + .parse() + .unwrap(); + + let r = client.get_token_price_by_address(tether).await.unwrap(); + + assert!((r - 1f64).abs() < 0.001, "USDT lost its peg"); + + println!("{r}"); + } +} + +/* +#[cfg(test)] +mod tests { + use std::{collections::HashMap, str::FromStr}; + + use bigdecimal::BigDecimal; + use httpmock::{Mock, MockServer}; + use zksync_types::{base_token_price::BaseTokenAPIPrice, Address}; + + use crate::{ + address_to_string, + cmc_api::{CMCPriceAPIClient, CMC_AUTH_HEADER, CMC_ETH_ID}, + tests::tests::{ + add_mock, base_token_price_not_found_test, eth_price_not_found_test, happy_day_test, + no_base_token_price_404_test, no_eth_price_404_test, server_url, + }, + PriceAPIClient, + }; + + const TEST_API_KEY: &str = "test"; + + fn mock_crypto_map<'a>( + server: &'a MockServer, + address: &'a Address, + mock_id: &'a String, + ) -> Mock<'a> { + let address_str = address_to_string(address); + let body = format!( + r#"{{ + "data": [ + {{ + "id": 9999, + "platform": {{ + "name": "Ethereum2", + "token_address": "{}" + }} + }}, + {{ + "id": {}, + "platform": {{ + "name": "Ethereum", + "token_address": "{}" + }} + }} + ] + }}"#, + address_str, mock_id, address_str + ); + add_mock( + server, + httpmock::Method::GET, + "/v1/cryptocurrency/map".to_string(), + HashMap::new(), + 200, + body, + CMC_AUTH_HEADER.to_string(), + Some(TEST_API_KEY.to_string()), + ) + } + + fn add_mock_by_id<'a>( + server: &'a MockServer, + id: &'a String, + price: &'a String, + currency: &'a String, + ) -> Mock<'a> { + let body = format!( + r#"{{ + "data": {{ + "{}": {{ + "quote": {{ + "{}": {{ + "price": {} + }} + }} + }} + }} + }}"#, + id, currency, price + ); + let mut params = HashMap::new(); + params.insert("id".to_string(), id.clone()); + add_mock( + server, + httpmock::Method::GET, + "/v1/cryptocurrency/quotes/latest".to_string(), + params, + 200, + body, + CMC_AUTH_HEADER.to_string(), + Some(TEST_API_KEY.to_string()), + ) + } + + fn happy_day_setup( + server: &MockServer, + api_key: Option, + address: Address, + base_token_price: f64, + eth_price: f64, + ) -> Box { + let id = "50".to_string(); + let currency = "USD".to_string(); + mock_crypto_map(server, &address, &id); + add_mock_by_id(server, &id, &base_token_price.to_string(), ¤cy); + add_mock_by_id( + server, + &CMC_ETH_ID.to_string(), + ð_price.to_string(), + ¤cy, + ); + Box::new(CMCPriceAPIClient::new( + server_url(&server), + api_key.unwrap(), + reqwest::Client::new(), + )) + } + + #[tokio::test] + async fn test_happy_day() { + happy_day_test(Some(TEST_API_KEY.to_string()), happy_day_setup).await + } + + #[tokio::test] + async fn test_no_token_id() { + let server = MockServer::start(); + let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; + let address = Address::from_str(address_str).unwrap(); + let id = "50".to_string(); + let base_token_price = 198.9; + let eth_price = 3000.0; + let currency = "USD".to_string(); + + // the response will be missing the token that we are seeking for + mock_crypto_map( + &server, + &Address::from_str("0x3Bad7800d9149B53Cba5da927E6449e4A3487a1F").unwrap(), + &"123".to_string(), + ); + add_mock_by_id(&server, &id, &base_token_price.to_string(), ¤cy); + add_mock_by_id( + &server, + &CMC_ETH_ID.to_string(), + ð_price.to_string(), + ¤cy, + ); + + let mut client = CMCPriceAPIClient::new( + server_url(&server), + TEST_API_KEY.to_string(), + reqwest::Client::new(), + ); + let api_price = client.fetch_price(address).await; + + assert!(api_price.is_err()); + let msg = api_price.err().unwrap().to_string(); + assert_eq!( + "Token id not found for address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984".to_string(), + msg + ) + } + + #[tokio::test] + async fn should_reuse_token_id_from_map() { + let server = MockServer::start(); + let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; + let address = Address::from_str(address_str).unwrap(); + let base_token_price = 198.9; + let eth_price = 3000.0; + let id = "50".to_string(); + let currency = "USD".to_string(); + + let cm_mock = mock_crypto_map(&server, &address, &id); + add_mock_by_id(&server, &id, &base_token_price.to_string(), ¤cy); + add_mock_by_id( + &server, + &CMC_ETH_ID.to_string(), + ð_price.to_string(), + ¤cy, + ); + let mut client = CMCPriceAPIClient::new( + server_url(&server), + TEST_API_KEY.to_string(), + reqwest::Client::new(), + ); + + client.fetch_price(address).await.unwrap(); + let api_price = client.fetch_price(address).await.unwrap(); + + assert_eq!( + BaseTokenAPIPrice { + base_token_price: BigDecimal::from_str(&base_token_price.to_string()).unwrap(), + eth_price: BigDecimal::from_str(ð_price.to_string()).unwrap(), + ratio_timestamp: api_price.ratio_timestamp, + }, + api_price + ); + // crypto map should be fetched only once + assert_eq!(1, cm_mock.hits()); + } + + #[tokio::test] + async fn test_no_eth_price_404() { + no_eth_price_404_test( + Some(TEST_API_KEY.to_string()), + |server: &MockServer, + api_key: Option, + address: Address, + _base_token_price: f64, + _eth_price: f64| + -> Box { + let id = "50".to_string(); + mock_crypto_map(&server, &address, &id); + add_mock_by_id(&server, &id, &"123".to_string(), &"USD".to_string()); + + Box::new(CMCPriceAPIClient::new( + server_url(&server), + api_key.unwrap(), + reqwest::Client::new(), + )) + }, + ) + .await; + } + + #[tokio::test] + async fn test_eth_price_not_found() { + eth_price_not_found_test( + Some(TEST_API_KEY.to_string()), + |server: &MockServer, + api_key: Option, + address: Address, + _base_token_price: f64, + _eth_price: f64| + -> Box { + let id = "50".to_string(); + mock_crypto_map(&server, &address, &id); + add_mock_by_id(&server, &id, &"123".to_string(), &"USD".to_string()); + let mut params = HashMap::new(); + params.insert("id".to_string(), CMC_ETH_ID.to_string()); + add_mock( + server, + httpmock::Method::GET, + "/v1/cryptocurrency/quotes/latest".to_string(), + params, + 200, + "{}".to_string(), + CMC_AUTH_HEADER.to_string(), + Some(TEST_API_KEY.to_string()), + ); + Box::new(CMCPriceAPIClient::new( + server_url(&server), + api_key.unwrap(), + reqwest::Client::new(), + )) + }, + ) + .await; + } + + #[tokio::test] + async fn test_no_base_token_price_404() { + no_base_token_price_404_test( + Some(TEST_API_KEY.to_string()), + |server: &MockServer, + api_key: Option, + address: Address, + _base_token_price: f64, + _eth_price: f64| + -> Box { + mock_crypto_map(&server, &address, &"55".to_string()); + add_mock_by_id( + &server, + &CMC_ETH_ID.to_string(), + &"3900.12".to_string(), + &"USD".to_string(), + ); + Box::new(CMCPriceAPIClient::new( + server_url(&server), + api_key.unwrap(), + reqwest::Client::new(), + )) + }, + ) + .await; + } + + #[tokio::test] + async fn test_base_token_price_not_found() { + base_token_price_not_found_test( + Some(TEST_API_KEY.to_string()), + |server: &MockServer, + api_key: Option, + address: Address, + _base_token_price: f64, + _eth_price: f64| + -> Box { + let id = "55".to_string(); + mock_crypto_map(&server, &address, &id); + add_mock_by_id( + &server, + &CMC_ETH_ID.to_string(), + &"3900.12".to_string(), + &"USD".to_string(), + ); + let mut params = HashMap::new(); + params.insert("id".to_string(), id); + add_mock( + server, + httpmock::Method::GET, + "/v1/cryptocurrency/quotes/latest".to_string(), + params, + 200, + "{}".to_string(), + CMC_AUTH_HEADER.to_string(), + Some(TEST_API_KEY.to_string()), + ); + Box::new(CMCPriceAPIClient::new( + server_url(&server), + api_key.unwrap(), + reqwest::Client::new(), + )) + }, + ) + .await; + } +} + */ diff --git a/core/lib/external_price_api/src/lib.rs b/core/lib/external_price_api/src/lib.rs index e86279dbe85..f923d65baaa 100644 --- a/core/lib/external_price_api/src/lib.rs +++ b/core/lib/external_price_api/src/lib.rs @@ -1,3 +1,4 @@ +pub mod cmc_api; pub mod coingecko_api; pub mod forced_price_client; mod utils; From bc73e39050dd1e5f004b30796fb470b2d6c26a0b Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 26 Sep 2024 22:04:54 +0900 Subject: [PATCH 2/7] chore: small cleanup --- core/lib/external_price_api/src/cmc_api.rs | 354 +-------------------- 1 file changed, 11 insertions(+), 343 deletions(-) diff --git a/core/lib/external_price_api/src/cmc_api.rs b/core/lib/external_price_api/src/cmc_api.rs index 99b09e78c15..2abfe41c550 100644 --- a/core/lib/external_price_api/src/cmc_api.rs +++ b/core/lib/external_price_api/src/cmc_api.rs @@ -12,9 +12,7 @@ use crate::{utils::get_fraction, PriceAPIClient}; const CMC_AUTH_HEADER: &str = "x-cmc_pro_api_key"; const DEFAULT_CMC_API_URL: &str = "https://pro-api.coinmarketcap.com"; -// it's safe to have id hardcoded as they are stable as claimed by CMC -// const CMC_ETH_ID: i32 = 1027; -const SUPPORT_TOKENS_ONLY_ON_CMC_PLATFORM_ID: i32 = 1; // 1 = Ethereum +const ALLOW_TOKENS_ONLY_ON_CMC_PLATFORM_ID: i32 = 1; // 1 = Ethereum #[derive(Debug)] pub struct CMCPriceAPIClient { @@ -28,13 +26,12 @@ impl CMCPriceAPIClient { let client = if let Some(api_key) = &config.api_key { use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; - let mut headers = HeaderMap::new(); - headers.insert( + let default_headers = HeaderMap::from_iter([( HeaderName::from_static(CMC_AUTH_HEADER), HeaderValue::from_str(api_key).expect("Failed to create header value"), - ); + )]); - reqwest::Client::builder().default_headers(headers) + reqwest::Client::builder().default_headers(default_headers) } else { reqwest::Client::builder() } @@ -43,11 +40,12 @@ impl CMCPriceAPIClient { .expect("Failed to build reqwest client"); let base_url = config.base_url.unwrap_or(DEFAULT_CMC_API_URL.to_string()); + let base_url = Url::parse(&base_url).expect("Failed to parse CoinMarketCap API URL"); Self { - base_url: Url::parse(&base_url).expect("Failed to parse CoinMarketCap API URL"), + base_url, client, - cache_token_id_by_address: RwLock::new(HashMap::new()), + cache_token_id_by_address: RwLock::default(), } } @@ -74,7 +72,7 @@ impl CMCPriceAPIClient { let parsed = response.json::().await?; for token_info in parsed.data { if let Some(platform) = token_info.platform { - if platform.id == SUPPORT_TOKENS_ONLY_ON_CMC_PLATFORM_ID + if platform.id == ALLOW_TOKENS_ONLY_ON_CMC_PLATFORM_ID && Address::from_str(&platform.token_address).is_ok_and(|a| a == address) { self.cache_token_id_by_address @@ -135,7 +133,6 @@ struct CryptocurrencyQuoteObject { #[derive(Debug, Deserialize)] struct MarketQuote { price: f64, - // last_updated: chrono::DateTime, // TODO: Recency? } #[derive(Debug, Deserialize)] @@ -169,17 +166,17 @@ impl PriceAPIClient for CMCPriceAPIClient { return Ok(BaseTokenAPIRatio { numerator, denominator, - ratio_timestamp: Utc::now(), // TODO: Should this be now (as written), or should it be the time returned by the API? + ratio_timestamp: Utc::now(), }); } } #[cfg(test)] -mod irl_tests { +mod tests { use super::*; #[tokio::test] - #[ignore = "run manually; specify CoinMarketCap API key in env var CMC_API_KEY"] + #[ignore = "run manually (accesses network); specify CoinMarketCap API key in env var CMC_API_KEY"] async fn test() { let client = CMCPriceAPIClient::new(ExternalPriceApiClientConfig { api_key: Some(std::env::var("CMC_API_KEY").unwrap()), @@ -200,332 +197,3 @@ mod irl_tests { println!("{r}"); } } - -/* -#[cfg(test)] -mod tests { - use std::{collections::HashMap, str::FromStr}; - - use bigdecimal::BigDecimal; - use httpmock::{Mock, MockServer}; - use zksync_types::{base_token_price::BaseTokenAPIPrice, Address}; - - use crate::{ - address_to_string, - cmc_api::{CMCPriceAPIClient, CMC_AUTH_HEADER, CMC_ETH_ID}, - tests::tests::{ - add_mock, base_token_price_not_found_test, eth_price_not_found_test, happy_day_test, - no_base_token_price_404_test, no_eth_price_404_test, server_url, - }, - PriceAPIClient, - }; - - const TEST_API_KEY: &str = "test"; - - fn mock_crypto_map<'a>( - server: &'a MockServer, - address: &'a Address, - mock_id: &'a String, - ) -> Mock<'a> { - let address_str = address_to_string(address); - let body = format!( - r#"{{ - "data": [ - {{ - "id": 9999, - "platform": {{ - "name": "Ethereum2", - "token_address": "{}" - }} - }}, - {{ - "id": {}, - "platform": {{ - "name": "Ethereum", - "token_address": "{}" - }} - }} - ] - }}"#, - address_str, mock_id, address_str - ); - add_mock( - server, - httpmock::Method::GET, - "/v1/cryptocurrency/map".to_string(), - HashMap::new(), - 200, - body, - CMC_AUTH_HEADER.to_string(), - Some(TEST_API_KEY.to_string()), - ) - } - - fn add_mock_by_id<'a>( - server: &'a MockServer, - id: &'a String, - price: &'a String, - currency: &'a String, - ) -> Mock<'a> { - let body = format!( - r#"{{ - "data": {{ - "{}": {{ - "quote": {{ - "{}": {{ - "price": {} - }} - }} - }} - }} - }}"#, - id, currency, price - ); - let mut params = HashMap::new(); - params.insert("id".to_string(), id.clone()); - add_mock( - server, - httpmock::Method::GET, - "/v1/cryptocurrency/quotes/latest".to_string(), - params, - 200, - body, - CMC_AUTH_HEADER.to_string(), - Some(TEST_API_KEY.to_string()), - ) - } - - fn happy_day_setup( - server: &MockServer, - api_key: Option, - address: Address, - base_token_price: f64, - eth_price: f64, - ) -> Box { - let id = "50".to_string(); - let currency = "USD".to_string(); - mock_crypto_map(server, &address, &id); - add_mock_by_id(server, &id, &base_token_price.to_string(), ¤cy); - add_mock_by_id( - server, - &CMC_ETH_ID.to_string(), - ð_price.to_string(), - ¤cy, - ); - Box::new(CMCPriceAPIClient::new( - server_url(&server), - api_key.unwrap(), - reqwest::Client::new(), - )) - } - - #[tokio::test] - async fn test_happy_day() { - happy_day_test(Some(TEST_API_KEY.to_string()), happy_day_setup).await - } - - #[tokio::test] - async fn test_no_token_id() { - let server = MockServer::start(); - let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; - let address = Address::from_str(address_str).unwrap(); - let id = "50".to_string(); - let base_token_price = 198.9; - let eth_price = 3000.0; - let currency = "USD".to_string(); - - // the response will be missing the token that we are seeking for - mock_crypto_map( - &server, - &Address::from_str("0x3Bad7800d9149B53Cba5da927E6449e4A3487a1F").unwrap(), - &"123".to_string(), - ); - add_mock_by_id(&server, &id, &base_token_price.to_string(), ¤cy); - add_mock_by_id( - &server, - &CMC_ETH_ID.to_string(), - ð_price.to_string(), - ¤cy, - ); - - let mut client = CMCPriceAPIClient::new( - server_url(&server), - TEST_API_KEY.to_string(), - reqwest::Client::new(), - ); - let api_price = client.fetch_price(address).await; - - assert!(api_price.is_err()); - let msg = api_price.err().unwrap().to_string(); - assert_eq!( - "Token id not found for address 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984".to_string(), - msg - ) - } - - #[tokio::test] - async fn should_reuse_token_id_from_map() { - let server = MockServer::start(); - let address_str = "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984"; - let address = Address::from_str(address_str).unwrap(); - let base_token_price = 198.9; - let eth_price = 3000.0; - let id = "50".to_string(); - let currency = "USD".to_string(); - - let cm_mock = mock_crypto_map(&server, &address, &id); - add_mock_by_id(&server, &id, &base_token_price.to_string(), ¤cy); - add_mock_by_id( - &server, - &CMC_ETH_ID.to_string(), - ð_price.to_string(), - ¤cy, - ); - let mut client = CMCPriceAPIClient::new( - server_url(&server), - TEST_API_KEY.to_string(), - reqwest::Client::new(), - ); - - client.fetch_price(address).await.unwrap(); - let api_price = client.fetch_price(address).await.unwrap(); - - assert_eq!( - BaseTokenAPIPrice { - base_token_price: BigDecimal::from_str(&base_token_price.to_string()).unwrap(), - eth_price: BigDecimal::from_str(ð_price.to_string()).unwrap(), - ratio_timestamp: api_price.ratio_timestamp, - }, - api_price - ); - // crypto map should be fetched only once - assert_eq!(1, cm_mock.hits()); - } - - #[tokio::test] - async fn test_no_eth_price_404() { - no_eth_price_404_test( - Some(TEST_API_KEY.to_string()), - |server: &MockServer, - api_key: Option, - address: Address, - _base_token_price: f64, - _eth_price: f64| - -> Box { - let id = "50".to_string(); - mock_crypto_map(&server, &address, &id); - add_mock_by_id(&server, &id, &"123".to_string(), &"USD".to_string()); - - Box::new(CMCPriceAPIClient::new( - server_url(&server), - api_key.unwrap(), - reqwest::Client::new(), - )) - }, - ) - .await; - } - - #[tokio::test] - async fn test_eth_price_not_found() { - eth_price_not_found_test( - Some(TEST_API_KEY.to_string()), - |server: &MockServer, - api_key: Option, - address: Address, - _base_token_price: f64, - _eth_price: f64| - -> Box { - let id = "50".to_string(); - mock_crypto_map(&server, &address, &id); - add_mock_by_id(&server, &id, &"123".to_string(), &"USD".to_string()); - let mut params = HashMap::new(); - params.insert("id".to_string(), CMC_ETH_ID.to_string()); - add_mock( - server, - httpmock::Method::GET, - "/v1/cryptocurrency/quotes/latest".to_string(), - params, - 200, - "{}".to_string(), - CMC_AUTH_HEADER.to_string(), - Some(TEST_API_KEY.to_string()), - ); - Box::new(CMCPriceAPIClient::new( - server_url(&server), - api_key.unwrap(), - reqwest::Client::new(), - )) - }, - ) - .await; - } - - #[tokio::test] - async fn test_no_base_token_price_404() { - no_base_token_price_404_test( - Some(TEST_API_KEY.to_string()), - |server: &MockServer, - api_key: Option, - address: Address, - _base_token_price: f64, - _eth_price: f64| - -> Box { - mock_crypto_map(&server, &address, &"55".to_string()); - add_mock_by_id( - &server, - &CMC_ETH_ID.to_string(), - &"3900.12".to_string(), - &"USD".to_string(), - ); - Box::new(CMCPriceAPIClient::new( - server_url(&server), - api_key.unwrap(), - reqwest::Client::new(), - )) - }, - ) - .await; - } - - #[tokio::test] - async fn test_base_token_price_not_found() { - base_token_price_not_found_test( - Some(TEST_API_KEY.to_string()), - |server: &MockServer, - api_key: Option, - address: Address, - _base_token_price: f64, - _eth_price: f64| - -> Box { - let id = "55".to_string(); - mock_crypto_map(&server, &address, &id); - add_mock_by_id( - &server, - &CMC_ETH_ID.to_string(), - &"3900.12".to_string(), - &"USD".to_string(), - ); - let mut params = HashMap::new(); - params.insert("id".to_string(), id); - add_mock( - server, - httpmock::Method::GET, - "/v1/cryptocurrency/quotes/latest".to_string(), - params, - 200, - "{}".to_string(), - CMC_AUTH_HEADER.to_string(), - Some(TEST_API_KEY.to_string()), - ); - Box::new(CMCPriceAPIClient::new( - server_url(&server), - api_key.unwrap(), - reqwest::Client::new(), - )) - }, - ) - .await; - } -} - */ From fb31d1f136acb5e7a1d88584cb66d3bf34ca9a00 Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 26 Sep 2024 23:32:14 +0900 Subject: [PATCH 3/7] chore: rename to proper CamelCase --- core/lib/external_price_api/src/cmc_api.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core/lib/external_price_api/src/cmc_api.rs b/core/lib/external_price_api/src/cmc_api.rs index 2abfe41c550..4ff605f2fa3 100644 --- a/core/lib/external_price_api/src/cmc_api.rs +++ b/core/lib/external_price_api/src/cmc_api.rs @@ -15,13 +15,13 @@ const DEFAULT_CMC_API_URL: &str = "https://pro-api.coinmarketcap.com"; const ALLOW_TOKENS_ONLY_ON_CMC_PLATFORM_ID: i32 = 1; // 1 = Ethereum #[derive(Debug)] -pub struct CMCPriceAPIClient { +pub struct CmcPriceApiClient { base_url: Url, client: reqwest::Client, cache_token_id_by_address: RwLock>, } -impl CMCPriceAPIClient { +impl CmcPriceApiClient { pub fn new(config: ExternalPriceApiClientConfig) -> Self { let client = if let Some(api_key) = &config.api_key { use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; @@ -158,7 +158,7 @@ struct CryptocurrencyPlatform { } #[async_trait] -impl PriceAPIClient for CMCPriceAPIClient { +impl PriceAPIClient for CmcPriceApiClient { async fn fetch_ratio(&self, token_address: Address) -> anyhow::Result { let base_token_in_eth = self.get_token_price_by_address(token_address).await?; let (numerator, denominator) = get_fraction(base_token_in_eth); @@ -178,7 +178,7 @@ mod tests { #[tokio::test] #[ignore = "run manually (accesses network); specify CoinMarketCap API key in env var CMC_API_KEY"] async fn test() { - let client = CMCPriceAPIClient::new(ExternalPriceApiClientConfig { + let client = CmcPriceApiClient::new(ExternalPriceApiClientConfig { api_key: Some(std::env::var("CMC_API_KEY").unwrap()), base_url: None, client_timeout_ms: 5000, From 4f4dfb8715910c78b3c050b5f7736140ba4cabc5 Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 26 Sep 2024 23:32:34 +0900 Subject: [PATCH 4/7] feat: adds cmc to node builder --- core/bin/zksync_server/src/node_builder.rs | 5 +- .../layers/base_token/cmc_client.rs | 55 +++++++++++++++++++ .../implementations/layers/base_token/mod.rs | 1 + 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 core/node/node_framework/src/implementations/layers/base_token/cmc_client.rs diff --git a/core/bin/zksync_server/src/node_builder.rs b/core/bin/zksync_server/src/node_builder.rs index 14db83b9f25..86d6fb9c3b5 100644 --- a/core/bin/zksync_server/src/node_builder.rs +++ b/core/bin/zksync_server/src/node_builder.rs @@ -19,7 +19,7 @@ use zksync_node_framework::{ implementations::layers::{ base_token::{ base_token_ratio_persister::BaseTokenRatioPersisterLayer, - base_token_ratio_provider::BaseTokenRatioProviderLayer, + base_token_ratio_provider::BaseTokenRatioProviderLayer, cmc_client::CmcClientLayer, coingecko_client::CoingeckoClientLayer, forced_price_client::ForcedPriceClientLayer, no_op_external_price_api_client::NoOpExternalPriceApiClientLayer, }, @@ -555,6 +555,9 @@ impl MainNodeBuilder { fn add_external_api_client_layer(mut self) -> anyhow::Result { let config = try_load_config!(self.configs.external_price_api_client_config); match config.source.as_str() { + CmcClientLayer::CLIENT_NAME => { + self.node.add_layer(CmcClientLayer::new(config)); + } CoingeckoClientLayer::CLIENT_NAME => { self.node.add_layer(CoingeckoClientLayer::new(config)); } diff --git a/core/node/node_framework/src/implementations/layers/base_token/cmc_client.rs b/core/node/node_framework/src/implementations/layers/base_token/cmc_client.rs new file mode 100644 index 00000000000..b1a2305f1d3 --- /dev/null +++ b/core/node/node_framework/src/implementations/layers/base_token/cmc_client.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use zksync_config::configs::ExternalPriceApiClientConfig; +use zksync_external_price_api::cmc_api::CmcPriceApiClient; + +use crate::{ + implementations::resources::price_api_client::PriceAPIClientResource, + wiring_layer::{WiringError, WiringLayer}, + IntoContext, +}; + +/// Wiring layer for `CmcPriceApiClient`. +/// +/// Responsible for inserting a resource with a client to get base token prices +/// from CoinMarketCap to be used by the `BaseTokenRatioPersister`. +#[derive(Debug)] +pub struct CmcClientLayer { + config: ExternalPriceApiClientConfig, +} + +impl CmcClientLayer { + /// Identifier of used client type. + /// Can be used to choose the layer for the client based on configuration variables. + pub const CLIENT_NAME: &'static str = "coinmarketcap"; +} + +#[derive(Debug, IntoContext)] +#[context(crate = crate)] +pub struct Output { + pub price_api_client: PriceAPIClientResource, +} + +impl CmcClientLayer { + pub fn new(config: ExternalPriceApiClientConfig) -> Self { + Self { config } + } +} + +#[async_trait::async_trait] +impl WiringLayer for CmcClientLayer { + type Input = (); + type Output = Output; + + fn layer_name(&self) -> &'static str { + "coinmarketcap_api_client" + } + + async fn wire(self, _input: Self::Input) -> Result { + let client = Arc::new(CmcPriceApiClient::new(self.config)); + + Ok(Output { + price_api_client: client.into(), + }) + } +} diff --git a/core/node/node_framework/src/implementations/layers/base_token/mod.rs b/core/node/node_framework/src/implementations/layers/base_token/mod.rs index 5b58527a3d8..1e58f8d8445 100644 --- a/core/node/node_framework/src/implementations/layers/base_token/mod.rs +++ b/core/node/node_framework/src/implementations/layers/base_token/mod.rs @@ -1,5 +1,6 @@ pub mod base_token_ratio_persister; pub mod base_token_ratio_provider; +pub mod cmc_client; pub mod coingecko_client; pub mod forced_price_client; pub mod no_op_external_price_api_client; From 8bb5c7ad655d9b030eff6eff79adee901584bb9c Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 27 Sep 2024 00:36:24 +0900 Subject: [PATCH 5/7] chore: trying enums instead of inline string matching --- core/bin/zksync_server/src/node_builder.rs | 27 +---- .../layers/base_token/cmc_client.rs | 55 ---------- .../layers/base_token/coingecko_client.rs | 55 ---------- .../layers/base_token/forced_price_client.rs | 52 --------- .../implementations/layers/base_token/mod.rs | 103 +++++++++++++++++- .../no_op_external_price_api_client.rs | 45 -------- 6 files changed, 102 insertions(+), 235 deletions(-) delete mode 100644 core/node/node_framework/src/implementations/layers/base_token/cmc_client.rs delete mode 100644 core/node/node_framework/src/implementations/layers/base_token/coingecko_client.rs delete mode 100644 core/node/node_framework/src/implementations/layers/base_token/forced_price_client.rs delete mode 100644 core/node/node_framework/src/implementations/layers/base_token/no_op_external_price_api_client.rs diff --git a/core/bin/zksync_server/src/node_builder.rs b/core/bin/zksync_server/src/node_builder.rs index 86d6fb9c3b5..87a2b407646 100644 --- a/core/bin/zksync_server/src/node_builder.rs +++ b/core/bin/zksync_server/src/node_builder.rs @@ -19,9 +19,7 @@ use zksync_node_framework::{ implementations::layers::{ base_token::{ base_token_ratio_persister::BaseTokenRatioPersisterLayer, - base_token_ratio_provider::BaseTokenRatioProviderLayer, cmc_client::CmcClientLayer, - coingecko_client::CoingeckoClientLayer, forced_price_client::ForcedPriceClientLayer, - no_op_external_price_api_client::NoOpExternalPriceApiClientLayer, + base_token_ratio_provider::BaseTokenRatioProviderLayer, ExternalPriceApiLayer, }, circuit_breaker_checker::CircuitBreakerCheckerLayer, commitment_generator::CommitmentGeneratorLayer, @@ -554,27 +552,8 @@ impl MainNodeBuilder { fn add_external_api_client_layer(mut self) -> anyhow::Result { let config = try_load_config!(self.configs.external_price_api_client_config); - match config.source.as_str() { - CmcClientLayer::CLIENT_NAME => { - self.node.add_layer(CmcClientLayer::new(config)); - } - CoingeckoClientLayer::CLIENT_NAME => { - self.node.add_layer(CoingeckoClientLayer::new(config)); - } - NoOpExternalPriceApiClientLayer::CLIENT_NAME => { - self.node.add_layer(NoOpExternalPriceApiClientLayer); - } - ForcedPriceClientLayer::CLIENT_NAME => { - self.node.add_layer(ForcedPriceClientLayer::new(config)); - } - _ => { - anyhow::bail!( - "Unknown external price API client source: {}", - config.source - ); - } - } - + self.node + .add_layer(ExternalPriceApiLayer::try_from(config)?); Ok(self) } diff --git a/core/node/node_framework/src/implementations/layers/base_token/cmc_client.rs b/core/node/node_framework/src/implementations/layers/base_token/cmc_client.rs deleted file mode 100644 index b1a2305f1d3..00000000000 --- a/core/node/node_framework/src/implementations/layers/base_token/cmc_client.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::sync::Arc; - -use zksync_config::configs::ExternalPriceApiClientConfig; -use zksync_external_price_api::cmc_api::CmcPriceApiClient; - -use crate::{ - implementations::resources::price_api_client::PriceAPIClientResource, - wiring_layer::{WiringError, WiringLayer}, - IntoContext, -}; - -/// Wiring layer for `CmcPriceApiClient`. -/// -/// Responsible for inserting a resource with a client to get base token prices -/// from CoinMarketCap to be used by the `BaseTokenRatioPersister`. -#[derive(Debug)] -pub struct CmcClientLayer { - config: ExternalPriceApiClientConfig, -} - -impl CmcClientLayer { - /// Identifier of used client type. - /// Can be used to choose the layer for the client based on configuration variables. - pub const CLIENT_NAME: &'static str = "coinmarketcap"; -} - -#[derive(Debug, IntoContext)] -#[context(crate = crate)] -pub struct Output { - pub price_api_client: PriceAPIClientResource, -} - -impl CmcClientLayer { - pub fn new(config: ExternalPriceApiClientConfig) -> Self { - Self { config } - } -} - -#[async_trait::async_trait] -impl WiringLayer for CmcClientLayer { - type Input = (); - type Output = Output; - - fn layer_name(&self) -> &'static str { - "coinmarketcap_api_client" - } - - async fn wire(self, _input: Self::Input) -> Result { - let client = Arc::new(CmcPriceApiClient::new(self.config)); - - Ok(Output { - price_api_client: client.into(), - }) - } -} diff --git a/core/node/node_framework/src/implementations/layers/base_token/coingecko_client.rs b/core/node/node_framework/src/implementations/layers/base_token/coingecko_client.rs deleted file mode 100644 index 14ab568c2f3..00000000000 --- a/core/node/node_framework/src/implementations/layers/base_token/coingecko_client.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::sync::Arc; - -use zksync_config::configs::ExternalPriceApiClientConfig; -use zksync_external_price_api::coingecko_api::CoinGeckoPriceAPIClient; - -use crate::{ - implementations::resources::price_api_client::PriceAPIClientResource, - wiring_layer::{WiringError, WiringLayer}, - IntoContext, -}; - -/// Wiring layer for `CoingeckoApiClient` -/// -/// Responsible for inserting a resource with a client to get base token prices from CoinGecko to be -/// used by the `BaseTokenRatioPersister`. -#[derive(Debug)] -pub struct CoingeckoClientLayer { - config: ExternalPriceApiClientConfig, -} - -impl CoingeckoClientLayer { - /// Identifier of used client type. - /// Can be used to choose the layer for the client based on configuration variables. - pub const CLIENT_NAME: &'static str = "coingecko"; -} - -#[derive(Debug, IntoContext)] -#[context(crate = crate)] -pub struct Output { - pub price_api_client: PriceAPIClientResource, -} - -impl CoingeckoClientLayer { - pub fn new(config: ExternalPriceApiClientConfig) -> Self { - Self { config } - } -} - -#[async_trait::async_trait] -impl WiringLayer for CoingeckoClientLayer { - type Input = (); - type Output = Output; - - fn layer_name(&self) -> &'static str { - "coingecko_api_client" - } - - async fn wire(self, _input: Self::Input) -> Result { - let cg_client = Arc::new(CoinGeckoPriceAPIClient::new(self.config)); - - Ok(Output { - price_api_client: cg_client.into(), - }) - } -} diff --git a/core/node/node_framework/src/implementations/layers/base_token/forced_price_client.rs b/core/node/node_framework/src/implementations/layers/base_token/forced_price_client.rs deleted file mode 100644 index 67785dc26ed..00000000000 --- a/core/node/node_framework/src/implementations/layers/base_token/forced_price_client.rs +++ /dev/null @@ -1,52 +0,0 @@ -use std::sync::Arc; - -use zksync_config::configs::ExternalPriceApiClientConfig; -use zksync_external_price_api::forced_price_client::ForcedPriceClient; - -use crate::{ - implementations::resources::price_api_client::PriceAPIClientResource, - wiring_layer::{WiringError, WiringLayer}, - IntoContext, -}; - -/// Wiring layer for `ForcedPriceClient` -/// -/// Inserts a resource with a forced configured price to be used by the `BaseTokenRatioPersister`. -#[derive(Debug)] -pub struct ForcedPriceClientLayer { - config: ExternalPriceApiClientConfig, -} - -impl ForcedPriceClientLayer { - pub fn new(config: ExternalPriceApiClientConfig) -> Self { - Self { config } - } - - /// Identifier of used client type. - /// Can be used to choose the layer for the client based on configuration variables. - pub const CLIENT_NAME: &'static str = "forced"; -} - -#[derive(Debug, IntoContext)] -#[context(crate = crate)] -pub struct Output { - pub price_api_client: PriceAPIClientResource, -} - -#[async_trait::async_trait] -impl WiringLayer for ForcedPriceClientLayer { - type Input = (); - type Output = Output; - - fn layer_name(&self) -> &'static str { - "forced_price_client" - } - - async fn wire(self, _input: Self::Input) -> Result { - let forced_client = Arc::new(ForcedPriceClient::new(self.config)); - - Ok(Output { - price_api_client: forced_client.into(), - }) - } -} diff --git a/core/node/node_framework/src/implementations/layers/base_token/mod.rs b/core/node/node_framework/src/implementations/layers/base_token/mod.rs index 1e58f8d8445..f2212c03fde 100644 --- a/core/node/node_framework/src/implementations/layers/base_token/mod.rs +++ b/core/node/node_framework/src/implementations/layers/base_token/mod.rs @@ -1,6 +1,101 @@ +use std::{str::FromStr, sync::Arc}; + +use zksync_config::configs::ExternalPriceApiClientConfig; +use zksync_external_price_api::{ + cmc_api::CmcPriceApiClient, coingecko_api::CoinGeckoPriceAPIClient, + forced_price_client::ForcedPriceClient, NoOpPriceAPIClient, +}; + +use crate::{ + implementations::resources::price_api_client::PriceAPIClientResource, IntoContext, WiringError, + WiringLayer, +}; + pub mod base_token_ratio_persister; pub mod base_token_ratio_provider; -pub mod cmc_client; -pub mod coingecko_client; -pub mod forced_price_client; -pub mod no_op_external_price_api_client; + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord, Default)] +enum ExternalPriceApiKind { + #[default] + NoOp, + Forced, + CoinGecko, + CoinMarketCap, +} + +impl ExternalPriceApiKind { + fn layer_name(&self) -> &'static str { + match self { + Self::NoOp => "no_op_external_price_api_client", + Self::Forced => "forced_price_client", + Self::CoinGecko => "coingecko_api_client", + Self::CoinMarketCap => "coinmarketcap_api_client", + } + } + + fn instantiate(&self, config: ExternalPriceApiClientConfig) -> PriceAPIClientResource { + PriceAPIClientResource(match self { + Self::NoOp => Arc::new(NoOpPriceAPIClient {}), + Self::Forced => Arc::new(ForcedPriceClient::new(config)), + Self::CoinGecko => Arc::new(CoinGeckoPriceAPIClient::new(config)), + Self::CoinMarketCap => Arc::new(CmcPriceApiClient::new(config)), + }) + } +} + +#[derive(Debug, thiserror::Error)] +#[error("Unknown external price API client source: {0}")] +pub struct UnknownExternalPriceApiClientSourceError(String); + +impl FromStr for ExternalPriceApiKind { + type Err = UnknownExternalPriceApiClientSourceError; + + fn from_str(s: &str) -> Result { + Ok(match &s.to_lowercase()[..] { + "no-op" => Self::NoOp, + "forced" => Self::Forced, + "coingecko" => Self::CoinGecko, + "coinmarketcap" => Self::CoinMarketCap, + _ => return Err(UnknownExternalPriceApiClientSourceError(s.to_owned())), + }) + } +} + +#[derive(Debug)] +pub struct ExternalPriceApiLayer { + kind: ExternalPriceApiKind, + config: ExternalPriceApiClientConfig, +} + +impl TryFrom for ExternalPriceApiLayer { + type Error = UnknownExternalPriceApiClientSourceError; + + fn try_from(config: ExternalPriceApiClientConfig) -> Result { + Ok(Self { + kind: config.source.parse()?, + config, + }) + } +} + +#[derive(Debug, IntoContext)] +#[context(crate = crate)] +pub struct Output { + pub price_api_client: PriceAPIClientResource, +} + +#[async_trait::async_trait] +impl WiringLayer for ExternalPriceApiLayer { + type Input = (); + type Output = Output; + + fn layer_name(&self) -> &'static str { + self.kind.layer_name() + } + + async fn wire(self, _input: Self::Input) -> Result { + Ok(Output { + price_api_client: self.kind.instantiate(self.config).into(), + }) + } +} diff --git a/core/node/node_framework/src/implementations/layers/base_token/no_op_external_price_api_client.rs b/core/node/node_framework/src/implementations/layers/base_token/no_op_external_price_api_client.rs deleted file mode 100644 index 2bf5eda798f..00000000000 --- a/core/node/node_framework/src/implementations/layers/base_token/no_op_external_price_api_client.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::sync::Arc; - -use zksync_external_price_api::NoOpPriceAPIClient; - -use crate::{ - implementations::resources::price_api_client::PriceAPIClientResource, - wiring_layer::{WiringError, WiringLayer}, - IntoContext, -}; - -/// Wiring layer for `NoOpExternalPriceApiClient` -/// -/// Inserts a resource with a no-op client to get base token prices to be used by the `BaseTokenRatioPersister`. -#[derive(Debug)] -pub struct NoOpExternalPriceApiClientLayer; - -impl NoOpExternalPriceApiClientLayer { - /// Identifier of used client type. - /// Can be used to choose the layer for the client based on configuration variables. - pub const CLIENT_NAME: &'static str = "no-op"; -} - -#[derive(Debug, IntoContext)] -#[context(crate = crate)] -pub struct Output { - pub price_api_client: PriceAPIClientResource, -} - -#[async_trait::async_trait] -impl WiringLayer for NoOpExternalPriceApiClientLayer { - type Input = (); - type Output = Output; - - fn layer_name(&self) -> &'static str { - "no_op_external_price_api_client" - } - - async fn wire(self, _input: Self::Input) -> Result { - let no_op_client = Arc::new(NoOpPriceAPIClient {}); - - Ok(Output { - price_api_client: no_op_client.into(), - }) - } -} From a6b02ded61dee9cfbc57a059071d677c98e1cafb Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 27 Sep 2024 15:43:29 +0900 Subject: [PATCH 6/7] chore: remove dynamic layer name --- .../implementations/layers/base_token/mod.rs | 37 +++++++------------ 1 file changed, 14 insertions(+), 23 deletions(-) diff --git a/core/node/node_framework/src/implementations/layers/base_token/mod.rs b/core/node/node_framework/src/implementations/layers/base_token/mod.rs index f2212c03fde..1146d9e9f00 100644 --- a/core/node/node_framework/src/implementations/layers/base_token/mod.rs +++ b/core/node/node_framework/src/implementations/layers/base_token/mod.rs @@ -23,28 +23,8 @@ enum ExternalPriceApiKind { CoinMarketCap, } -impl ExternalPriceApiKind { - fn layer_name(&self) -> &'static str { - match self { - Self::NoOp => "no_op_external_price_api_client", - Self::Forced => "forced_price_client", - Self::CoinGecko => "coingecko_api_client", - Self::CoinMarketCap => "coinmarketcap_api_client", - } - } - - fn instantiate(&self, config: ExternalPriceApiClientConfig) -> PriceAPIClientResource { - PriceAPIClientResource(match self { - Self::NoOp => Arc::new(NoOpPriceAPIClient {}), - Self::Forced => Arc::new(ForcedPriceClient::new(config)), - Self::CoinGecko => Arc::new(CoinGeckoPriceAPIClient::new(config)), - Self::CoinMarketCap => Arc::new(CmcPriceApiClient::new(config)), - }) - } -} - #[derive(Debug, thiserror::Error)] -#[error("Unknown external price API client source: {0}")] +#[error("Unknown external price API client source: \"{0}\"")] pub struct UnknownExternalPriceApiClientSourceError(String); impl FromStr for ExternalPriceApiKind { @@ -52,7 +32,7 @@ impl FromStr for ExternalPriceApiKind { fn from_str(s: &str) -> Result { Ok(match &s.to_lowercase()[..] { - "no-op" => Self::NoOp, + "no-op" | "noop" => Self::NoOp, "forced" => Self::Forced, "coingecko" => Self::CoinGecko, "coinmarketcap" => Self::CoinMarketCap, @@ -61,6 +41,17 @@ impl FromStr for ExternalPriceApiKind { } } +impl ExternalPriceApiKind { + fn instantiate(&self, config: ExternalPriceApiClientConfig) -> PriceAPIClientResource { + PriceAPIClientResource(match self { + Self::NoOp => Arc::new(NoOpPriceAPIClient {}), + Self::Forced => Arc::new(ForcedPriceClient::new(config)), + Self::CoinGecko => Arc::new(CoinGeckoPriceAPIClient::new(config)), + Self::CoinMarketCap => Arc::new(CmcPriceApiClient::new(config)), + }) + } +} + #[derive(Debug)] pub struct ExternalPriceApiLayer { kind: ExternalPriceApiKind, @@ -90,7 +81,7 @@ impl WiringLayer for ExternalPriceApiLayer { type Output = Output; fn layer_name(&self) -> &'static str { - self.kind.layer_name() + "external_price_api" } async fn wire(self, _input: Self::Input) -> Result { From 1042be07b1d14fea6e3acadf5c8b8564d18e0b22 Mon Sep 17 00:00:00 2001 From: Jacob Date: Fri, 27 Sep 2024 15:49:52 +0900 Subject: [PATCH 7/7] fix: removes superfluous .into() call --- .../node_framework/src/implementations/layers/base_token/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/node/node_framework/src/implementations/layers/base_token/mod.rs b/core/node/node_framework/src/implementations/layers/base_token/mod.rs index 1146d9e9f00..7a63b573d78 100644 --- a/core/node/node_framework/src/implementations/layers/base_token/mod.rs +++ b/core/node/node_framework/src/implementations/layers/base_token/mod.rs @@ -86,7 +86,7 @@ impl WiringLayer for ExternalPriceApiLayer { async fn wire(self, _input: Self::Input) -> Result { Ok(Output { - price_api_client: self.kind.instantiate(self.config).into(), + price_api_client: self.kind.instantiate(self.config), }) } }