Skip to content

Commit 161cd21

Browse files
authored
feat(balance): implementing tokens metadata caching between Zerion and Dune providers (#906)
* feat(balance): adding tokens metadata caching between Zerion and Dune * fix: decreasing the cache to 1 day * fix: setting the cache async * fix: don't fall to eip155:1 in Zerion when the chainId not found in converter
1 parent 1dbf863 commit 161cd21

File tree

7 files changed

+273
-114
lines changed

7 files changed

+273
-114
lines changed

src/handlers/balance.rs

+52-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use {
44
analytics::{BalanceLookupInfo, MessageSource},
55
error::RpcError,
66
state::AppState,
7+
storage::KeyValueStorage,
78
utils::{crypto, network},
89
},
910
axum::{
@@ -14,13 +15,17 @@ use {
1415
ethers::{abi::Address, types::H160},
1516
hyper::HeaderMap,
1617
serde::{Deserialize, Serialize},
17-
std::{net::SocketAddr, sync::Arc},
18+
std::{net::SocketAddr, sync::Arc, time::Duration},
1819
tap::TapFallible,
1920
tracing::log::{debug, error},
2021
wc::future::FutureExt,
2122
};
2223

24+
// Empty address for the contract address mimicking the Ethereum native token
25+
pub const H160_EMPTY_ADDRESS: H160 = H160::repeat_byte(0xee);
26+
2327
const PROVIDER_MAX_CALLS: usize = 2;
28+
const METADATA_CACHE_TTL: Duration = Duration::from_secs(60 * 60 * 24); // 1 day
2429

2530
#[derive(Debug, Deserialize, Clone)]
2631
#[serde(rename_all = "camelCase")]
@@ -61,6 +66,46 @@ pub struct BalanceQuantity {
6166
pub numeric: String,
6267
}
6368

69+
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
70+
#[serde(rename_all = "camelCase")]
71+
pub struct TokenMetadataCacheItem {
72+
pub name: String,
73+
pub symbol: String,
74+
pub icon_url: String,
75+
}
76+
77+
fn token_metadata_cache_key(caip10_token_address: &str) -> String {
78+
format!("token_metadata/{}", caip10_token_address)
79+
}
80+
81+
pub async fn get_cached_metadata(
82+
cache: &Option<Arc<dyn KeyValueStorage<TokenMetadataCacheItem>>>,
83+
caip10_token_address: &str,
84+
) -> Option<TokenMetadataCacheItem> {
85+
let cache = cache.as_ref()?;
86+
cache
87+
.get(&token_metadata_cache_key(caip10_token_address))
88+
.await
89+
.unwrap_or(None)
90+
}
91+
92+
pub async fn set_cached_metadata(
93+
cache: &Option<Arc<dyn KeyValueStorage<TokenMetadataCacheItem>>>,
94+
caip10_token_address: &str,
95+
item: &TokenMetadataCacheItem,
96+
) {
97+
if let Some(cache) = cache {
98+
cache
99+
.set(
100+
&token_metadata_cache_key(caip10_token_address),
101+
item,
102+
Some(METADATA_CACHE_TTL),
103+
)
104+
.await
105+
.unwrap_or_else(|e| error!("Failed to set metadata cache: {}", e));
106+
}
107+
}
108+
64109
pub async fn handler(
65110
state: State<Arc<AppState>>,
66111
query: Query<BalanceQueryParams>,
@@ -113,7 +158,12 @@ async fn handler_internal(
113158
let mut balance_response = None;
114159
for provider in providers.iter() {
115160
let provider_response = provider
116-
.get_balance(address.clone(), query.clone().0, state.metrics.clone())
161+
.get_balance(
162+
address.clone(),
163+
query.clone().0,
164+
&state.token_metadata_cache,
165+
state.metrics.clone(),
166+
)
117167
.await
118168
.tap_err(|e| {
119169
error!("Failed to call balance with {}", e);
@@ -170,7 +220,6 @@ async fn handler_internal(
170220
if namespace != crypto::CaipNamespaces::Eip155 {
171221
return Err(RpcError::UnsupportedNamespace(namespace));
172222
}
173-
const H160_EMPTY_ADDRESS: H160 = H160::repeat_byte(0xee);
174223
let rpc_project_id = state
175224
.config
176225
.server

src/lib.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use {
22
crate::{
33
env::Config,
4-
handlers::{identity::IdentityResponse, rate_limit_middleware},
4+
handlers::{
5+
balance::TokenMetadataCacheItem, identity::IdentityResponse, rate_limit_middleware,
6+
},
57
metrics::Metrics,
68
project::Registry,
79
providers::ProvidersConfig,
@@ -130,6 +132,12 @@ pub async fn bootstrap(config: Config) -> RpcResult<()> {
130132
.map(|addr| redis::Redis::new(&addr, config.storage.redis_max_connections))
131133
.transpose()?
132134
.map(|r| Arc::new(r) as Arc<dyn KeyValueStorage<IdentityResponse> + 'static>);
135+
let token_metadata_cache = config
136+
.storage
137+
.project_data_redis_addr()
138+
.map(|addr| redis::Redis::new(&addr, config.storage.redis_max_connections))
139+
.transpose()?
140+
.map(|r| Arc::new(r) as Arc<dyn KeyValueStorage<TokenMetadataCacheItem> + 'static>);
133141

134142
let providers = init_providers(&config.providers);
135143

@@ -187,11 +195,12 @@ pub async fn bootstrap(config: Config) -> RpcResult<()> {
187195
providers,
188196
metrics.clone(),
189197
registry,
190-
identity_cache,
191198
analytics,
192199
http_client,
193200
rate_limiting,
194201
irn_client,
202+
identity_cache,
203+
token_metadata_cache,
195204
);
196205

197206
let port = state.config.server.port;

src/providers/dune.rs

+118-65
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ use {
33
crate::{
44
env::DuneConfig,
55
error::{RpcError, RpcResult},
6-
handlers::balance::{BalanceQueryParams, BalanceResponseBody},
6+
handlers::balance::{
7+
get_cached_metadata, set_cached_metadata, BalanceQueryParams, BalanceResponseBody,
8+
TokenMetadataCacheItem, H160_EMPTY_ADDRESS,
9+
},
710
providers::{
811
balance::{BalanceItem, BalanceQuantity},
912
ProviderKind,
1013
},
14+
storage::KeyValueStorage,
1115
utils::crypto,
1216
Metrics,
1317
},
@@ -153,6 +157,7 @@ impl BalanceProvider for DuneProvider {
153157
&self,
154158
address: String,
155159
params: BalanceQueryParams,
160+
metadata_cache: &Option<Arc<dyn KeyValueStorage<TokenMetadataCacheItem>>>,
156161
metrics: Arc<Metrics>,
157162
) -> RpcResult<BalanceResponseBody> {
158163
let namespace = params
@@ -175,76 +180,124 @@ impl BalanceProvider for DuneProvider {
175180
}
176181
};
177182

178-
let balances_vec = balance_response
179-
.balances
180-
.into_iter()
181-
.filter_map(|mut f| {
182-
// Skip the asset if there are no symbol, decimals, since this
183-
// is likely a spam token
184-
let symbol = f.symbol.take()?;
185-
let price_usd = f.price_usd.take()?;
186-
let decimals = f.decimals.take()?;
187-
let caip2_chain_id = match f.chain_id {
188-
Some(cid) => format!("{}:{}", namespace, cid),
189-
None => match namespace {
190-
// Using defaul Mainnet chain ids if not provided since
191-
// Dune doesn't provide balances for testnets
192-
crypto::CaipNamespaces::Eip155 => format!("{}:{}", namespace, "1"),
193-
crypto::CaipNamespaces::Solana => {
194-
format!("{}:{}", namespace, "5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp")
183+
let mut balances_vec = Vec::new();
184+
for f in balance_response.balances {
185+
// Skip if missing required fields as a possible spam token
186+
let (Some(symbol), Some(price_usd), Some(decimals)) =
187+
(f.symbol, f.price_usd, f.decimals)
188+
else {
189+
continue;
190+
};
191+
192+
// Build a CAIP-2 chain ID
193+
let caip2_chain_id = match f.chain_id {
194+
Some(cid) => format!("{}:{}", namespace, cid),
195+
None => match namespace {
196+
// Use default Mainnet chain IDs if not provided
197+
crypto::CaipNamespaces::Eip155 => format!("{}:1", namespace),
198+
crypto::CaipNamespaces::Solana => {
199+
format!("{}:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", namespace)
200+
}
201+
},
202+
};
203+
204+
// Determine name
205+
let name = if f.address == "native" {
206+
f.chain.clone()
207+
} else {
208+
symbol.clone()
209+
};
210+
211+
// Determine icon URL
212+
let icon_url = if f.address == "native" {
213+
NATIVE_TOKEN_ICONS.get(&symbol).unwrap_or(&"").to_string()
214+
} else {
215+
// If there's no token_metadata or no logo, skip
216+
match &f.token_metadata {
217+
Some(m) => m.logo.clone(),
218+
None => continue,
219+
}
220+
};
221+
222+
// Build the CAIP-10 address
223+
let caip10_token_address_strict = if f.address == "native" {
224+
match namespace {
225+
crypto::CaipNamespaces::Eip155 => {
226+
format!("{}:{}", caip2_chain_id, H160_EMPTY_ADDRESS)
227+
}
228+
crypto::CaipNamespaces::Solana => {
229+
format!("{}:{}", caip2_chain_id, crypto::SOLANA_NATIVE_TOKEN_ADDRESS)
230+
}
231+
}
232+
} else {
233+
format!("{}:{}", caip2_chain_id, f.address)
234+
};
235+
236+
// Get token metadata from the cache or update it
237+
let token_metadata =
238+
match get_cached_metadata(metadata_cache, &caip10_token_address_strict).await {
239+
Some(cached) => cached,
240+
None => {
241+
let new_item = TokenMetadataCacheItem {
242+
name: name.clone(),
243+
symbol: symbol.clone(),
244+
icon_url: icon_url.clone(),
245+
};
246+
// Spawn a background task to set the cache without blocking
247+
{
248+
let metadata_cache = metadata_cache.clone();
249+
let address_key = caip10_token_address_strict.clone();
250+
let new_item_to_store = new_item.clone();
251+
tokio::spawn(async move {
252+
set_cached_metadata(
253+
&metadata_cache,
254+
&address_key,
255+
&new_item_to_store,
256+
)
257+
.await;
258+
});
195259
}
196-
},
260+
new_item
261+
}
197262
};
198-
Some(BalanceItem {
199-
name: {
200-
if f.address == "native" {
201-
f.chain
202-
} else {
203-
symbol.clone()
204-
}
205-
},
206-
symbol: symbol.clone(),
207-
chain_id: Some(caip2_chain_id.clone()),
208-
address: {
209-
// Return None if the address is native for the native token
210-
if f.address == "native" {
211-
// UI expecting `None`` for the Eip155 and Solana's native
212-
// token address for Solana
213-
match namespace {
214-
crypto::CaipNamespaces::Eip155 => None,
215-
crypto::CaipNamespaces::Solana => {
216-
Some(crypto::SOLANA_NATIVE_TOKEN_ADDRESS.to_string())
217-
}
263+
264+
// Construct the final BalanceItem
265+
let balance_item = BalanceItem {
266+
name: token_metadata.name,
267+
symbol: token_metadata.symbol,
268+
chain_id: Some(caip2_chain_id.clone()),
269+
address: {
270+
// Return None if the address is native (for EIP-155).
271+
// For Solana’s “native” token, we return the Solana native token address.
272+
if f.address == "native" {
273+
match namespace {
274+
crypto::CaipNamespaces::Eip155 => None,
275+
crypto::CaipNamespaces::Solana => {
276+
Some(crypto::SOLANA_NATIVE_TOKEN_ADDRESS.to_string())
218277
}
219-
} else {
220-
Some(format!("{}:{}", caip2_chain_id, f.address.clone()))
221-
}
222-
},
223-
value: f.value_usd,
224-
price: price_usd,
225-
quantity: BalanceQuantity {
226-
decimals: decimals.to_string(),
227-
numeric: crypto::format_token_amount(
228-
U256::from_dec_str(&f.amount).unwrap_or_default(),
229-
decimals,
230-
),
231-
},
232-
icon_url: {
233-
if f.address == "native" {
234-
NATIVE_TOKEN_ICONS.get(&symbol).unwrap_or(&"").to_string()
235-
} else {
236-
f.token_metadata?.logo
237278
}
238-
},
239-
})
240-
})
241-
.collect::<Vec<_>>();
279+
} else {
280+
Some(format!("{}:{}", caip2_chain_id, f.address))
281+
}
282+
},
283+
value: f.value_usd,
284+
price: price_usd,
285+
quantity: BalanceQuantity {
286+
decimals: decimals.to_string(),
287+
numeric: crypto::format_token_amount(
288+
U256::from_dec_str(&f.amount).unwrap_or_default(),
289+
decimals,
290+
),
291+
},
292+
icon_url: token_metadata.icon_url,
293+
};
242294

243-
let response = BalanceResponseBody {
244-
balances: balances_vec,
245-
};
295+
balances_vec.push(balance_item);
296+
}
246297

247-
Ok(response)
298+
Ok(BalanceResponseBody {
299+
balances: balances_vec,
300+
})
248301
}
249302
}
250303

src/providers/mod.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use {
44
env::{BalanceProviderConfig, ProviderConfig},
55
error::{RpcError, RpcResult},
66
handlers::{
7-
balance::{self, BalanceQueryParams, BalanceResponseBody},
7+
balance::{self, BalanceQueryParams, BalanceResponseBody, TokenMetadataCacheItem},
88
convert::{
99
allowance::{AllowanceQueryParams, AllowanceResponseBody},
1010
approve::{ConvertApproveQueryParams, ConvertApproveResponseBody},
@@ -22,6 +22,7 @@ use {
2222
portfolio::{PortfolioQueryParams, PortfolioResponseBody},
2323
RpcQueryParams, SupportedCurrencies,
2424
},
25+
storage::KeyValueStorage,
2526
utils::crypto::CaipNamespaces,
2627
Metrics,
2728
},
@@ -863,6 +864,7 @@ pub trait BalanceProvider: Send + Sync {
863864
&self,
864865
address: String,
865866
params: BalanceQueryParams,
867+
metadata_cache: &Option<Arc<dyn KeyValueStorage<TokenMetadataCacheItem>>>,
866868
metrics: Arc<Metrics>,
867869
) -> RpcResult<BalanceResponseBody>;
868870
}

0 commit comments

Comments
 (0)