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 log::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 log::debug!(
224 "Token {token_address} failed individual fetch (likely expired/broken): {e}"
225 );
226 token_infos.insert(*token_address, Err(e));
227 }
228 }
229 }
230
231 return Ok(token_infos);
232 }
233 };
234
235 let mut token_infos = HashMap::with_capacity(token_addresses.len());
236 for (i, token_address) in token_addresses.iter().enumerate() {
237 let base_idx = i * 3;
238
239 if base_idx + 2 >= results.len() {
241 log::error!("Incomplete results from multicall for token {token_address}");
242 token_infos.insert(
243 *token_address,
244 Err(TokenInfoError::UnexpectedResultCount {
245 expected: 3,
246 actual: results.len().saturating_sub(base_idx),
247 }),
248 );
249 continue;
250 }
251
252 let token_info =
253 parse_batch_token_results(&results[base_idx..base_idx + 3], token_address);
254 token_infos.insert(*token_address, token_info);
255 }
256
257 Ok(token_infos)
258 }
259
260 pub async fn balance_of(
268 &self,
269 token_address: &Address,
270 account: &Address,
271 ) -> Result<U256, BlockchainRpcClientError> {
272 let call_data = ERC20::balanceOfCall { account: *account }.abi_encode();
273 let result = self
274 .base
275 .execute_call(token_address, &call_data, None)
276 .await?;
277
278 ERC20::balanceOfCall::abi_decode_returns(&result)
279 .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
280 }
281}
282
283fn decode_revert_reason(data: &Bytes) -> String {
286 if data.is_empty() {
289 "Call failed without revert data".to_string()
290 } else {
291 format!("Call failed with data: {data}")
292 }
293}
294
295fn parse_erc20_string_result(
297 result: &Multicall3::Result,
298 field_name: Erc20Field,
299 token_address: &Address,
300) -> Result<String, TokenInfoError> {
301 if !result.success {
303 let reason = if result.returnData.is_empty() {
304 "Call failed without revert data".to_string()
305 } else {
306 decode_revert_reason(&result.returnData)
308 };
309
310 return Err(TokenInfoError::CallFailed {
311 field: field_name.to_string(),
312 address: *token_address,
313 reason,
314 raw_data: result.returnData.to_string(),
315 });
316 }
317
318 if result.returnData.is_empty() {
319 return Err(TokenInfoError::EmptyTokenField {
320 field: field_name,
321 address: *token_address,
322 });
323 }
324
325 match field_name {
326 Erc20Field::Name => ERC20::nameCall::abi_decode_returns(&result.returnData),
327 Erc20Field::Symbol => ERC20::symbolCall::abi_decode_returns(&result.returnData),
328 Erc20Field::Decimals => {
329 return Err(TokenInfoError::DecodingError {
330 field: field_name.to_string(),
331 address: *token_address,
332 reason: "Expected Name or Symbol for parse_erc20_string_result function argument"
333 .to_string(),
334 raw_data: result.returnData.to_string(),
335 });
336 }
337 }
338 .map_err(|e| TokenInfoError::DecodingError {
339 field: field_name.to_string(),
340 address: *token_address,
341 reason: e.to_string(),
342 raw_data: result.returnData.to_string(),
343 })
344}
345
346fn parse_erc20_decimals_result(
348 result: &Multicall3::Result,
349 token_address: &Address,
350) -> Result<u8, TokenInfoError> {
351 if !result.success {
353 let reason = if result.returnData.is_empty() {
354 "Call failed without revert data".to_string()
355 } else {
356 decode_revert_reason(&result.returnData)
357 };
358
359 return Err(TokenInfoError::CallFailed {
360 field: "decimals".to_string(),
361 address: *token_address,
362 reason,
363 raw_data: result.returnData.to_string(),
364 });
365 }
366
367 if result.returnData.is_empty() {
368 return Err(TokenInfoError::EmptyTokenField {
369 field: Erc20Field::Decimals,
370 address: *token_address,
371 });
372 }
373
374 ERC20::decimalsCall::abi_decode_returns(&result.returnData).map_err(|e| {
375 TokenInfoError::DecodingError {
376 field: "decimals".to_string(),
377 address: *token_address,
378 reason: e.to_string(),
379 raw_data: result.returnData.to_string(),
380 }
381 })
382}
383
384fn parse_batch_token_results(
390 results: &[Multicall3::Result],
391 token_address: &Address,
392) -> Result<TokenInfo, TokenInfoError> {
393 if results.len() != 3 {
394 return Err(TokenInfoError::UnexpectedResultCount {
395 expected: 3,
396 actual: results.len(),
397 });
398 }
399
400 let name = parse_erc20_string_result(&results[0], Erc20Field::Name, token_address)?;
401 let symbol = parse_erc20_string_result(&results[1], Erc20Field::Symbol, token_address)?;
402 let decimals = parse_erc20_decimals_result(&results[2], token_address)?;
403
404 Ok(TokenInfo {
405 name,
406 symbol,
407 decimals,
408 })
409}
410
411#[cfg(test)]
412mod tests {
413 use alloy::primitives::{Bytes, address};
414 use nautilus_core::hex;
415 use rstest::{fixture, rstest};
416
417 use super::*;
418
419 #[fixture]
420 fn token_address() -> Address {
421 address!("25b76A90E389bD644a29db919b136Dc63B174Ec7")
422 }
423
424 #[fixture]
425 fn successful_name_result() -> Multicall3::Result {
426 Multicall3::Result {
427 success: true,
428 returnData: Bytes::from(hex::decode("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007546f6b656e204100000000000000000000000000000000000000000000000000").unwrap()),
429 }
430 }
431
432 #[fixture]
433 fn successful_symbol_result() -> Multicall3::Result {
434 Multicall3::Result {
435 success: true,
436 returnData: Bytes::from(hex::decode("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000776546f6b656e4100000000000000000000000000000000000000000000000000").unwrap()),
437 }
438 }
439
440 #[fixture]
441 fn failed_name_result() -> Multicall3::Result {
442 Multicall3::Result {
443 success: false,
444 returnData: Bytes::from(vec![]),
445 }
446 }
447
448 #[fixture]
449 fn failed_token_address() -> Address {
450 address!("00000000049084A92F8964B76845ab6DE54EB229")
451 }
452
453 #[fixture]
454 fn success_but_empty_result() -> Multicall3::Result {
455 Multicall3::Result {
456 success: true,
457 returnData: Bytes::from(vec![]),
458 }
459 }
460
461 #[fixture]
462 fn empty_token_address() -> Address {
463 address!("a5b00cEc63694319495d605AA414203F9714F47E")
464 }
465
466 #[fixture]
467 fn non_abi_encoded_string_result() -> Multicall3::Result {
468 Multicall3::Result {
470 success: true,
471 returnData: Bytes::from(
472 hex::decode("5269636f00000000000000000000000000000000000000000000000000000000")
473 .unwrap(),
474 ),
475 }
476 }
477
478 #[fixture]
479 fn non_abi_encoded_token_address() -> Address {
480 address!("5374EcC160A4bd68446B43B5A6B132F9c001C54C")
481 }
482
483 #[fixture]
484 fn non_standard_selector_result() -> Multicall3::Result {
485 Multicall3::Result {
487 success: true,
488 returnData: Bytes::from(
489 hex::decode("06fdde0300000000000000000000000000000000000000000000000000000000")
490 .unwrap(),
491 ),
492 }
493 }
494
495 #[fixture]
496 fn non_abi_encoded_long_string_result() -> Multicall3::Result {
497 Multicall3::Result {
499 success: true,
500 returnData: Bytes::from(
501 hex::decode("5269636f62616e6b205269736b20536861726500000000000000000000000000")
502 .unwrap(),
503 ),
504 }
505 }
506
507 #[rstest]
508 fn test_parse_erc20_string_result_name_success(
509 successful_name_result: Multicall3::Result,
510 token_address: Address,
511 ) {
512 let result =
513 parse_erc20_string_result(&successful_name_result, Erc20Field::Name, &token_address);
514 assert!(result.is_ok());
515 assert_eq!(result.unwrap(), "Token A");
516 }
517
518 #[rstest]
519 fn test_parse_erc20_string_result_symbol_success(
520 successful_symbol_result: Multicall3::Result,
521 token_address: Address,
522 ) {
523 let result = parse_erc20_string_result(
524 &successful_symbol_result,
525 Erc20Field::Symbol,
526 &token_address,
527 );
528 assert!(result.is_ok());
529 assert_eq!(result.unwrap(), "vTokenA");
530 }
531
532 #[rstest]
533 fn test_parse_erc20_string_result_name_failed_with_specific_address(
534 failed_name_result: Multicall3::Result,
535 failed_token_address: Address,
536 ) {
537 let result =
538 parse_erc20_string_result(&failed_name_result, Erc20Field::Name, &failed_token_address);
539 assert!(result.is_err());
540 match result.unwrap_err() {
541 TokenInfoError::CallFailed {
542 field,
543 address,
544 reason,
545 raw_data: _,
546 } => {
547 assert_eq!(field, "Name");
548 assert_eq!(address, failed_token_address);
549 assert_eq!(reason, "Call failed without revert data");
550 }
551 _ => panic!("Expected DecodingError"),
552 }
553 }
554
555 #[rstest]
556 fn test_parse_erc20_string_result_success_but_empty_name(
557 success_but_empty_result: Multicall3::Result,
558 empty_token_address: Address,
559 ) {
560 let result = parse_erc20_string_result(
561 &success_but_empty_result,
562 Erc20Field::Name,
563 &empty_token_address,
564 );
565 assert!(result.is_err());
566 match result.unwrap_err() {
567 TokenInfoError::EmptyTokenField { field, address } => {
568 assert!(matches!(field, Erc20Field::Name));
569 assert_eq!(address, empty_token_address);
570 }
571 _ => panic!("Expected EmptyTokenField error"),
572 }
573 }
574
575 #[rstest]
576 fn test_parse_erc20_decimals_result_success_but_empty(
577 success_but_empty_result: Multicall3::Result,
578 empty_token_address: Address,
579 ) {
580 let result = parse_erc20_decimals_result(&success_but_empty_result, &empty_token_address);
581 assert!(result.is_err());
582 match result.unwrap_err() {
583 TokenInfoError::EmptyTokenField { field, address } => {
584 assert!(matches!(field, Erc20Field::Decimals));
585 assert_eq!(address, empty_token_address);
586 }
587 _ => panic!("Expected EmptyTokenField error"),
588 }
589 }
590
591 #[rstest]
592 fn test_parse_non_abi_encoded_string(
593 non_abi_encoded_string_result: Multicall3::Result,
594 non_abi_encoded_token_address: Address,
595 ) {
596 let result = parse_erc20_string_result(
597 &non_abi_encoded_string_result,
598 Erc20Field::Name,
599 &non_abi_encoded_token_address,
600 );
601 assert!(result.is_err());
602 match result.unwrap_err() {
603 TokenInfoError::DecodingError {
604 field,
605 address,
606 reason,
607 raw_data,
608 } => {
609 assert_eq!(field, "Name");
610 assert_eq!(address, non_abi_encoded_token_address);
611 assert!(reason.contains("type check failed"));
612 assert_eq!(
613 raw_data,
614 "0x5269636f00000000000000000000000000000000000000000000000000000000"
615 );
616 }
618 _ => panic!("Expected DecodingError"),
619 }
620 }
621
622 #[rstest]
623 fn test_parse_non_standard_selector_return(
624 non_standard_selector_result: Multicall3::Result,
625 token_address: Address,
626 ) {
627 let result = parse_erc20_string_result(
628 &non_standard_selector_result,
629 Erc20Field::Name,
630 &token_address,
631 );
632 assert!(result.is_err());
633 match result.unwrap_err() {
634 TokenInfoError::DecodingError {
635 field,
636 address,
637 reason,
638 raw_data,
639 } => {
640 assert_eq!(field, "Name");
641 assert_eq!(address, token_address);
642 assert!(reason.contains("type check failed"));
643 assert_eq!(
644 raw_data,
645 "0x06fdde0300000000000000000000000000000000000000000000000000000000"
646 );
647 }
648 _ => panic!("Expected DecodingError"),
649 }
650 }
651
652 #[rstest]
653 fn test_parse_non_abi_encoded_long_string(
654 non_abi_encoded_long_string_result: Multicall3::Result,
655 token_address: Address,
656 ) {
657 let result = parse_erc20_string_result(
658 &non_abi_encoded_long_string_result,
659 Erc20Field::Name,
660 &token_address,
661 );
662 assert!(result.is_err());
663 match result.unwrap_err() {
664 TokenInfoError::DecodingError {
665 field,
666 address,
667 reason,
668 raw_data,
669 } => {
670 assert_eq!(field, "Name");
671 assert_eq!(address, token_address);
672 assert!(reason.contains("type check failed"));
673 assert_eq!(
674 raw_data,
675 "0x5269636f62616e6b205269736b20536861726500000000000000000000000000"
676 );
677 }
679 _ => panic!("Expected DecodingError"),
680 }
681 }
682}