Skip to content

Commit 576419a

Browse files
committed
Core: cache website fee estimates per chain (BD50)
1 parent 2ad6964 commit 576419a

15 files changed

Lines changed: 295 additions & 38 deletions

File tree

‎core/apps/api/src/routes/public/fee.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@ use primitives::ChainFeeEstimates;
55
use services::chain::FeeEstimatesClient;
66

77
use crate::error::ApiError;
8+
use crate::request::{ChainParam, Path};
89
use crate::response::ApiResponse;
910

1011
pub async fn get_fee_estimates(State(client): State<Arc<FeeEstimatesClient>>) -> Result<ApiResponse<Vec<ChainFeeEstimates>>, ApiError> {
1112
Ok(client.get_fee_estimates().await?.into())
1213
}
14+
15+
pub async fn get_chain_fee_estimates(Path(chain): Path<ChainParam>, State(client): State<Arc<FeeEstimatesClient>>) -> Result<ApiResponse<ChainFeeEstimates>, ApiError> {
16+
Ok(client.get_chain_fee_estimates(chain.0).await?.into())
17+
}

‎core/apps/api/src/routes/public/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,5 +36,6 @@ pub fn router() -> Router<AppState> {
3636
.route("/markets", get(markets::get_markets))
3737
.route("/rewards/leaderboard", get(markets::get_rewards_leaderboard))
3838
.route("/chain/fee-estimates", get(fee::get_fee_estimates))
39+
.route("/chain/fee-estimates/{chain}", get(fee::get_chain_fee_estimates))
3940
.merge(webhooks::router())
4041
}

