Skip to main content

nautilus_blockchain/contracts/
erc20.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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        function allowance(address owner, address spender) external view returns (uint256);
37        function approve(address spender, uint256 amount) external returns (bool);
38    }
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
42pub enum Erc20Field {
43    Name,
44    Symbol,
45    Decimals,
46}
47
48/// Represents the essential metadata information for an ERC20 token.
49#[derive(Debug, Clone)]
50pub struct TokenInfo {
51    /// The full name of the token.
52    pub name: String,
53    /// The ticker symbol of the token.
54    pub symbol: String,
55    /// The number of decimal places the token uses for representing fractional amounts.
56    pub decimals: u8,
57}
58
59/// Represents errors that can occur when interacting with a blockchain RPC client.
60#[derive(Debug, Error)]
61pub enum TokenInfoError {
62    #[error("RPC error: {0}")]
63    RpcError(#[from] BlockchainRpcClientError),
64    #[error("Token {field} is empty for address {address}")]
65    EmptyTokenField { field: Erc20Field, address: Address },
66    #[error("Multicall returned unexpected number of results: expected {expected}, was {actual}")]
67    UnexpectedResultCount { expected: usize, actual: usize },
68    #[error("Call failed for {field} at address {address}: {reason} (raw data: {raw_data})")]
69    CallFailed {
70        field: String,
71        address: Address,
72        reason: String,
73        raw_data: String,
74    },
75    #[error("Failed to decode {field} for address {address}: {reason} (raw data: {raw_data})")]
76    DecodingError {
77        field: String,
78        address: Address,
79        reason: String,
80        raw_data: String,
81    },
82}
83
84/// Interface for interacting with ERC20 token contracts on a blockchain.
85///
86/// This struct provides methods to fetch token metadata (name, symbol, decimals).
87/// From ERC20-compliant tokens on any EVM-compatible blockchain.
88#[derive(Debug)]
89pub struct Erc20Contract {
90    /// The base contract providing common RPC execution functionality.
91    base: BaseContract,
92    /// Whether to enforce that token name and symbol fields must be non-empty.
93    enforce_token_fields: bool,
94}
95
96impl Erc20Contract {
97    /// Creates a new ERC20 contract interface with the specified RPC client.
98    #[must_use]
99    pub fn new(client: Arc<BlockchainHttpRpcClient>, enforce_token_fields: bool) -> Self {
100        Self {
101            base: BaseContract::new(client),
102            enforce_token_fields,
103        }
104    }
105
106    /// Creates a new ERC20 contract interface with the specified RPC client and a per-request
107    /// RPC timeout applied to its reads.
108    #[must_use]
109    pub fn new_with_timeout(
110        client: Arc<BlockchainHttpRpcClient>,
111        rpc_timeout_secs: Option<u64>,
112        enforce_token_fields: bool,
113    ) -> Self {
114        Self {
115            base: BaseContract::new_with_multicall_limit_and_timeout(
116                client,
117                super::base::DEFAULT_MULTICALL_CALLS_PER_RPC_REQUEST,
118                rpc_timeout_secs,
119            ),
120            enforce_token_fields,
121        }
122    }
123
124    /// Fetches complete token information (name, symbol, decimals) from an ERC20 contract.
125    ///
126    /// # Errors
127    ///
128    /// Returns an error if any of the contract calls fail.
129    /// - [`BlockchainRpcClientError::ClientError`] if an RPC call fails.
130    /// - [`BlockchainRpcClientError::AbiDecodingError`] if ABI decoding fails.
131    pub async fn fetch_token_info(
132        &self,
133        token_address: &Address,
134    ) -> Result<TokenInfo, TokenInfoError> {
135        let calls = vec![
136            ContractCall {
137                target: *token_address,
138                allow_failure: true,
139                call_data: ERC20::nameCall.abi_encode(),
140            },
141            ContractCall {
142                target: *token_address,
143                allow_failure: true,
144                call_data: ERC20::symbolCall.abi_encode(),
145            },
146            ContractCall {
147                target: *token_address,
148                allow_failure: true,
149                call_data: ERC20::decimalsCall.abi_encode(),
150            },
151        ];
152
153        let results = self.base.execute_multicall(calls, None).await?;
154
155        if results.len() != 3 {
156            return Err(TokenInfoError::UnexpectedResultCount {
157                expected: 3,
158                actual: results.len(),
159            });
160        }
161
162        let name = parse_erc20_string_result(&results[0], Erc20Field::Name, token_address)?;
163        let symbol = parse_erc20_string_result(&results[1], Erc20Field::Symbol, token_address)?;
164        let decimals = parse_erc20_decimals_result(&results[2], token_address)?;
165
166        if self.enforce_token_fields && name.is_empty() {
167            return Err(TokenInfoError::EmptyTokenField {
168                field: Erc20Field::Name,
169                address: *token_address,
170            });
171        }
172
173        if self.enforce_token_fields && symbol.is_empty() {
174            return Err(TokenInfoError::EmptyTokenField {
175                field: Erc20Field::Symbol,
176                address: *token_address,
177            });
178        }
179
180        Ok(TokenInfo {
181            name,
182            symbol,
183            decimals,
184        })
185    }
186
187    /// Fetches token information for multiple tokens in a single multicall.
188    ///
189    /// If the multicall fails (typically due to expired/broken contracts causing RPC "out of gas"),
190    /// automatically falls back to individual token fetches to isolate problematic contracts.
191    ///
192    /// # Errors
193    ///
194    /// Returns an error only if the operation cannot proceed. Multicall failures trigger
195    /// automatic fallback to individual fetches. Individual token failures are captured
196    /// in the Result values of the returned `HashMap`.
197    pub async fn batch_fetch_token_info(
198        &self,
199        token_addresses: &[Address],
200    ) -> Result<HashMap<Address, Result<TokenInfo, TokenInfoError>>, BlockchainRpcClientError> {
201        // Build calls for all tokens (3 calls per token)
202        let mut calls = Vec::with_capacity(token_addresses.len() * 3);
203
204        for token_address in token_addresses {
205            calls.extend([
206                ContractCall {
207                    target: *token_address,
208                    allow_failure: true, // Allow individual token failures
209                    call_data: ERC20::nameCall.abi_encode(),
210                },
211                ContractCall {
212                    target: *token_address,
213                    allow_failure: true,
214                    call_data: ERC20::symbolCall.abi_encode(),
215                },
216                ContractCall {
217                    target: *token_address,
218                    allow_failure: true,
219                    call_data: ERC20::decimalsCall.abi_encode(),
220                },
221            ]);
222        }
223
224        // Try batch multicall first
225        let results = match self.base.execute_multicall(calls, None).await {
226            Ok(results) => results,
227            Err(e) => {
228                // Multicall failed (likely expired/broken contract causing RPC failure)
229                log::warn!(
230                    "Batch multicall failed: {}. Falling back to individual fetches for {} tokens",
231                    e,
232                    token_addresses.len()
233                );
234
235                // Fallback: fetch each token individually to isolate problematic contracts
236                let mut token_infos = HashMap::with_capacity(token_addresses.len());
237                for token_address in token_addresses {
238                    match self.fetch_token_info(token_address).await {
239                        Ok(info) => {
240                            token_infos.insert(*token_address, Ok(info));
241                        }
242                        Err(e) => {
243                            log::debug!(
244                                "Token {token_address} failed individual fetch (likely expired/broken): {e}"
245                            );
246                            token_infos.insert(*token_address, Err(e));
247                        }
248                    }
249                }
250
251                return Ok(token_infos);
252            }
253        };
254
255        let mut token_infos = HashMap::with_capacity(token_addresses.len());
256        for (i, token_address) in token_addresses.iter().enumerate() {
257            let base_idx = i * 3;
258
259            // Check if we have all 3 results for this token.
260            if base_idx + 2 >= results.len() {
261                log::error!("Incomplete results from multicall for token {token_address}");
262                token_infos.insert(
263                    *token_address,
264                    Err(TokenInfoError::UnexpectedResultCount {
265                        expected: 3,
266                        actual: results.len().saturating_sub(base_idx),
267                    }),
268                );
269                continue;
270            }
271
272            let token_info =
273                parse_batch_token_results(&results[base_idx..base_idx + 3], token_address);
274            token_infos.insert(*token_address, token_info);
275        }
276
277        Ok(token_infos)
278    }
279
280    /// Fetches the balance of a specific account for this ERC20 token.
281    ///
282    /// # Errors
283    ///
284    /// Returns an error if the contract call fails.
285    /// - [`BlockchainRpcClientError::ClientError`] if an RPC call fails.
286    /// - [`BlockchainRpcClientError::AbiDecodingError`] if ABI decoding fails.
287    pub async fn balance_of(
288        &self,
289        token_address: &Address,
290        account: &Address,
291    ) -> Result<U256, BlockchainRpcClientError> {
292        self.balance_of_with_block(token_address, account, None)
293            .await
294    }
295
296    async fn balance_of_with_block(
297        &self,
298        token_address: &Address,
299        account: &Address,
300        block: Option<u64>,
301    ) -> Result<U256, BlockchainRpcClientError> {
302        let call_data = ERC20::balanceOfCall { account: *account }.abi_encode();
303        let result = self
304            .base
305            .execute_call(token_address, &call_data, block)
306            .await?;
307
308        ERC20::balanceOfCall::abi_decode_returns(&result)
309            .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
310    }
311
312    /// Fetches the token decimal precision used to scale raw amounts.
313    ///
314    /// # Errors
315    ///
316    /// Returns an error if the contract call fails or its result cannot be decoded.
317    pub async fn decimals(&self, token_address: &Address) -> Result<u8, BlockchainRpcClientError> {
318        self.decimals_with_block(token_address, None).await
319    }
320
321    async fn decimals_with_block(
322        &self,
323        token_address: &Address,
324        block: Option<u64>,
325    ) -> Result<u8, BlockchainRpcClientError> {
326        let call_data = ERC20::decimalsCall {}.abi_encode();
327        let result = self
328            .base
329            .execute_call(token_address, &call_data, block)
330            .await?;
331
332        ERC20::decimalsCall::abi_decode_returns(&result)
333            .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
334    }
335
336    /// Fetches the exact allowance an owner has granted a spender for this ERC20 token.
337    ///
338    /// # Errors
339    ///
340    /// Returns an error if the contract call fails.
341    /// - [`BlockchainRpcClientError::ClientError`] if an RPC call fails.
342    /// - [`BlockchainRpcClientError::AbiDecodingError`] if ABI decoding fails.
343    pub async fn allowance(
344        &self,
345        token_address: &Address,
346        owner: &Address,
347        spender: &Address,
348    ) -> Result<U256, BlockchainRpcClientError> {
349        self.allowance_with_block(token_address, owner, spender, None)
350            .await
351    }
352
353    async fn allowance_with_block(
354        &self,
355        token_address: &Address,
356        owner: &Address,
357        spender: &Address,
358        block: Option<u64>,
359    ) -> Result<U256, BlockchainRpcClientError> {
360        let call_data = ERC20::allowanceCall {
361            owner: *owner,
362            spender: *spender,
363        }
364        .abi_encode();
365        let result = self
366            .base
367            .execute_call(token_address, &call_data, block)
368            .await?;
369
370        ERC20::allowanceCall::abi_decode_returns(&result)
371            .map_err(|e| BlockchainRpcClientError::AbiDecodingError(e.to_string()))
372    }
373}
374
375/// Generic parser for ERC20 string results (name, symbol)
376fn parse_erc20_string_result(
377    result: &Multicall3::Result,
378    field_name: Erc20Field,
379    token_address: &Address,
380) -> Result<String, TokenInfoError> {
381    let data = validate_multicall_result(result, field_name, token_address)?;
382
383    match field_name {
384        Erc20Field::Name => ERC20::nameCall::abi_decode_returns(data),
385        Erc20Field::Symbol => ERC20::symbolCall::abi_decode_returns(data),
386        Erc20Field::Decimals => {
387            return Err(TokenInfoError::DecodingError {
388                field: field_name.to_string(),
389                address: *token_address,
390                reason: "Expected Name or Symbol for parse_erc20_string_result function argument"
391                    .to_string(),
392                raw_data: data.to_string(),
393            });
394        }
395    }
396    .map_err(|e| TokenInfoError::DecodingError {
397        field: field_name.to_string(),
398        address: *token_address,
399        reason: e.to_string(),
400        raw_data: result.returnData.to_string(),
401    })
402}
403
404/// Generic parser for ERC20 decimals result
405fn parse_erc20_decimals_result(
406    result: &Multicall3::Result,
407    token_address: &Address,
408) -> Result<u8, TokenInfoError> {
409    let data = validate_multicall_result(result, Erc20Field::Decimals, token_address)?;
410
411    ERC20::decimalsCall::abi_decode_returns(data).map_err(|e| TokenInfoError::DecodingError {
412        field: "decimals".to_string(),
413        address: *token_address,
414        reason: e.to_string(),
415        raw_data: result.returnData.to_string(),
416    })
417}
418
419fn validate_multicall_result<'a>(
420    result: &'a Multicall3::Result,
421    field: Erc20Field,
422    token_address: &Address,
423) -> Result<&'a Bytes, TokenInfoError> {
424    if !result.success {
425        return Err(TokenInfoError::CallFailed {
426            field: match field {
427                Erc20Field::Decimals => "decimals".to_string(),
428                _ => field.to_string(),
429            },
430            address: *token_address,
431            reason: decode_revert_reason(&result.returnData),
432            raw_data: result.returnData.to_string(),
433        });
434    }
435
436    if result.returnData.is_empty() {
437        return Err(TokenInfoError::EmptyTokenField {
438            field,
439            address: *token_address,
440        });
441    }
442
443    Ok(&result.returnData)
444}
445
446fn decode_revert_reason(data: &Bytes) -> String {
447    if data.is_empty() {
448        "Call failed without revert data".to_string()
449    } else {
450        format!("Call failed with data: {data}")
451    }
452}
453
454/// Parses token information from a slice of 3 multicall results.
455///
456/// Expects results in order: name, symbol, decimals.
457/// Returns Ok(TokenInfo) if all three calls succeeded, or an Err with a
458/// descriptive error message if any call failed.
459fn parse_batch_token_results(
460    results: &[Multicall3::Result],
461    token_address: &Address,
462) -> Result<TokenInfo, TokenInfoError> {
463    if results.len() != 3 {
464        return Err(TokenInfoError::UnexpectedResultCount {
465            expected: 3,
466            actual: results.len(),
467        });
468    }
469
470    let name = parse_erc20_string_result(&results[0], Erc20Field::Name, token_address)?;
471    let symbol = parse_erc20_string_result(&results[1], Erc20Field::Symbol, token_address)?;
472    let decimals = parse_erc20_decimals_result(&results[2], token_address)?;
473
474    Ok(TokenInfo {
475        name,
476        symbol,
477        decimals,
478    })
479}
480
481#[cfg(test)]
482mod tests {
483    use alloy::primitives::{Bytes, address};
484    use nautilus_core::hex;
485    use rstest::{fixture, rstest};
486
487    use super::*;
488    use crate::rpc::http::tests::mock::{MockRpcState, start_mock_rpc_server};
489
490    const CALL_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_call_balance.json");
491    const CALL_ALLOWANCE: &str =
492        include_str!("../../test_data/execution/rpc_eth_call_allowance.json");
493    #[fixture]
494    fn token_address() -> Address {
495        address!("25b76A90E389bD644a29db919b136Dc63B174Ec7")
496    }
497
498    #[fixture]
499    fn successful_name_result() -> Multicall3::Result {
500        Multicall3::Result {
501            success: true,
502            returnData: Bytes::from(hex::decode("00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000007546f6b656e204100000000000000000000000000000000000000000000000000").unwrap()),
503        }
504    }
505
506    #[fixture]
507    fn successful_symbol_result() -> Multicall3::Result {
508        Multicall3::Result {
509            success: true,
510            returnData: Bytes::from(hex::decode("0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000776546f6b656e4100000000000000000000000000000000000000000000000000").unwrap()),
511        }
512    }
513
514    #[fixture]
515    fn failed_name_result() -> Multicall3::Result {
516        Multicall3::Result {
517            success: false,
518            returnData: Bytes::from(vec![]),
519        }
520    }
521
522    #[fixture]
523    fn failed_token_address() -> Address {
524        address!("00000000049084A92F8964B76845ab6DE54EB229")
525    }
526
527    #[fixture]
528    fn success_but_empty_result() -> Multicall3::Result {
529        Multicall3::Result {
530            success: true,
531            returnData: Bytes::from(vec![]),
532        }
533    }
534
535    #[fixture]
536    fn empty_token_address() -> Address {
537        address!("a5b00cEc63694319495d605AA414203F9714F47E")
538    }
539
540    #[fixture]
541    fn non_abi_encoded_string_result() -> Multicall3::Result {
542        // Returns raw string bytes without ABI encoding - "Rico" as raw bytes
543        Multicall3::Result {
544            success: true,
545            returnData: Bytes::from(
546                hex::decode("5269636f00000000000000000000000000000000000000000000000000000000")
547                    .unwrap(),
548            ),
549        }
550    }
551
552    #[fixture]
553    fn non_abi_encoded_token_address() -> Address {
554        address!("5374EcC160A4bd68446B43B5A6B132F9c001C54C")
555    }
556
557    #[fixture]
558    fn non_standard_selector_result() -> Multicall3::Result {
559        // Returns function selector instead of actual data
560        Multicall3::Result {
561            success: true,
562            returnData: Bytes::from(
563                hex::decode("06fdde0300000000000000000000000000000000000000000000000000000000")
564                    .unwrap(),
565            ),
566        }
567    }
568
569    #[fixture]
570    fn non_abi_encoded_long_string_result() -> Multicall3::Result {
571        // Returns raw string bytes without ABI encoding - longer string example
572        Multicall3::Result {
573            success: true,
574            returnData: Bytes::from(
575                hex::decode("5269636f62616e6b205269736b20536861726500000000000000000000000000")
576                    .unwrap(),
577            ),
578        }
579    }
580
581    #[rstest]
582    fn test_parse_erc20_string_result_name_success(
583        successful_name_result: Multicall3::Result,
584        token_address: Address,
585    ) {
586        let result =
587            parse_erc20_string_result(&successful_name_result, Erc20Field::Name, &token_address);
588        assert!(result.is_ok());
589        assert_eq!(result.unwrap(), "Token A");
590    }
591
592    #[rstest]
593    fn test_parse_erc20_string_result_symbol_success(
594        successful_symbol_result: Multicall3::Result,
595        token_address: Address,
596    ) {
597        let result = parse_erc20_string_result(
598            &successful_symbol_result,
599            Erc20Field::Symbol,
600            &token_address,
601        );
602        assert!(result.is_ok());
603        assert_eq!(result.unwrap(), "vTokenA");
604    }
605
606    #[rstest]
607    fn test_parse_erc20_string_result_name_failed_with_specific_address(
608        failed_name_result: Multicall3::Result,
609        failed_token_address: Address,
610    ) {
611        let result =
612            parse_erc20_string_result(&failed_name_result, Erc20Field::Name, &failed_token_address);
613        assert!(result.is_err());
614        match result.unwrap_err() {
615            TokenInfoError::CallFailed {
616                field,
617                address,
618                reason,
619                raw_data: _,
620            } => {
621                assert_eq!(field, "Name");
622                assert_eq!(address, failed_token_address);
623                assert_eq!(reason, "Call failed without revert data");
624            }
625            _ => panic!("Expected DecodingError"),
626        }
627    }
628
629    #[rstest]
630    fn test_parse_erc20_string_result_success_but_empty_name(
631        success_but_empty_result: Multicall3::Result,
632        empty_token_address: Address,
633    ) {
634        let result = parse_erc20_string_result(
635            &success_but_empty_result,
636            Erc20Field::Name,
637            &empty_token_address,
638        );
639        assert!(result.is_err());
640        match result.unwrap_err() {
641            TokenInfoError::EmptyTokenField { field, address } => {
642                assert!(matches!(field, Erc20Field::Name));
643                assert_eq!(address, empty_token_address);
644            }
645            _ => panic!("Expected EmptyTokenField error"),
646        }
647    }
648
649    #[rstest]
650    fn test_parse_erc20_decimals_result_failed(
651        failed_name_result: Multicall3::Result,
652        failed_token_address: Address,
653    ) {
654        let result = parse_erc20_decimals_result(&failed_name_result, &failed_token_address);
655        match result.unwrap_err() {
656            TokenInfoError::CallFailed {
657                field,
658                address,
659                reason,
660                raw_data,
661            } => {
662                assert_eq!(field, "decimals");
663                assert_eq!(address, failed_token_address);
664                assert_eq!(reason, "Call failed without revert data");
665                assert_eq!(raw_data, "0x");
666            }
667            _ => panic!("Expected CallFailed"),
668        }
669    }
670
671    #[rstest]
672    fn test_parse_erc20_decimals_result_success_but_empty(
673        success_but_empty_result: Multicall3::Result,
674        empty_token_address: Address,
675    ) {
676        let result = parse_erc20_decimals_result(&success_but_empty_result, &empty_token_address);
677        assert!(result.is_err());
678        match result.unwrap_err() {
679            TokenInfoError::EmptyTokenField { field, address } => {
680                assert!(matches!(field, Erc20Field::Decimals));
681                assert_eq!(address, empty_token_address);
682            }
683            _ => panic!("Expected EmptyTokenField error"),
684        }
685    }
686
687    #[rstest]
688    fn test_parse_non_abi_encoded_string(
689        non_abi_encoded_string_result: Multicall3::Result,
690        non_abi_encoded_token_address: Address,
691    ) {
692        let result = parse_erc20_string_result(
693            &non_abi_encoded_string_result,
694            Erc20Field::Name,
695            &non_abi_encoded_token_address,
696        );
697        assert!(result.is_err());
698        match result.unwrap_err() {
699            TokenInfoError::DecodingError {
700                field,
701                address,
702                reason,
703                raw_data,
704            } => {
705                assert_eq!(field, "Name");
706                assert_eq!(address, non_abi_encoded_token_address);
707                assert!(reason.contains("type check failed"));
708                assert_eq!(
709                    raw_data,
710                    "0x5269636f00000000000000000000000000000000000000000000000000000000"
711                );
712                // Raw bytes "Rico" without ABI encoding
713            }
714            _ => panic!("Expected DecodingError"),
715        }
716    }
717
718    #[rstest]
719    fn test_parse_non_standard_selector_return(
720        non_standard_selector_result: Multicall3::Result,
721        token_address: Address,
722    ) {
723        let result = parse_erc20_string_result(
724            &non_standard_selector_result,
725            Erc20Field::Name,
726            &token_address,
727        );
728        assert!(result.is_err());
729        match result.unwrap_err() {
730            TokenInfoError::DecodingError {
731                field,
732                address,
733                reason,
734                raw_data,
735            } => {
736                assert_eq!(field, "Name");
737                assert_eq!(address, token_address);
738                assert!(reason.contains("type check failed"));
739                assert_eq!(
740                    raw_data,
741                    "0x06fdde0300000000000000000000000000000000000000000000000000000000"
742                );
743            }
744            _ => panic!("Expected DecodingError"),
745        }
746    }
747
748    #[rstest]
749    fn test_parse_non_abi_encoded_long_string(
750        non_abi_encoded_long_string_result: Multicall3::Result,
751        non_abi_encoded_token_address: Address,
752    ) {
753        let result = parse_erc20_string_result(
754            &non_abi_encoded_long_string_result,
755            Erc20Field::Name,
756            &non_abi_encoded_token_address,
757        );
758        assert!(result.is_err());
759        match result.unwrap_err() {
760            TokenInfoError::DecodingError {
761                field,
762                address,
763                reason,
764                raw_data,
765            } => {
766                assert_eq!(field, "Name");
767                assert_eq!(address, non_abi_encoded_token_address);
768                assert!(reason.contains("type check failed"));
769                assert_eq!(
770                    raw_data,
771                    "0x5269636f62616e6b205269736b20536861726500000000000000000000000000"
772                );
773                // Example of longer non-ABI encoded string
774            }
775            _ => panic!("Expected DecodingError"),
776        }
777    }
778
779    #[rstest]
780    fn test_approve_calldata_matches_known_vector() {
781        let spender = address!("E592427A0AEce92De3Edee1F18E0157C05861564");
782        let calldata = ERC20::approveCall {
783            spender,
784            amount: U256::from(1_000_000_000_000_000_000u64),
785        }
786        .abi_encode();
787
788        // approve(address,uint256) selector 0x095ea7b3, spender word, amount word
789        let expected = format!(
790            "095ea7b3{:0>64}{:0>64}",
791            "e592427a0aece92de3edee1f18e0157c05861564", "0de0b6b3a7640000"
792        );
793        assert_eq!(hex::encode(&calldata), expected);
794    }
795
796    #[rstest]
797    fn test_allowance_calldata_matches_known_vector() {
798        let owner = address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266");
799        let spender = address!("E592427A0AEce92De3Edee1F18E0157C05861564");
800        let calldata = ERC20::allowanceCall { owner, spender }.abi_encode();
801
802        // allowance(address,address) selector 0xdd62ed3e, owner word, spender word
803        let expected = format!(
804            "dd62ed3e{:0>64}{:0>64}",
805            "f39fd6e51aad88f6f4ce6ab8827279cfffb92266", "e592427a0aece92de3edee1f18e0157c05861564"
806        );
807        assert_eq!(hex::encode(&calldata), expected);
808    }
809
810    async fn erc20_contract_against(state: MockRpcState) -> (Erc20Contract, MockRpcState) {
811        let addr = start_mock_rpc_server(state.clone()).await;
812        let rpc_client = Arc::new(BlockchainHttpRpcClient::new(
813            format!("http://{addr}"),
814            None,
815            None,
816        ));
817        (Erc20Contract::new(rpc_client, true), state)
818    }
819
820    #[tokio::test]
821    async fn test_balance_of_against_mock_rpc() {
822        let state = MockRpcState::default().with_call_response("0x70a08231", CALL_BALANCE);
823        let (contract, state) = erc20_contract_against(state).await;
824
825        let balance = contract
826            .balance_of(
827                &address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
828                &address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
829            )
830            .await
831            .unwrap();
832
833        assert_eq!(balance, U256::from(500_000_000_000_000_000u64));
834        let requests = state.recorded_requests();
835        assert_eq!(requests.len(), 1);
836        assert_eq!(requests[0]["params"][1], "latest");
837    }
838
839    #[tokio::test]
840    async fn test_allowance_against_mock_rpc() {
841        let state = MockRpcState::default().with_call_response("0xdd62ed3e", CALL_ALLOWANCE);
842        let (contract, state) = erc20_contract_against(state).await;
843
844        let allowance = contract
845            .allowance(
846                &address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
847                &address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
848                &address!("E592427A0AEce92De3Edee1F18E0157C05861564"),
849            )
850            .await
851            .unwrap();
852
853        assert_eq!(allowance, U256::from(1_000_000_000_000_000_000u64));
854        let requests = state.recorded_requests();
855        assert_eq!(requests.len(), 1);
856        assert_eq!(requests[0]["params"][1], "latest");
857    }
858}