1use std::future::Future;
5use std::pin::Pin;
6
7use crate::provider::DataProvider;
8
9#[derive(Debug, Clone)]
11pub struct TokenMeta {
12 pub symbol: String,
13 pub decimals: u8,
14 pub name: String,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
19pub struct TokenLookupKey(pub String);
20
21impl TokenLookupKey {
22 pub fn new(chain_id: u64, address: &str) -> Self {
24 let addr = address.to_lowercase();
25 Self(format!("eip155:{chain_id}/erc20:{addr}"))
26 }
27}
28
29pub struct WellKnownTokenSource {
31 tokens: std::collections::HashMap<TokenLookupKey, TokenMeta>,
32}
33
34impl WellKnownTokenSource {
35 pub fn new() -> Self {
36 let json_str = include_str!("assets/tokens.json");
37 let raw: std::collections::HashMap<String, WellKnownEntry> =
38 serde_json::from_str(json_str).expect("embedded tokens.json is valid");
39 let mut tokens = std::collections::HashMap::new();
40 for (key, entry) in raw {
41 tokens.insert(
42 TokenLookupKey(key),
43 TokenMeta {
44 symbol: entry.symbol,
45 decimals: entry.decimals,
46 name: entry.name,
47 },
48 );
49 }
50 Self { tokens }
51 }
52}
53
54impl Default for WellKnownTokenSource {
55 fn default() -> Self {
56 Self::new()
57 }
58}
59
60impl DataProvider for WellKnownTokenSource {
61 fn resolve_token(
62 &self,
63 chain_id: u64,
64 address: &str,
65 ) -> Pin<Box<dyn Future<Output = Option<TokenMeta>> + Send + '_>> {
66 let key = TokenLookupKey::new(chain_id, address);
67 let result = self.tokens.get(&key).cloned();
68 Box::pin(async move { result })
69 }
70}
71
72#[derive(serde::Deserialize)]
73struct WellKnownEntry {
74 symbol: String,
75 decimals: u8,
76 name: String,
77}
78
79pub struct CompositeDataProvider {
81 providers: Vec<Box<dyn DataProvider>>,
82}
83
84impl CompositeDataProvider {
85 pub fn new(providers: Vec<Box<dyn DataProvider>>) -> Self {
86 Self { providers }
87 }
88}
89
90impl DataProvider for CompositeDataProvider {
91 fn resolve_token(
92 &self,
93 chain_id: u64,
94 address: &str,
95 ) -> Pin<Box<dyn Future<Output = Option<TokenMeta>> + Send + '_>> {
96 let address = address.to_string();
97 Box::pin(async move {
98 for provider in &self.providers {
99 if let Some(meta) = provider.resolve_token(chain_id, &address).await {
100 return Some(meta);
101 }
102 }
103 None
104 })
105 }
106
107 fn resolve_ens_name(
108 &self,
109 address: &str,
110 chain_id: u64,
111 types: Option<&[String]>,
112 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>> {
113 let address = address.to_string();
114 let types_owned: Option<Vec<String>> = types.map(|t| t.to_vec());
115 Box::pin(async move {
116 for provider in &self.providers {
117 if let Some(name) = provider
118 .resolve_ens_name(&address, chain_id, types_owned.as_deref())
119 .await
120 {
121 return Some(name);
122 }
123 }
124 None
125 })
126 }
127
128 fn resolve_local_name(
129 &self,
130 address: &str,
131 chain_id: u64,
132 types: Option<&[String]>,
133 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>> {
134 let address = address.to_string();
135 let types_owned: Option<Vec<String>> = types.map(|t| t.to_vec());
136 Box::pin(async move {
137 for provider in &self.providers {
138 if let Some(name) = provider
139 .resolve_local_name(&address, chain_id, types_owned.as_deref())
140 .await
141 {
142 return Some(name);
143 }
144 }
145 None
146 })
147 }
148
149 fn resolve_nft_collection_name(
150 &self,
151 collection_address: &str,
152 chain_id: u64,
153 ) -> Pin<Box<dyn Future<Output = Option<String>> + Send + '_>> {
154 let collection_address = collection_address.to_string();
155 Box::pin(async move {
156 for provider in &self.providers {
157 if let Some(name) = provider
158 .resolve_nft_collection_name(&collection_address, chain_id)
159 .await
160 {
161 return Some(name);
162 }
163 }
164 None
165 })
166 }
167}
168
169pub struct StaticTokenSource {
171 tokens: std::collections::HashMap<TokenLookupKey, TokenMeta>,
172}
173
174impl StaticTokenSource {
175 pub fn new() -> Self {
176 Self {
177 tokens: std::collections::HashMap::new(),
178 }
179 }
180
181 pub fn insert(&mut self, chain_id: u64, address: &str, meta: TokenMeta) {
182 self.tokens
183 .insert(TokenLookupKey::new(chain_id, address), meta);
184 }
185}
186
187impl Default for StaticTokenSource {
188 fn default() -> Self {
189 Self::new()
190 }
191}
192
193impl DataProvider for StaticTokenSource {
194 fn resolve_token(
195 &self,
196 chain_id: u64,
197 address: &str,
198 ) -> Pin<Box<dyn Future<Output = Option<TokenMeta>> + Send + '_>> {
199 let key = TokenLookupKey::new(chain_id, address);
200 let result = self.tokens.get(&key).cloned();
201 Box::pin(async move { result })
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[tokio::test]
210 async fn test_well_known_usdc_mainnet() {
211 let source = WellKnownTokenSource::new();
212 let meta = source
213 .resolve_token(1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
214 .await
215 .expect("USDC should be in well-known tokens");
216 assert_eq!(meta.symbol, "USDC");
217 assert_eq!(meta.decimals, 6);
218 }
219
220 #[tokio::test]
221 async fn test_well_known_usdc_base() {
222 let source = WellKnownTokenSource::new();
223 let meta = source
224 .resolve_token(8453, "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")
225 .await
226 .expect("USDC on Base should be found");
227 assert_eq!(meta.symbol, "USDC");
228 assert_eq!(meta.decimals, 6);
229 }
230
231 #[tokio::test]
232 async fn test_well_known_not_found() {
233 let source = WellKnownTokenSource::new();
234 assert!(source
235 .resolve_token(1, "0x0000000000000000000000000000000000000001")
236 .await
237 .is_none());
238 }
239
240 #[tokio::test]
241 async fn test_composite_source_fallthrough() {
242 let mut custom = StaticTokenSource::new();
243 custom.insert(
244 1,
245 "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
246 TokenMeta {
247 symbol: "CUSTOM_USDC".to_string(),
248 decimals: 6,
249 name: "Custom USDC".to_string(),
250 },
251 );
252
253 let composite = CompositeDataProvider::new(vec![
254 Box::new(custom),
255 Box::new(WellKnownTokenSource::new()),
256 ]);
257
258 let meta = composite
260 .resolve_token(1, "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48")
261 .await
262 .unwrap();
263 assert_eq!(meta.symbol, "CUSTOM_USDC");
264
265 let meta2 = composite
267 .resolve_token(1, "0xdac17f958d2ee523a2206206994597c13d831ec7")
268 .await
269 .unwrap();
270 assert_eq!(meta2.symbol, "USDT");
271 }
272}