-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement CoinGecko for fetching native prices (#2824)
# Description Implement a native prices fetch from CoinGecko. As we see in the issue #2814, we are getting many orders rejected because we do not have native prices for many tokens. And the cache of outdated tokens keeps growing over time. In order to solve this, we add CoinGecko as a native price fetcher too. This PR standalone (without infrastructure PR) doesn't have any impact, as CoinGecko must be configured as a native price estimator source. The PR can already prove if fetching prices from CoinGecko improves the cache and the order errors rate, but the following changes are needed in upcoming PRs: - Instead of fetching prices from all the sources at one, have a primary method for fetching prices (CoinGecko) and fallback methods (solvers + 1inch) - Potentially improve performance by doing bulk requests - Move the code from autopilot Please, see my comments below for more clarification. # Changes - Add configuration for CoinGeck estimator source - Implement the trait for fetching native prices for CoinGecko ## How to test 1. Unit test ## Related Issues ### Partially fixes: #2814 --------- Co-authored-by: ilya <[email protected]>
- Loading branch information
1 parent
681faa0
commit 11772f9
Showing
5 changed files
with
178 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
use { | ||
super::{NativePriceEstimateResult, NativePriceEstimating}, | ||
crate::price_estimation::PriceEstimationError, | ||
anyhow::{anyhow, Result}, | ||
futures::{future::BoxFuture, FutureExt}, | ||
primitive_types::H160, | ||
reqwest::{Client, StatusCode}, | ||
serde::Deserialize, | ||
std::collections::HashMap, | ||
url::Url, | ||
}; | ||
|
||
#[derive(Debug, Deserialize)] | ||
struct Response(HashMap<H160, Price>); | ||
|
||
#[derive(Debug, Deserialize)] | ||
struct Price { | ||
eth: f64, | ||
} | ||
|
||
type Token = H160; | ||
|
||
pub struct CoinGecko { | ||
client: Client, | ||
base_url: Url, | ||
api_key: Option<String>, | ||
chain: String, | ||
} | ||
|
||
impl CoinGecko { | ||
/// Authorization header for CoinGecko | ||
const AUTHORIZATION: &'static str = "x-cg-pro-api-key"; | ||
|
||
pub fn new( | ||
client: Client, | ||
base_url: Url, | ||
api_key: Option<String>, | ||
chain_id: u64, | ||
) -> Result<Self> { | ||
let chain = match chain_id { | ||
1 => "ethereum".to_string(), | ||
100 => "xdai".to_string(), | ||
42161 => "arbitrum-one".to_string(), | ||
n => anyhow::bail!("unsupported network {n}"), | ||
}; | ||
Ok(Self { | ||
client, | ||
base_url, | ||
api_key, | ||
chain, | ||
}) | ||
} | ||
} | ||
|
||
impl NativePriceEstimating for CoinGecko { | ||
fn estimate_native_price(&self, token: Token) -> BoxFuture<'_, NativePriceEstimateResult> { | ||
async move { | ||
let url = format!( | ||
"{}/{}?contract_addresses={token:#x}&vs_currencies=eth", | ||
self.base_url, self.chain | ||
); | ||
let mut builder = self.client.get(&url); | ||
if let Some(ref api_key) = self.api_key { | ||
builder = builder.header(Self::AUTHORIZATION, api_key) | ||
} | ||
observe::coingecko_request(&url); | ||
let response = builder.send().await.map_err(|e| { | ||
PriceEstimationError::EstimatorInternal(anyhow!( | ||
"failed to sent CoinGecko price request: {e:?}" | ||
)) | ||
})?; | ||
if !response.status().is_success() { | ||
let status = response.status(); | ||
return match status { | ||
StatusCode::TOO_MANY_REQUESTS => Err(PriceEstimationError::RateLimited), | ||
status => Err(PriceEstimationError::EstimatorInternal(anyhow!( | ||
"failed to retrieve prices from CoinGecko: error with status code \ | ||
{status}." | ||
))), | ||
}; | ||
} | ||
let response = response.text().await; | ||
observe::coingecko_response(&url, response.as_deref()); | ||
let response = response.map_err(|e| { | ||
PriceEstimationError::EstimatorInternal(anyhow!( | ||
"failed to fetch native CoinGecko prices: {e:?}" | ||
)) | ||
})?; | ||
let prices = serde_json::from_str::<Response>(&response) | ||
.map_err(|e| { | ||
PriceEstimationError::EstimatorInternal(anyhow!( | ||
"failed to parse native CoinGecko prices from {response:?}: {e:?}" | ||
)) | ||
})? | ||
.0; | ||
|
||
let price = prices | ||
.get(&token) | ||
.ok_or(PriceEstimationError::NoLiquidity)?; | ||
Ok(price.eth) | ||
} | ||
.boxed() | ||
} | ||
} | ||
|
||
mod observe { | ||
/// Observe a request to be sent to CoinGecko | ||
pub fn coingecko_request(endpoint: &str) { | ||
tracing::trace!(%endpoint, "sending request to CoinGecko"); | ||
} | ||
|
||
/// Observe that a response was received from CoinGecko | ||
pub fn coingecko_response(endpoint: &str, res: Result<&str, &reqwest::Error>) { | ||
match res { | ||
Ok(res) => { | ||
tracing::trace!(%endpoint, ?res, "received response from CoinGecko") | ||
} | ||
Err(err) => { | ||
tracing::warn!(%endpoint, ?err, "failed to receive response from CoinGecko") | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use {super::*, std::str::FromStr}; | ||
|
||
// It is ok to call this API without an API for local testing purposes as it is | ||
// difficulty to hit the rate limit manually | ||
const BASE_URL: &str = "https://api.coingecko.com/api/v3/simple/token_price"; | ||
|
||
#[tokio::test] | ||
#[ignore] | ||
async fn works() { | ||
let native_token = H160::from_str("0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); | ||
let instance = | ||
CoinGecko::new(Client::default(), Url::parse(BASE_URL).unwrap(), None, 1).unwrap(); | ||
|
||
let estimated_price = instance.estimate_native_price(native_token).await.unwrap(); | ||
// Since the WETH precise price against ETH is not always exact to 1.0 (it can | ||
// vary slightly) | ||
assert!((0.95..=1.05).contains(&estimated_price)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters