1use std::{collections::HashMap, sync::Arc};
17
18use alloy::{
19 primitives::{Address, Bytes, U256},
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 function balanceOf(address account) external view returns (uint256);
36 }
37}
38
39#[derive(Debug, Display)]
40pub enum Erc20Field {
41 Name,
42 Symbol,
43 Decimals,
44}
45
46#[derive(Debug, Clone)]
48pub struct TokenInfo {
49 pub name: String,
51 pub symbol: String,
53 pub decimals: u8,
55}
56
57#[derive(Debug, Error)]
59pub enum TokenInfoError {
60 #[error("RPC error: {0}")]
61 RpcError(#[from] BlockchainRpcClientError),
62 #[error("Token {field} is empty for address {address}")]
63 EmptyTokenField { field: Erc20Field, address: Address },
64 #[error("Multicall returned unexpected number of results: expected {expected}, was {actual}")]
65 UnexpectedResultCount { expected: usize, actual: usize },
66 #[error("Call failed for {field} at address {address}: {reason} (raw data: {raw_data})")]
67 CallFailed {
68 field: String,
69 address: Address,
70 reason: String,
71 raw_data: String,
72 },
73 #[error("Failed to decode {field} for address {address}: {reason} (raw data: {raw_data})")]
74 DecodingError {
75 field: String,
76 address: Address,
77 reason: String,
78 raw_data: String,
79 },
80}
81
82#[derive(Debug)]
87pub struct Erc20Contract {
88 base: BaseContract,
90 enforce_token_fields: bool,
92}
93
94impl Erc20Contract {
95 #[must_use]
97 pub fn new(client: Arc<BlockchainHttpRpcClient>, enforce_token_fields: bool) -> Self {
98 Self {
99 base: BaseContract::new(client),
100 enforce_token_fields,
101 }
102 }
103
104 pub async fn fetch_token_info(
112 &self,
113 token_address: &Address,
114 ) -> Result<TokenInfo, TokenInfoError> {
115 let calls = vec![
116 ContractCall {
117 target: *token_address,
118 allow_failure: true,
119 call_data: ERC20::nameCall.abi_encode(),
120 },
121 ContractCall {
122 target: *token_address,
123 allow_failure: true,
124 call_data: ERC20::symbolCall.abi_encode(),
125 },
126 ContractCall {
127 target: *token_address,
128 allow_failure: true,
129 call_data: ERC20::decimalsCall.abi_encode(),
130 },
131 ];
132
133 let results = self.base.execute_multicall(calls, None).await?;
134
135 if results.len() != 3 {
136 return Err(TokenInfoError::UnexpectedResultCount {
137 expected: 3,
138 actual: results.len(),
139 });
140 }
141
142 let name = parse_erc20_string_result(&results[0], Erc20Field::Name, token_address)?;
143 let symbol = parse_erc20_string_result(&results[1], Erc20Field::Symbol, token_address)?;
144 let decimals = parse_erc20_decimals_result(&results[2], token_address)?;
145
146 if self.enforce_token_fields && name.is_empty() {
147 return Err(TokenInfoError::EmptyTokenField {
148 field: Erc20Field::Name,
149 address: *token_address,
150 });
151 }
152
153 if self.enforce_token_fields && symbol.is_empty() {
154 return Err(TokenInfoError::EmptyTokenField {
155 field: Erc20Field::Symbol,
156 address: *token_address,
157 });
158 }
159
160 Ok(TokenInfo {
161 name,
162 symbol,
163 decimals,
164 })
165 }
166
167 pub async fn batch_fetch_token_info(
178 &self,
179 token_addresses: &[Address],
180 ) -> Result<HashMap<Address, Result<TokenInfo, TokenInfoError>>, BlockchainRpcClientError> {
181 let mut calls = Vec::with_capacity(token_addresses.len() * 3);
183
184 for token_address in token_addresses {
185 calls.extend([
186 ContractCall {
187 target: *token_address,
188 allow_failure: true, call_data: ERC20::nameCall.abi_encode(),
190 },
191 ContractCall {
192 target: *token_address,
193 allow_failure: true,
194 call_data: ERC20::symbolCall.abi_encode(),
195 },
196 ContractCall {
197 target: *token_address,
198 allow_failure: true,
199 call_data: ERC20::decimalsCall.abi_encode(),
200 },
201 ]);
202 }
203
204 let results = match self.base.execute_multicall(calls, None).await {
206 Ok(results) => results,
207 Err(e) => {
208 tracing::warn!(
210 "Batch multicall failed: {}. Falling back to individual fetches for {} tokens",
211 e,
212 token_addresses.len()
213 );
214
215 let mut token_infos = HashMap::with_capacity(token_addresses.len());
217 for token_address in token_addresses {
218 match self.fetch_token_info(token_address).await {
219 Ok(info) => {
220 token_infos.insert(*token_address, Ok(info));
221 }
222 Err(e) => {
223 tracing::debug!(
224 "Token {} failed individual fetch (likely expired/broken): {}",
225 token_address,
226 e
227 );
228 token_infos.insert(*token_address, Err(e));
229 }
230 }
231 }
232
233 return Ok(token_infos);
234 }
235 };
236
237 let mut token_infos = HashMap::with_capacity(token_addresses.len());
238 for (i, token_address) in token_addresses.iter().enumerate() {
239 let base_idx = i * 3;
240
241 if base_idx + 2 >= results.len() {
243 tracing::error!("Incomplete results from multicall for token {token_address}");
244 token_infos.insert(
245 *token_address,
246 Err(TokenInfoError::UnexpectedResultCount {
247 expected: 3,
248 actual: results.len().saturating_sub(base_idx),
249 }),
250 );
251 continue;
252 }
253
254 let token_info =
255 parse_batch_token_results(&results[base_idx..base_idx + 3], token_address);
256 token_infos.insert(*token_address, token_info);
257 }
258
259 Ok(token_infos)
260 }
261
262 pub async fn balance_of(
270 &self,
271 token_address: &Address,
272 account: &Address,
273 ) -> Result<U256, BlockchainRpcClientError> {
274 let call_data = ERC20::balanceOfCall { account: *account }.abi_encode();
275 let result = self
276 .base
277 .execute_call(token_address, &call_data, None)
278 .await?;
279
280 ERC20::balanceOfCall::abi_decode_returns(&result)
281 .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
282 }
283}
284
285fn decode_revert_reason(data: &Bytes) -> String {
288 if data.is_empty() {
291 "Call failed without revert data".to_string()
292 } else {
293 format!("Call failed with data: {data}")
294 }
295}
296
297fn parse_erc20_string_result(
299 result: &Multicall3::Result,
300 field_name: Erc20Field,
301 token_address: &Address,
302) -> Result<String, TokenInfoError> {
303 if !result.success {
305 let reason = if result.returnData.is_empty() {
306 "Call failed without revert data".to_string()
307 } else {
308 decode_revert_reason(&result.returnData)
310 };
311
312 return Err(TokenInfoError::CallFailed {
313 field: field_name.to_string(),
314 address: *token_address,
315 reason,
316 raw_data: result.returnData.to_string(),
317 });
318 }
319
320 if result.returnData.is_empty() {
321 return Err(TokenInfoError::EmptyTokenField {
322 field: field_name,
323 address: *token_address,
324 });
325 }
326
327 match field_name {
328 Erc20Field::Name => ERC20::nameCall::abi_decode_returns(&result.returnData),
329 Erc20Field::Symbol => ERC20::symbolCall::abi_decode_returns(&result.returnData),
330 Erc20Field::Decimals => {
331 return Err(TokenInfoError::DecodingError {
332 field: field_name.to_string(),
333 address: *token_address,
334 reason: "Expected Name or Symbol for parse_erc20_string_result function argument"
335 .to_string(),
336 raw_data: result.returnData.to_string(),
337 });
338 }
339 }
340 .map_err(|e| TokenInfoError::DecodingError {
341 field: field_name.to_string(),
342 address: *token_address,
343 reason: e.to_string(),
344 raw_data: result.returnData.to_string(),
345 })
346}
347
348fn parse_erc20_decimals_result(
350 result: &Multicall3::Result,
351 token_address: &Address,
352) -> Result<u8, TokenInfoError> {
353 if !result.success {
355 let reason = if result.returnData.is_empty() {
356 "Call failed without revert data".to_string()
357 } else {
358 decode_revert_reason(&result.returnData)
359 };
360
361 return Err(TokenInfoError::CallFailed {
362 field: "decimals".to_string(),
363 address: *token_address,
364 reason,
365 raw_data: result.returnData.to_string(),
366 });
367 }
368
369 if result.returnData.is_empty() {
370 return Err(TokenInfoError::EmptyTokenField {
371 field: Erc20Field::Decimals,
372 address: *token_address,
373 });
374 }
375
376 ERC20::decimalsCall::abi_decode_returns(&result.returnData).map_err(|e| {
377 TokenInfoError::DecodingError {
378 field: "decimals".to_string(),
379 address: *token_address,
380 reason: e.to_string(),
381 raw_data: result.returnData.to_string(),
382 }
383 })
384}
385
386fn parse_batch_token_results(
392 results: &[Multicall3::Result],
393 token_address: &Address,
394) -> Result<TokenInfo, TokenInfoError> {
395 if results.len() != 3 {
396 return Err(TokenInfoError::UnexpectedResultCount {
397 expected: 3,
398 actual: results.len(),
399 });
400 }
401
402 let name = parse_erc20_string_result(&results[0], Erc20Field::Name, token_address)?;
403 let symbol = parse_erc20_string_result(&results[1], Erc20Field::Symbol, token_address)?;
404 let decimals = parse_erc20_decimals_result(&results[2], token_address)?;
405
406 Ok(TokenInfo {
407 name,
408 symbol,
409 decimals,
410 })
411}
412
413#[cfg(test)]
414mod tests {
415 use alloy::primitives::{Bytes, address};
416 use rstest::{fixture, rstest};
417
418 use super::*;
419
420 #[fixture]
421 fn token_address() -> Address {
422 address!("25b76A90E389bD644a29db919b136Dc63B174Ec7")
423 }
424
425 #[fixture]
426 fn successful_name_result() -> Multicall3::Result {
427 Multicall3::Result {
428 success: true,
429 returnData: Bytes::from(hex::decode("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007546f6b656e204100000000000000000000000000000000000000000000000000").unwrap()),
430 }
431 }
432
433 #[fixture]
434 fn successful_symbol_result() -> Multicall3::Result {
435 Multicall3::Result {
436 success: true,
437 returnData: Bytes::from(hex::decode("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000776546f6b656e4100000000000000000000000000000000000000000000000000").unwrap()),
438 }
439 }
440
441 #[fixture]
442 fn failed_name_result() -> Multicall3::Result {
443 Multicall3::Result {
444 success: false,
445 returnData: Bytes::from(vec![]),
446 }
447 }
448
449 #[fixture]
450 fn failed_token_address() -> Address {
451 address!("00000000049084A92F8964B76845ab6DE54EB229")
452 }
453
454 #[fixture]
455 fn success_but_empty_result() -> Multicall3::Result {
456 Multicall3::Result {
457 success: true,
458 returnData: Bytes::from(vec![]),
459 }
460 }
461
462 #[fixture]
463 fn empty_token_address() -> Address {
464 address!("a5b00cEc63694319495d605AA414203F9714F47E")
465 }
466
467 #[fixture]
468 fn non_abi_encoded_string_result() -> Multicall3::Result {
469 Multicall3::Result {
471 success: true,
472 returnData: Bytes::from(
473 hex::decode("5269636f00000000000000000000000000000000000000000000000000000000")
474 .unwrap(),
475 ),
476 }
477 }
478
479 #[fixture]
480 fn non_abi_encoded_token_address() -> Address {
481 address!("5374EcC160A4bd68446B43B5A6B132F9c001C54C")
482 }
483
484 #[fixture]
485 fn non_standard_selector_result() -> Multicall3::Result {
486 Multicall3::Result {
488 success: true,
489 returnData: Bytes::from(
490 hex::decode("06fdde0300000000000000000000000000000000000000000000000000000000")
491 .unwrap(),
492 ),
493 }
494 }
495
496 #[fixture]
497 fn non_abi_encoded_long_string_result() -> Multicall3::Result {
498 Multicall3::Result {
500 success: true,
501 returnData: Bytes::from(
502 hex::decode("5269636f62616e6b205269736b20536861726500000000000000000000000000")
503 .unwrap(),
504 ),
505 }
506 }
507
508 #[rstest]
509 fn test_parse_erc20_string_result_name_success(
510 successful_name_result: Multicall3::Result,
511 token_address: Address,
512 ) {
513 let result =
514 parse_erc20_string_result(&successful_name_result, Erc20Field::Name, &token_address);
515 assert!(result.is_ok());
516 assert_eq!(result.unwrap(), "Token A");
517 }
518
519 #[rstest]
520 fn test_parse_erc20_string_result_symbol_success(
521 successful_symbol_result: Multicall3::Result,
522 token_address: Address,
523 ) {
524 let result = parse_erc20_string_result(
525 &successful_symbol_result,
526 Erc20Field::Symbol,
527 &token_address,
528 );
529 assert!(result.is_ok());
530 assert_eq!(result.unwrap(), "vTokenA");
531 }
532
533 #[rstest]
534 fn test_parse_erc20_string_result_name_failed_with_specific_address(
535 failed_name_result: Multicall3::Result,
536 failed_token_address: Address,
537 ) {
538 let result =
539 parse_erc20_string_result(&failed_name_result, Erc20Field::Name, &failed_token_address);
540 assert!(result.is_err());
541 match result.unwrap_err() {
542 TokenInfoError::CallFailed {
543 field,
544 address,
545 reason,
546 raw_data: _,
547 } => {
548 assert_eq!(field, "Name");
549 assert_eq!(address, failed_token_address);
550 assert_eq!(reason, "Call failed without revert data");
551 }
552 _ => panic!("Expected DecodingError"),
553 }
554 }
555
556 #[rstest]
557 fn test_parse_erc20_string_result_success_but_empty_name(
558 success_but_empty_result: Multicall3::Result,
559 empty_token_address: Address,
560 ) {
561 let result = parse_erc20_string_result(
562 &success_but_empty_result,
563 Erc20Field::Name,
564 &empty_token_address,
565 );
566 assert!(result.is_err());
567 match result.unwrap_err() {
568 TokenInfoError::EmptyTokenField { field, address } => {
569 assert!(matches!(field, Erc20Field::Name));
570 assert_eq!(address, empty_token_address);
571 }
572 _ => panic!("Expected EmptyTokenField error"),
573 }
574 }
575
576 #[rstest]
577 fn test_parse_erc20_decimals_result_success_but_empty(
578 success_but_empty_result: Multicall3::Result,
579 empty_token_address: Address,
580 ) {
581 let result = parse_erc20_decimals_result(&success_but_empty_result, &empty_token_address);
582 assert!(result.is_err());
583 match result.unwrap_err() {
584 TokenInfoError::EmptyTokenField { field, address } => {
585 assert!(matches!(field, Erc20Field::Decimals));
586 assert_eq!(address, empty_token_address);
587 }
588 _ => panic!("Expected EmptyTokenField error"),
589 }
590 }
591
592 #[rstest]
593 fn test_parse_non_abi_encoded_string(
594 non_abi_encoded_string_result: Multicall3::Result,
595 non_abi_encoded_token_address: Address,
596 ) {
597 let result = parse_erc20_string_result(
598 &non_abi_encoded_string_result,
599 Erc20Field::Name,
600 &non_abi_encoded_token_address,
601 );
602 assert!(result.is_err());
603 match result.unwrap_err() {
604 TokenInfoError::DecodingError {
605 field,
606 address,
607 reason,
608 raw_data,
609 } => {
610 assert_eq!(field, "Name");
611 assert_eq!(address, non_abi_encoded_token_address);
612 assert!(reason.contains("type check failed"));
613 assert_eq!(
614 raw_data,
615 "0x5269636f00000000000000000000000000000000000000000000000000000000"
616 );
617 }
619 _ => panic!("Expected DecodingError"),
620 }
621 }
622
623 #[rstest]
624 fn test_parse_non_standard_selector_return(
625 non_standard_selector_result: Multicall3::Result,
626 token_address: Address,
627 ) {
628 let result = parse_erc20_string_result(
629 &non_standard_selector_result,
630 Erc20Field::Name,
631 &token_address,
632 );
633 assert!(result.is_err());
634 match result.unwrap_err() {
635 TokenInfoError::DecodingError {
636 field,
637 address,
638 reason,
639 raw_data,
640 } => {
641 assert_eq!(field, "Name");
642 assert_eq!(address, token_address);
643 assert!(reason.contains("type check failed"));
644 assert_eq!(
645 raw_data,
646 "0x06fdde0300000000000000000000000000000000000000000000000000000000"
647 );
648 }
649 _ => panic!("Expected DecodingError"),
650 }
651 }
652
653 #[rstest]
654 fn test_parse_non_abi_encoded_long_string(
655 non_abi_encoded_long_string_result: Multicall3::Result,
656 token_address: Address,
657 ) {
658 let result = parse_erc20_string_result(
659 &non_abi_encoded_long_string_result,
660 Erc20Field::Name,
661 &token_address,
662 );
663 assert!(result.is_err());
664 match result.unwrap_err() {
665 TokenInfoError::DecodingError {
666 field,
667 address,
668 reason,
669 raw_data,
670 } => {
671 assert_eq!(field, "Name");
672 assert_eq!(address, token_address);
673 assert!(reason.contains("type check failed"));
674 assert_eq!(
675 raw_data,
676 "0x5269636f62616e6b205269736b20536861726500000000000000000000000000"
677 );
678 }
680 _ => panic!("Expected DecodingError"),
681 }
682 }
683}