1use std::{collections::HashMap, sync::Arc};
17
18use alloy::{
19 primitives::{Address, Bytes},
20 sol,
21 sol_types::SolCall,
22};
23use strum::Display;
24use thiserror::Error;
25
26use super::base::{BaseContract, ContractCall, Multicall3};
27use crate::rpc::{error::BlockchainRpcClientError, http::BlockchainHttpRpcClient};
28
29sol! {
30 #[sol(rpc)]
31 contract ERC20 {
32 function name() external view returns (string);
33 function symbol() external view returns (string);
34 function decimals() external view returns (uint8);
35 }
36}
37
38#[derive(Debug, Display)]
39pub enum Erc20Field {
40 Name,
41 Symbol,
42 Decimals,
43}
44
45#[derive(Debug, Clone)]
47pub struct TokenInfo {
48 pub name: String,
50 pub symbol: String,
52 pub decimals: u8,
54}
55
56#[derive(Debug, Error)]
58pub enum TokenInfoError {
59 #[error("RPC error: {0}")]
60 RpcError(#[from] BlockchainRpcClientError),
61 #[error("Token {field} is empty for address {address}")]
62 EmptyTokenField { field: Erc20Field, address: Address },
63 #[error("Multicall returned unexpected number of results: expected {expected}, got {actual}")]
64 UnexpectedResultCount { expected: usize, actual: usize },
65 #[error("Call failed for {field} at address {address}: {reason} (raw data: {raw_data})")]
66 CallFailed {
67 field: String,
68 address: Address,
69 reason: String,
70 raw_data: String,
71 },
72 #[error("Failed to decode {field} for address {address}: {reason} (raw data: {raw_data})")]
73 DecodingError {
74 field: String,
75 address: Address,
76 reason: String,
77 raw_data: String,
78 },
79}
80
81#[derive(Debug)]
86pub struct Erc20Contract {
87 base: BaseContract,
89 enforce_token_fields: bool,
91}
92
93impl Erc20Contract {
94 #[must_use]
96 pub fn new(client: Arc<BlockchainHttpRpcClient>, enforce_token_fields: bool) -> Self {
97 Self {
98 base: BaseContract::new(client),
99 enforce_token_fields,
100 }
101 }
102
103 pub async fn fetch_token_info(
111 &self,
112 token_address: &Address,
113 ) -> Result<TokenInfo, TokenInfoError> {
114 let calls = vec![
115 ContractCall {
116 target: *token_address,
117 allow_failure: true,
118 call_data: ERC20::nameCall.abi_encode(),
119 },
120 ContractCall {
121 target: *token_address,
122 allow_failure: true,
123 call_data: ERC20::symbolCall.abi_encode(),
124 },
125 ContractCall {
126 target: *token_address,
127 allow_failure: true,
128 call_data: ERC20::decimalsCall.abi_encode(),
129 },
130 ];
131
132 let results = self.base.execute_multicall(calls).await?;
133
134 if results.len() != 3 {
135 return Err(TokenInfoError::UnexpectedResultCount {
136 expected: 3,
137 actual: results.len(),
138 });
139 }
140
141 let name = parse_erc20_string_result(&results[0], Erc20Field::Name, token_address)?;
142 let symbol = parse_erc20_string_result(&results[1], Erc20Field::Symbol, token_address)?;
143 let decimals = parse_erc20_decimals_result(&results[2], token_address)?;
144
145 if self.enforce_token_fields && name.is_empty() {
146 return Err(TokenInfoError::EmptyTokenField {
147 field: Erc20Field::Name,
148 address: *token_address,
149 });
150 }
151
152 if self.enforce_token_fields && symbol.is_empty() {
153 return Err(TokenInfoError::EmptyTokenField {
154 field: Erc20Field::Symbol,
155 address: *token_address,
156 });
157 }
158
159 Ok(TokenInfo {
160 name,
161 symbol,
162 decimals,
163 })
164 }
165
166 pub async fn batch_fetch_token_info(
173 &self,
174 token_addresses: &[Address],
175 ) -> Result<HashMap<Address, Result<TokenInfo, TokenInfoError>>, BlockchainRpcClientError> {
176 let mut calls = Vec::with_capacity(token_addresses.len() * 3);
178
179 for token_address in token_addresses {
180 calls.extend([
181 ContractCall {
182 target: *token_address,
183 allow_failure: true, call_data: ERC20::nameCall.abi_encode(),
185 },
186 ContractCall {
187 target: *token_address,
188 allow_failure: true,
189 call_data: ERC20::symbolCall.abi_encode(),
190 },
191 ContractCall {
192 target: *token_address,
193 allow_failure: true,
194 call_data: ERC20::decimalsCall.abi_encode(),
195 },
196 ]);
197 }
198
199 let results = self.base.execute_multicall(calls).await?;
200
201 let mut token_infos = HashMap::with_capacity(token_addresses.len());
202 for (i, token_address) in token_addresses.iter().enumerate() {
203 let base_idx = i * 3;
204
205 if base_idx + 2 >= results.len() {
207 tracing::error!("Incomplete results from multicall for token {token_address}");
208 token_infos.insert(
209 *token_address,
210 Err(TokenInfoError::UnexpectedResultCount {
211 expected: 3,
212 actual: results.len().saturating_sub(base_idx),
213 }),
214 );
215 continue;
216 }
217
218 let token_info =
219 parse_batch_token_results(&results[base_idx..base_idx + 3], token_address);
220 token_infos.insert(*token_address, token_info);
221 }
222
223 Ok(token_infos)
224 }
225}
226
227fn decode_revert_reason(data: &Bytes) -> String {
230 if data.is_empty() {
233 "Call failed without revert data".to_string()
234 } else {
235 format!("Call failed with data: {data}")
236 }
237}
238
239fn parse_erc20_string_result(
241 result: &Multicall3::Result,
242 field_name: Erc20Field,
243 token_address: &Address,
244) -> Result<String, TokenInfoError> {
245 if !result.success {
247 let reason = if result.returnData.is_empty() {
248 "Call failed without revert data".to_string()
249 } else {
250 decode_revert_reason(&result.returnData)
252 };
253
254 return Err(TokenInfoError::CallFailed {
255 field: field_name.to_string(),
256 address: *token_address,
257 reason,
258 raw_data: result.returnData.to_string(),
259 });
260 }
261
262 if result.returnData.is_empty() {
263 return Err(TokenInfoError::EmptyTokenField {
264 field: field_name,
265 address: *token_address,
266 });
267 }
268
269 match field_name {
270 Erc20Field::Name => ERC20::nameCall::abi_decode_returns(&result.returnData),
271 Erc20Field::Symbol => ERC20::symbolCall::abi_decode_returns(&result.returnData),
272 _ => panic!("Expected Name or Symbol for for parse_erc20_string_result function argument"),
273 }
274 .map_err(|e| TokenInfoError::DecodingError {
275 field: field_name.to_string(),
276 address: *token_address,
277 reason: e.to_string(),
278 raw_data: result.returnData.to_string(),
279 })
280}
281
282fn parse_erc20_decimals_result(
284 result: &Multicall3::Result,
285 token_address: &Address,
286) -> Result<u8, TokenInfoError> {
287 if !result.success {
289 let reason = if result.returnData.is_empty() {
290 "Call failed without revert data".to_string()
291 } else {
292 decode_revert_reason(&result.returnData)
293 };
294
295 return Err(TokenInfoError::CallFailed {
296 field: "decimals".to_string(),
297 address: *token_address,
298 reason,
299 raw_data: result.returnData.to_string(),
300 });
301 }
302
303 if result.returnData.is_empty() {
304 return Err(TokenInfoError::EmptyTokenField {
305 field: Erc20Field::Decimals,
306 address: *token_address,
307 });
308 }
309
310 ERC20::decimalsCall::abi_decode_returns(&result.returnData).map_err(|e| {
311 TokenInfoError::DecodingError {
312 field: "decimals".to_string(),
313 address: *token_address,
314 reason: e.to_string(),
315 raw_data: result.returnData.to_string(),
316 }
317 })
318}
319
320fn parse_batch_token_results(
326 results: &[Multicall3::Result],
327 token_address: &Address,
328) -> Result<TokenInfo, TokenInfoError> {
329 if results.len() != 3 {
330 return Err(TokenInfoError::UnexpectedResultCount {
331 expected: 3,
332 actual: results.len(),
333 });
334 }
335
336 let name = parse_erc20_string_result(&results[0], Erc20Field::Name, token_address)?;
337 let symbol = parse_erc20_string_result(&results[1], Erc20Field::Symbol, token_address)?;
338 let decimals = parse_erc20_decimals_result(&results[2], token_address)?;
339
340 Ok(TokenInfo {
341 name,
342 symbol,
343 decimals,
344 })
345}
346
347#[cfg(test)]
348mod tests {
349 use alloy::primitives::{Bytes, address};
350 use rstest::{fixture, rstest};
351
352 use super::*;
353
354 #[fixture]
355 fn token_address() -> Address {
356 address!("25b76A90E389bD644a29db919b136Dc63B174Ec7")
357 }
358
359 #[fixture]
360 fn successful_name_result() -> Multicall3::Result {
361 Multicall3::Result {
362 success: true,
363 returnData: Bytes::from(hex::decode("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007546f6b656e204100000000000000000000000000000000000000000000000000").unwrap()),
364 }
365 }
366
367 #[fixture]
368 fn successful_symbol_result() -> Multicall3::Result {
369 Multicall3::Result {
370 success: true,
371 returnData: Bytes::from(hex::decode("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000776546f6b656e4100000000000000000000000000000000000000000000000000").unwrap()),
372 }
373 }
374
375 #[fixture]
376 fn failed_name_result() -> Multicall3::Result {
377 Multicall3::Result {
378 success: false,
379 returnData: Bytes::from(vec![]),
380 }
381 }
382
383 #[fixture]
384 fn failed_token_address() -> Address {
385 address!("00000000049084A92F8964B76845ab6DE54EB229")
386 }
387
388 #[fixture]
389 fn success_but_empty_result() -> Multicall3::Result {
390 Multicall3::Result {
391 success: true,
392 returnData: Bytes::from(vec![]),
393 }
394 }
395
396 #[fixture]
397 fn empty_token_address() -> Address {
398 address!("a5b00cEc63694319495d605AA414203F9714F47E")
399 }
400
401 #[fixture]
402 fn non_abi_encoded_string_result() -> Multicall3::Result {
403 Multicall3::Result {
405 success: true,
406 returnData: Bytes::from(
407 hex::decode("5269636f00000000000000000000000000000000000000000000000000000000")
408 .unwrap(),
409 ),
410 }
411 }
412
413 #[fixture]
414 fn non_abi_encoded_token_address() -> Address {
415 address!("5374EcC160A4bd68446B43B5A6B132F9c001C54C")
416 }
417
418 #[fixture]
419 fn non_standard_selector_result() -> Multicall3::Result {
420 Multicall3::Result {
422 success: true,
423 returnData: Bytes::from(
424 hex::decode("06fdde0300000000000000000000000000000000000000000000000000000000")
425 .unwrap(),
426 ),
427 }
428 }
429
430 #[fixture]
431 fn non_abi_encoded_long_string_result() -> Multicall3::Result {
432 Multicall3::Result {
434 success: true,
435 returnData: Bytes::from(
436 hex::decode("5269636f62616e6b205269736b20536861726500000000000000000000000000")
437 .unwrap(),
438 ),
439 }
440 }
441
442 #[rstest]
443 fn test_parse_erc20_string_result_name_success(
444 successful_name_result: Multicall3::Result,
445 token_address: Address,
446 ) {
447 let result =
448 parse_erc20_string_result(&successful_name_result, Erc20Field::Name, &token_address);
449 assert!(result.is_ok());
450 assert_eq!(result.unwrap(), "Token A");
451 }
452
453 #[rstest]
454 fn test_parse_erc20_string_result_symbol_success(
455 successful_symbol_result: Multicall3::Result,
456 token_address: Address,
457 ) {
458 let result = parse_erc20_string_result(
459 &successful_symbol_result,
460 Erc20Field::Symbol,
461 &token_address,
462 );
463 assert!(result.is_ok());
464 assert_eq!(result.unwrap(), "vTokenA");
465 }
466
467 #[rstest]
468 fn test_parse_erc20_string_result_name_failed_with_specific_address(
469 failed_name_result: Multicall3::Result,
470 failed_token_address: Address,
471 ) {
472 let result =
473 parse_erc20_string_result(&failed_name_result, Erc20Field::Name, &failed_token_address);
474 assert!(result.is_err());
475 match result.unwrap_err() {
476 TokenInfoError::CallFailed {
477 field,
478 address,
479 reason,
480 raw_data: _,
481 } => {
482 assert_eq!(field, "Name");
483 assert_eq!(address, failed_token_address);
484 assert_eq!(reason, "Call failed without revert data");
485 }
486 _ => panic!("Expected DecodingError"),
487 }
488 }
489
490 #[rstest]
491 fn test_parse_erc20_string_result_success_but_empty_name(
492 success_but_empty_result: Multicall3::Result,
493 empty_token_address: Address,
494 ) {
495 let result = parse_erc20_string_result(
496 &success_but_empty_result,
497 Erc20Field::Name,
498 &empty_token_address,
499 );
500 assert!(result.is_err());
501 match result.unwrap_err() {
502 TokenInfoError::EmptyTokenField { field, address } => {
503 assert!(matches!(field, Erc20Field::Name));
504 assert_eq!(address, empty_token_address);
505 }
506 _ => panic!("Expected EmptyTokenField error"),
507 }
508 }
509
510 #[rstest]
511 fn test_parse_erc20_decimals_result_success_but_empty(
512 success_but_empty_result: Multicall3::Result,
513 empty_token_address: Address,
514 ) {
515 let result = parse_erc20_decimals_result(&success_but_empty_result, &empty_token_address);
516 assert!(result.is_err());
517 match result.unwrap_err() {
518 TokenInfoError::EmptyTokenField { field, address } => {
519 assert!(matches!(field, Erc20Field::Decimals));
520 assert_eq!(address, empty_token_address);
521 }
522 _ => panic!("Expected EmptyTokenField error"),
523 }
524 }
525
526 #[rstest]
527 fn test_parse_non_abi_encoded_string(
528 non_abi_encoded_string_result: Multicall3::Result,
529 non_abi_encoded_token_address: Address,
530 ) {
531 let result = parse_erc20_string_result(
532 &non_abi_encoded_string_result,
533 Erc20Field::Name,
534 &non_abi_encoded_token_address,
535 );
536 assert!(result.is_err());
537 match result.unwrap_err() {
538 TokenInfoError::DecodingError {
539 field,
540 address,
541 reason,
542 raw_data,
543 } => {
544 assert_eq!(field, "Name");
545 assert_eq!(address, non_abi_encoded_token_address);
546 assert!(reason.contains("type check failed"));
547 assert_eq!(
548 raw_data,
549 "0x5269636f00000000000000000000000000000000000000000000000000000000"
550 );
551 }
553 _ => panic!("Expected DecodingError"),
554 }
555 }
556
557 #[rstest]
558 fn test_parse_non_standard_selector_return(
559 non_standard_selector_result: Multicall3::Result,
560 token_address: Address,
561 ) {
562 let result = parse_erc20_string_result(
563 &non_standard_selector_result,
564 Erc20Field::Name,
565 &token_address,
566 );
567 assert!(result.is_err());
568 match result.unwrap_err() {
569 TokenInfoError::DecodingError {
570 field,
571 address,
572 reason,
573 raw_data,
574 } => {
575 assert_eq!(field, "Name");
576 assert_eq!(address, token_address);
577 assert!(reason.contains("type check failed"));
578 assert_eq!(
579 raw_data,
580 "0x06fdde0300000000000000000000000000000000000000000000000000000000"
581 );
582 }
583 _ => panic!("Expected DecodingError"),
584 }
585 }
586
587 #[rstest]
588 fn test_parse_non_abi_encoded_long_string(
589 non_abi_encoded_long_string_result: Multicall3::Result,
590 token_address: Address,
591 ) {
592 let result = parse_erc20_string_result(
593 &non_abi_encoded_long_string_result,
594 Erc20Field::Name,
595 &token_address,
596 );
597 assert!(result.is_err());
598 match result.unwrap_err() {
599 TokenInfoError::DecodingError {
600 field,
601 address,
602 reason,
603 raw_data,
604 } => {
605 assert_eq!(field, "Name");
606 assert_eq!(address, token_address);
607 assert!(reason.contains("type check failed"));
608 assert_eq!(
609 raw_data,
610 "0x5269636f62616e6b205269736b20536861726500000000000000000000000000"
611 );
612 }
614 _ => panic!("Expected DecodingError"),
615 }
616 }
617}