‎core/apps/api/testdata/contract.json‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,20 @@
491491
}
492492
}
493493
},
494+
"v1_chain_fee_estimates": {
495+
"status": 200,
496+
"content_type": "application/json",
497+
"body": "<volatile>"
498+
},
499+
"v1_chain_fee_estimates_bad": {
500+
"status": 422,
501+
"content_type": "application/json",
502+
"body": {
503+
"error": {
504+
"message": "Invalid chain: mars"
505+
}
506+
}
507+
},
494508
"v1_fee_estimates": {
495509
"status": 200,
496510
"content_type": "application/json",

‎core/apps/api/tests/contract.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,8 @@ fn public_cases() -> Vec<Case> {
358358
Case::new("v1_markets", Method::GET, "/v1/markets"),
359359
Case::new("v1_rewards_leaderboard", Method::GET, "/v1/rewards/leaderboard"),
360360
Case::new("v1_fee_estimates", Method::GET, "/v1/chain/fee-estimates").volatile(),
361+
Case::new("v1_chain_fee_estimates", Method::GET, "/v1/chain/fee-estimates/tron").volatile(),
362+
Case::new("v1_chain_fee_estimates_bad", Method::GET, "/v1/chain/fee-estimates/mars"),
361363
Case::new("v1_nft_asset_preview", Method::GET, "/v1/nft/assets/ethereum_0x0000000000000000000000000000000000000001::1/preview").volatile(),
362364
Case::new("v1_nft_asset_preview_bad", Method::GET, "/v1/nft/assets/not-an-id/preview"),
363365
Case::new("v1_nft_collection_preview", Method::GET, "/v1/nft/collections/ethereum_0x0000000000000000000000000000000000000001/preview").volatile(),

‎core/crates/cacher/src/cache.rs‎

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use std::{
2+
error::Error,
3+
future::Future,
4+
time::{Duration, SystemTime},
5+
};
6+
7+
use futures::future::BoxFuture;
8+
use gem_tracing::warn_with_fields;
9+
use serde::{Deserialize, Serialize, de::DeserializeOwned};
10+
11+
use crate::{CacheKey, CacherClient};
12+
13+
pub type CacheFuture<'a, T> = BoxFuture<'a, Result<T, Box<dyn Error + Send + Sync>>>;
14+
15+
#[derive(Serialize, Deserialize)]
16+
#[serde(rename_all = "camelCase")]
17+
pub(crate) struct Cached<T> {
18+
pub(crate) value: T,
19+
updated_at: SystemTime,
20+
}
21+
22+
impl<T> Cached<T> {
23+
fn is_fresh(&self, duration: Duration) -> bool {
24+
self.updated_at.elapsed().is_ok_and(|elapsed| elapsed < duration)
25+
}
26+
}
27+
28+
impl CacherClient {
29+
pub(crate) async fn get_or_fetch<T, F>(&self, key: CacheKey<'_>, duration: Duration, fetch: F) -> Result<T, Box<dyn Error + Send + Sync>>
30+
where
31+
T: DeserializeOwned + Serialize,
32+
F: Future<Output = Result<T, Box<dyn Error + Send + Sync>>>,
33+
{
34+
if let Some(cached) = self.get::<Cached<T>>(key).await?
35+
&& cached.is_fresh(duration)
36+
{
37+
return Ok(cached.value);
38+
}
39+
let cached = Cached {
40+
value: fetch.await?,
41+
updated_at: SystemTime::now(),
42+
};
43+
if let Err(error) = self.set(key, &cached).await {
44+
warn_with_fields!("cache write failed", error = error.as_ref());
45+
}
46+
Ok(cached.value)
47+
}
48+
49+
pub(crate) async fn get_or_fetch_with_fallback<T, F>(&self, key: CacheKey<'_>, duration: Duration, fetch: F) -> Result<T, Box<dyn Error + Send + Sync>>
50+
where
51+
T: DeserializeOwned + Serialize,
52+
F: Future<Output = Result<T, Box<dyn Error + Send + Sync>>>,
53+
{
54+
match self.get_or_fetch(key, duration, fetch).await {
55+
Ok(value) => Ok(value),
56+
Err(error) => {
57+
if let Some(cached) = self.get::<Cached<T>>(key).await? {
58+
warn_with_fields!("cache refresh failed, serving previous value", error = error.as_ref());
59+
return Ok(cached.value);
60+
}
61+
Err(error)
62+
}
63+
}
64+
}
65+
}
Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
11
use std::error::Error;
2+
use std::time::Duration;
23

34
use async_trait::async_trait;
45
use primitives::{Chain, ChainFeeEstimates};
56
use strum::IntoEnumIterator;
67

7-
use crate::{CacheKey, CacherClient};
8+
use crate::cache::Cached;
9+
use crate::{CacheFuture, CacheKey, CacherClient};
810

911
#[async_trait]
1012
pub trait FeeEstimatesCacher: Send + Sync {
11-
async fn fresh_estimates(&self, chain: Chain) -> Result<Option<ChainFeeEstimates>, Box<dyn Error + Send + Sync>>;
12-
async fn set_estimates(&self, chain: Chain, estimates: &ChainFeeEstimates) -> Result<(), Box<dyn Error + Send + Sync>>;
13+
async fn get_or_fetch_estimates(&self, chain: Chain, duration: Duration, fetch: CacheFuture<'_, ChainFeeEstimates>) -> Result<ChainFeeEstimates, Box<dyn Error + Send + Sync>>;
1314
async fn all_estimates(&self) -> Result<Vec<ChainFeeEstimates>, Box<dyn Error + Send + Sync>>;
1415
}
1516

1617
#[async_trait]
1718
impl FeeEstimatesCacher for CacherClient {
18-
async fn fresh_estimates(&self, chain: Chain) -> Result<Option<ChainFeeEstimates>, Box<dyn Error + Send + Sync>> {
19-
let (cached, fresh) = futures::try_join!(
20-
self.get::<ChainFeeEstimates>(CacheKey::TransactionFeeEstimates(chain.as_ref())),
21-
self.get::<()>(CacheKey::TransactionFeeEstimatesFresh(chain.as_ref())),
22-
)?;
23-
Ok(match (cached, fresh) {
24-
(Some(estimates), Some(())) => Some(estimates),
25-
(Some(_), None) | (None, _) => None,
26-
})
27-
}
28-
29-
async fn set_estimates(&self, chain: Chain, estimates: &ChainFeeEstimates) -> Result<(), Box<dyn Error + Send + Sync>> {
30-
self.set(CacheKey::TransactionFeeEstimates(chain.as_ref()), estimates).await?;
31-
self.set(CacheKey::TransactionFeeEstimatesFresh(chain.as_ref()), &()).await
19+
async fn get_or_fetch_estimates(&self, chain: Chain, duration: Duration, fetch: CacheFuture<'_, ChainFeeEstimates>) -> Result<ChainFeeEstimates, Box<dyn Error + Send + Sync>> {
20+
self.get_or_fetch_with_fallback(CacheKey::TransactionFeeEstimates(chain.as_ref()), duration, fetch).await
3221
}
3322

3423
async fn all_estimates(&self) -> Result<Vec<ChainFeeEstimates>, Box<dyn Error + Send + Sync>> {
3524
let chains = Chain::iter().collect::<Vec<_>>();
3625
let keys = chains.iter().map(|chain| CacheKey::TransactionFeeEstimates(chain.as_ref())).collect::<Vec<_>>();
37-
self.get_many(&keys).await
26+
Ok(self.get_many::<Cached<ChainFeeEstimates>>(&keys).await?.into_iter().map(|cached| cached.value).collect())
3827
}
3928
}

‎core/crates/cacher/src/keys.rs‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use primitives::{SECONDS_PER_DAY, SECONDS_PER_HOUR, SECONDS_PER_MINUTE};
33

44
const SECONDS_PER_YEAR: u64 = 365 * SECONDS_PER_DAY;
55

6+
#[derive(Clone, Copy)]
67
pub(crate) enum CacheKey<'a> {
78
ReferralIpCheck(&'a str),
89

@@ -56,7 +57,6 @@ pub(crate) enum CacheKey<'a> {
5657
PendingTransactions(&'a str),
5758
TransactionCheckSchedule(&'a str),
5859
TransactionFeeEstimates(&'a str),
59-
TransactionFeeEstimatesFresh(&'a str),
6060

6161
ScanSafe(&'a str, &'a str, u64),
6262
}
@@ -101,8 +101,7 @@ impl CacheKey<'_> {
101101
Self::PerpetualObserverCheckpoint(chain, address) => format!("perpetual:last_seen:{}:{}", chain, address),
102102
Self::PendingTransactions(chain) => format!("transactions:pending:{}", chain),
103103
Self::TransactionCheckSchedule(queue) => format!("transactions:check_schedule:{}", queue),
104-
Self::TransactionFeeEstimates(chain) => format!("transactions:fee_estimates:{}", chain),
105-
Self::TransactionFeeEstimatesFresh(chain) => format!("transactions:fee_estimates:fresh:{}", chain),
104+
Self::TransactionFeeEstimates(chain) => format!("transactions:fee_estimates:v2:{}", chain),
106105
Self::ScanSafe(scan_type, target, _) => format!("scan:safe:{}:{}", scan_type, target),
107106
}
108107
}
@@ -145,7 +144,6 @@ impl CacheKey<'_> {
145144
Self::PerpetualObserverCheckpoint(_, _) => 30 * SECONDS_PER_DAY,
146145
Self::PendingTransactions(_) | Self::TransactionCheckSchedule(_) => 30 * SECONDS_PER_DAY,
147146
Self::TransactionFeeEstimates(_) => 5 * SECONDS_PER_YEAR,
148-
Self::TransactionFeeEstimatesFresh(_) => SECONDS_PER_HOUR,
149147
Self::ScanSafe(_, _, ttl) => *ttl,
150148
}
151149
}
@@ -169,6 +167,12 @@ mod tests {
169167
assert_eq!(key.ttl(), 3600);
170168
}
171169

170+
#[test]
171+
fn test_fee_estimates_schema_and_retention() {
172+
assert_eq!(CacheKey::TransactionFeeEstimates("tron").key(), "transactions:fee_estimates:v2:tron");
173+
assert_eq!(CacheKey::TransactionFeeEstimates("tron").ttl(), 5 * 365 * SECONDS_PER_DAY);
174+
}
175+
172176
#[test]
173177
fn test_fiat_quote() {
174178
let key = CacheKey::FiatQuote(1, 2, "quote");

‎core/crates/cacher/src/lib.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ use redis::{AsyncCommands, Client, aio::ConnectionManager};
44
use serde::Serialize;
55
use serde::de::DeserializeOwned;
66

7+
mod cache;
78
mod cachers;
89
mod error;
910
mod keys;
11+
pub use cache::CacheFuture;
1012
pub use cachers::*;
1113
pub use error::*;
1214
pub(crate) use keys::CacheKey;

‎core/crates/config_keys/src/config_param_key.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ impl RateLimit {
118118
pub enum ConfigParamKey {
119119
TransactionsRequestLimit(Chain),
120120
TransactionsPendingErrorMaxAge(Chain),
121+
TransactionsFeeEstimatesCacheDuration(Chain),
121122
SwapperVaultAddresses(SwapProvider),
122123
PriceProviderAssetsLimit(PriceProvider),
123124
PriceProviderAssetsDuration(PriceProvider),
@@ -137,6 +138,7 @@ impl ConfigParamKey {
137138
pub fn all() -> Vec<Self> {
138139
let transactions = Chain::all().into_iter().map(Self::TransactionsRequestLimit);
139140
let pending_transactions = Chain::all().into_iter().map(Self::TransactionsPendingErrorMaxAge);
141+
let fee_estimates = Chain::all().into_iter().map(Self::TransactionsFeeEstimatesCacheDuration);
140142
let swapper = SwapProvider::cross_chain_providers().into_iter().map(Self::SwapperVaultAddresses);
141143
let assets_limit = PriceProvider::all().into_iter().map(Self::PriceProviderAssetsLimit);
142144
let assets = PriceProvider::all().into_iter().map(Self::PriceProviderAssetsDuration);
@@ -152,6 +154,7 @@ impl ConfigParamKey {
152154
let rate_limits = RateLimitKey::iter().flat_map(|key| RateLimitWindow::ALL.into_iter().map(move |window| Self::RateLimit(key, window)));
153155
transactions
154156
.chain(pending_transactions)
157+
.chain(fee_estimates)
155158
.chain(swapper)
156159
.chain(assets_limit)
157160
.chain(assets)
@@ -172,6 +175,7 @@ impl ConfigParamKey {
172175
match self {
173176
Self::TransactionsRequestLimit(chain) => format!("{}.{}", self.as_ref(), chain.as_ref()),
174177
Self::TransactionsPendingErrorMaxAge(chain) => format!("{}.{}", self.as_ref(), chain.as_ref()),
178+
Self::TransactionsFeeEstimatesCacheDuration(chain) => format!("{}.{}", self.as_ref(), chain.as_ref()),
175179
Self::SwapperVaultAddresses(provider) => format!("{}.{}", self.as_ref(), provider.as_ref()),
176180
Self::PriceProviderAssetsLimit(provider) => format!("{}.{}", self.as_ref(), provider.as_ref()),
177181
Self::PriceProviderAssetsDuration(provider) => format!("{}.{}", self.as_ref(), provider.as_ref()),
@@ -192,6 +196,7 @@ impl ConfigParamKey {
192196
match self {
193197
Self::TransactionsRequestLimit(_) => "100".to_string(),
194198
Self::TransactionsPendingErrorMaxAge(_) => "3d".to_string(),
199+
Self::TransactionsFeeEstimatesCacheDuration(_) => "1m".to_string(),
195200
Self::SwapperVaultAddresses(_) => "5m".to_string(),
196201
Self::PriceProviderAssetsLimit(PriceProvider::TonApi) => "1000".to_string(),
197202
Self::PriceProviderAssetsLimit(_) => "5000".to_string(),
@@ -233,6 +238,15 @@ mod tests {
233238
assert_eq!(bitcoin.default_value(), "3d");
234239
}
235240

241+
#[test]
242+
fn test_transactions_fee_estimates_cache_duration() {
243+
let key = ConfigParamKey::TransactionsFeeEstimatesCacheDuration(Chain::Tron);
244+
245+
assert_eq!(key.key(), "transactionsFeeEstimatesCacheDuration.tron");
246+
assert_eq!(key.default_value(), "1m");
247+
assert_eq!(ConfigParamKey::all().iter().filter(|key| matches!(key, ConfigParamKey::TransactionsFeeEstimatesCacheDuration(_))).count(), Chain::all().len());
248+
}
249+
236250
#[test]
237251
fn test_scan_type_enable() {
238252
let key = ConfigParamKey::ScanTypeEnable(ScanType::AddressPoisoning);

‎core/crates/services/src/backend.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ impl Services {
350350
}
351351

352352
pub fn fee_estimates(&self, assets: AssetsClient, prices: PriceClient, cacher: CacherClient, user_agent: &str) -> FeeEstimatesClient {
353-
FeeEstimatesClient::new(self.chain(user_agent), assets, prices, Arc::new(cacher))
353+
FeeEstimatesClient::new(self.chain(user_agent), assets, prices, Arc::new(cacher), self.config())
354354
}
355355

356356
pub fn nodes_status(&self) -> NodesStatusClient {

0 commit comments

Comments
 (0)