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(
174 &self,
175 token_addresses: &[Address],
176 ) -> Result<HashMap<Address, Result<TokenInfo, TokenInfoError>>, BlockchainRpcClientError> {
177 let mut calls = Vec::with_capacity(token_addresses.len() * 3);
179
180 for token_address in token_addresses {
181 calls.extend([
182 ContractCall {
183 target: *token_address,
184 allow_failure: true, call_data: ERC20::nameCall.abi_encode(),
186 },
187 ContractCall {
188 target: *token_address,
189 allow_failure: true,
190 call_data: ERC20::symbolCall.abi_encode(),
191 },
192 ContractCall {
193 target: *token_address,
194 allow_failure: true,
195 call_data: ERC20::decimalsCall.abi_encode(),
196 },
197 ]);
198 }
199
200 let results = self.base.execute_multicall(calls, None).await?;
201
202 let mut token_infos = HashMap::with_capacity(token_addresses.len());
203 for (i, token_address) in token_addresses.iter().enumerate() {
204 let base_idx = i * 3;
205
206 if base_idx + 2 >= results.len() {
208 tracing::error!("Incomplete results from multicall for token {token_address}");
209 token_infos.insert(
210 *token_address,
211 Err(TokenInfoError::UnexpectedResultCount {
212 expected: 3,
213 actual: results.len().saturating_sub(base_idx),
214 }),
215 );
216 continue;
217 }
218
219 let token_info =
220 parse_batch_token_results(&results[base_idx..base_idx + 3], token_address);
221 token_infos.insert(*token_address, token_info);
222 }
223
224 Ok(token_infos)
225 }
226
227 pub async fn balance_of(
235 &self,
236 token_address: &Address,
237 account: &Address,
238 ) -> Result<U256, BlockchainRpcClientError> {
239 let call_data = ERC20::balanceOfCall { account: *account }.abi_encode();
240 let result = self
241 .base
242 .execute_call(token_address, &call_data, None)
243 .await?;
244
245 ERC20::balanceOfCall::abi_decode_returns(&result)
246 .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
247 }
248}
249
250fn decode_revert_reason(data: &Bytes) -> String {
253 if data.is_empty() {
256 "Call failed without revert data".to_string()
257 } else {
258 format!("Call failed with data: {data}")
259 }
260}
261
262fn parse_erc20_string_result(
264 result: &Multicall3::Result,
265 field_name: Erc20Field,
266 token_address: &Address,
267) -> Result<String, TokenInfoError> {
268 if !result.success {
270 let reason = if result.returnData.is_empty() {
271 "Call failed without revert data".to_string()
272 } else {
273 decode_revert_reason(&result.returnData)
275 };
276
277 return Err(TokenInfoError::CallFailed {
278 field: field_name.to_string(),
279 address: *token_address,
280 reason,
281 raw_data: result.returnData.to_string(),
282 });
283 }
284
285 if result.returnData.is_empty() {
286 return Err(TokenInfoError::EmptyTokenField {
287 field: field_name,
288 address: *token_address,
289 });
290 }
291
292 match field_name {
293 Erc20Field::Name => ERC20::nameCall::abi_decode_returns(&result.returnData),
294 Erc20Field::Symbol => ERC20::symbolCall::abi_decode_returns(&result.returnData),
295 _ => panic!("Expected Name or Symbol for for parse_erc20_string_result function argument"),
296 }
297 .map_err(|e| TokenInfoError::DecodingError {
298 field: field_name.to_string(),
299 address: *token_address,
300 reason: e.to_string(),
301 raw_data: result.returnData.to_string(),
302 })
303}
304
305fn parse_erc20_decimals_result(
307 result: &Multicall3::Result,
308 token_address: &Address,
309) -> Result<u8, TokenInfoError> {
310 if !result.success {
312 let reason = if result.returnData.is_empty() {
313 "Call failed without revert data".to_string()
314 } else {
315 decode_revert_reason(&result.returnData)
316 };
317
318 return Err(TokenInfoError::CallFailed {
319 field: "decimals".to_string(),
320 address: *token_address,
321 reason,
322 raw_data: result.returnData.to_string(),
323 });
324 }
325
326 if result.returnData.is_empty() {
327 return Err(TokenInfoError::EmptyTokenField {
328 field: Erc20Field::Decimals,
329 address: *token_address,
330 });
331 }
332
333 ERC20::decimalsCall::abi_decode_returns(&result.returnData).map_err(|e| {
334 TokenInfoError::DecodingError {
335 field: "decimals".to_string(),
336 address: *token_address,
337 reason: e.to_string(),
338 raw_data: result.returnData.to_string(),
339 }
340 })
341}
342
343fn parse_batch_token_results(
349 results: &[Multicall3::Result],
350 token_address: &Address,
351) -> Result<TokenInfo, TokenInfoError> {
352 if results.len() != 3 {
353 return Err(TokenInfoError::UnexpectedResultCount {
354 expected: 3,
355 actual: results.len(),
356 });
357 }
358
359 let name = parse_erc20_string_result(&results[0], Erc20Field::Name, token_address)?;
360 let symbol = parse_erc20_string_result(&results[1], Erc20Field::Symbol, token_address)?;
361 let decimals = parse_erc20_decimals_result(&results[2], token_address)?;
362
363 Ok(TokenInfo {
364 name,
365 symbol,
366 decimals,
367 })
368}
369
370#[cfg(test)]
371mod tests {
372 use alloy::primitives::{Bytes, address};
373 use rstest::{fixture, rstest};
374
375 use super::*;
376
377 #[fixture]
378 fn token_address() -> Address {
379 address!("25b76A90E389bD644a29db919b136Dc63B174Ec7")
380 }
381
382 #[fixture]
383 fn successful_name_result() -> Multicall3::Result {
384 Multicall3::Result {
385 success: true,
386 returnData: Bytes::from(hex::decode("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007546f6b656e204100000000000000000000000000000000000000000000000000").unwrap()),
387 }
388 }
389
390 #[fixture]
391 fn successful_symbol_result() -> Multicall3::Result {
392 Multicall3::Result {
393 success: true,
394 returnData: Bytes::from(hex::decode("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000776546f6b656e4100000000000000000000000000000000000000000000000000").unwrap()),
395 }
396 }
397
398 #[fixture]
399 fn failed_name_result() -> Multicall3::Result {
400 Multicall3::Result {
401 success: false,
402 returnData: Bytes::from(vec![]),
403 }
404 }
405
406 #[fixture]
407 fn failed_token_address() -> Address {
408 address!("00000000049084A92F8964B76845ab6DE54EB229")
409 }
410
411 #[fixture]
412 fn success_but_empty_result() -> Multicall3::Result {
413 Multicall3::Result {
414 success: true,
415 returnData: Bytes::from(vec![]),
416 }
417 }
418
419 #[fixture]
420 fn empty_token_address() -> Address {
421 address!("a5b00cEc63694319495d605AA414203F9714F47E")
422 }
423
424 #[fixture]
425 fn non_abi_encoded_string_result() -> Multicall3::Result {
426 Multicall3::Result {
428 success: true,
429 returnData: Bytes::from(
430 hex::decode("5269636f00000000000000000000000000000000000000000000000000000000")
431 .unwrap(),
432 ),
433 }
434 }
435
436 #[fixture]
437 fn non_abi_encoded_token_address() -> Address {
438 address!("5374EcC160A4bd68446B43B5A6B132F9c001C54C")
439 }
440
441 #[fixture]
442 fn non_standard_selector_result() -> Multicall3::Result {
443 Multicall3::Result {
445 success: true,
446 returnData: Bytes::from(
447 hex::decode("06fdde0300000000000000000000000000000000000000000000000000000000")
448 .unwrap(),
449 ),
450 }
451 }
452
453 #[fixture]
454 fn non_abi_encoded_long_string_result() -> Multicall3::Result {
455 Multicall3::Result {
457 success: true,
458 returnData: Bytes::from(
459 hex::decode("5269636f62616e6b205269736b20536861726500000000000000000000000000")
460 .unwrap(),
461 ),
462 }
463 }
464
465 #[rstest]
466 fn test_parse_erc20_string_result_name_success(
467 successful_name_result: Multicall3::Result,
468 token_address: Address,
469 ) {
470 let result =
471 parse_erc20_string_result(&successful_name_result, Erc20Field::Name, &token_address);
472 assert!(result.is_ok());
473 assert_eq!(result.unwrap(), "Token A");
474 }
475
476 #[rstest]
477 fn test_parse_erc20_string_result_symbol_success(
478 successful_symbol_result: Multicall3::Result,
479 token_address: Address,
480 ) {
481 let result = parse_erc20_string_result(
482 &successful_symbol_result,
483 Erc20Field::Symbol,
484 &token_address,
485 );
486 assert!(result.is_ok());
487 assert_eq!(result.unwrap(), "vTokenA");
488 }
489
490 #[rstest]
491 fn test_parse_erc20_string_result_name_failed_with_specific_address(
492 failed_name_result: Multicall3::Result,
493 failed_token_address: Address,
494 ) {
495 let result =
496 parse_erc20_string_result(&failed_name_result, Erc20Field::Name, &failed_token_address);
497 assert!(result.is_err());
498 match result.unwrap_err() {
499 TokenInfoError::CallFailed {
500 field,
501 address,
502 reason,
503 raw_data: _,
504 } => {
505 assert_eq!(field, "Name");
506 assert_eq!(address, failed_token_address);
507 assert_eq!(reason, "Call failed without revert data");
508 }
509 _ => panic!("Expected DecodingError"),
510 }
511 }
512
513 #[rstest]
514 fn test_parse_erc20_string_result_success_but_empty_name(
515 success_but_empty_result: Multicall3::Result,
516 empty_token_address: Address,
517 ) {
518 let result = parse_erc20_string_result(
519 &success_but_empty_result,
520 Erc20Field::Name,
521 &empty_token_address,
522 );
523 assert!(result.is_err());
524 match result.unwrap_err() {
525 TokenInfoError::EmptyTokenField { field, address } => {
526 assert!(matches!(field, Erc20Field::Name));
527 assert_eq!(address, empty_token_address);
528 }
529 _ => panic!("Expected EmptyTokenField error"),
530 }
531 }
532
533 #[rstest]
534 fn test_parse_erc20_decimals_result_success_but_empty(
535 success_but_empty_result: Multicall3::Result,
536 empty_token_address: Address,
537 ) {
538 let result = parse_erc20_decimals_result(&success_but_empty_result, &empty_token_address);
539 assert!(result.is_err());
540 match result.unwrap_err() {
541 TokenInfoError::EmptyTokenField { field, address } => {
542 assert!(matches!(field, Erc20Field::Decimals));
543 assert_eq!(address, empty_token_address);
544 }
545 _ => panic!("Expected EmptyTokenField error"),
546 }
547 }
548
549 #[rstest]
550 fn test_parse_non_abi_encoded_string(
551 non_abi_encoded_string_result: Multicall3::Result,
552 non_abi_encoded_token_address: Address,
553 ) {
554 let result = parse_erc20_string_result(
555 &non_abi_encoded_string_result,
556 Erc20Field::Name,
557 &non_abi_encoded_token_address,
558 );
559 assert!(result.is_err());
560 match result.unwrap_err() {
561 TokenInfoError::DecodingError {
562 field,
563 address,
564 reason,
565 raw_data,
566 } => {
567 assert_eq!(field, "Name");
568 assert_eq!(address, non_abi_encoded_token_address);
569 assert!(reason.contains("type check failed"));
570 assert_eq!(
571 raw_data,
572 "0x5269636f00000000000000000000000000000000000000000000000000000000"
573 );
574 }
576 _ => panic!("Expected DecodingError"),
577 }
578 }
579
580 #[rstest]
581 fn test_parse_non_standard_selector_return(
582 non_standard_selector_result: Multicall3::Result,
583 token_address: Address,
584 ) {
585 let result = parse_erc20_string_result(
586 &non_standard_selector_result,
587 Erc20Field::Name,
588 &token_address,
589 );
590 assert!(result.is_err());
591 match result.unwrap_err() {
592 TokenInfoError::DecodingError {
593 field,
594 address,
595 reason,
596 raw_data,
597 } => {
598 assert_eq!(field, "Name");
599 assert_eq!(address, token_address);
600 assert!(reason.contains("type check failed"));
601 assert_eq!(
602 raw_data,
603 "0x06fdde0300000000000000000000000000000000000000000000000000000000"
604 );
605 }
606 _ => panic!("Expected DecodingError"),
607 }
608 }
609
610 #[rstest]
611 fn test_parse_non_abi_encoded_long_string(
612 non_abi_encoded_long_string_result: Multicall3::Result,
613 token_address: Address,
614 ) {
615 let result = parse_erc20_string_result(
616 &non_abi_encoded_long_string_result,
617 Erc20Field::Name,
618 &token_address,
619 );
620 assert!(result.is_err());
621 match result.unwrap_err() {
622 TokenInfoError::DecodingError {
623 field,
624 address,
625 reason,
626 raw_data,
627 } => {
628 assert_eq!(field, "Name");
629 assert_eq!(address, token_address);
630 assert!(reason.contains("type check failed"));
631 assert_eq!(
632 raw_data,
633 "0x5269636f62616e6b205269736b20536861726500000000000000000000000000"
634 );
635 }
637 _ => panic!("Expected DecodingError"),
638 }
639 }
640}