nautilus_blockchain/contracts/
erc20.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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::sync::Arc;
17
18use alloy::{primitives::Address, sol, sol_types::SolCall};
19
20use crate::rpc::{error::BlockchainRpcClientError, http::BlockchainHttpRpcClient};
21
22sol! {
23    #[sol(rpc)]
24    contract ERC20 {
25        function name() external view returns (string);
26        function symbol() external view returns (string);
27        function decimals() external view returns (uint8);
28    }
29}
30
31/// Represents the essential metadata information for an ERC20 token.
32#[derive(Debug, Clone)]
33pub struct TokenInfo {
34    /// The full name of the token.
35    pub name: String,
36    /// The ticker symbol of the token.
37    pub symbol: String,
38    /// The number of decimal places the token uses for representing fractional amounts.
39    pub decimals: u8,
40}
41
42/// Interface for interacting with ERC20 token contracts on a blockchain.
43///
44/// This struct provides methods to fetch token metadata (name, symbol, decimals).
45/// From ERC20-compliant tokens on any EVM-compatible blockchain.
46#[derive(Debug)]
47pub struct Erc20Contract {
48    /// The HTTP RPC client used to communicate with the blockchain node.
49    client: Arc<BlockchainHttpRpcClient>,
50}
51
52/// Decodes a hexadecimal string response from a blockchain RPC call.
53///
54/// # Errors
55///
56/// Returns an `BlockchainRpcClientError::AbiDecodingError` if the hex decoding fails.
57fn decode_hex_response(encoded_response: &str) -> Result<Vec<u8>, BlockchainRpcClientError> {
58    // Remove the "0x" prefix if present
59    let encoded_str = encoded_response
60        .strip_prefix("0x")
61        .unwrap_or(encoded_response);
62    hex::decode(encoded_str).map_err(|e| {
63        BlockchainRpcClientError::AbiDecodingError(format!("Error decoding hex response: {e}"))
64    })
65}
66
67impl Erc20Contract {
68    /// Creates a new ERC20 contract interface with the specified RPC client.
69    #[must_use]
70    pub const fn new(client: Arc<BlockchainHttpRpcClient>) -> Self {
71        Self { client }
72    }
73
74    /// Fetches complete token information (name, symbol, decimals) from an ERC20 contract.
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if any of the contract calls fail.
79    /// - [`BlockchainRpcClientError::ClientError`] if an RPC call fails.
80    /// - [`BlockchainRpcClientError::AbiDecodingError`] if ABI decoding fails.
81    pub async fn fetch_token_info(
82        &self,
83        token_address: &Address,
84    ) -> Result<TokenInfo, BlockchainRpcClientError> {
85        let token_name = self.fetch_name(token_address).await?;
86        let token_symbol = self.fetch_symbol(token_address).await?;
87        let token_decimals = self.fetch_decimals(token_address).await?;
88
89        Ok(TokenInfo {
90            name: token_name,
91            symbol: token_symbol,
92            decimals: token_decimals,
93        })
94    }
95
96    /// Fetches the name of an ERC20 token.
97    async fn fetch_name(
98        &self,
99        token_address: &Address,
100    ) -> Result<String, BlockchainRpcClientError> {
101        let name_call = ERC20::nameCall.abi_encode();
102        let rpc_request = self
103            .client
104            .construct_eth_call(&token_address.to_string(), name_call.as_slice());
105        let encoded_name = self
106            .client
107            .execute_eth_call::<String>(rpc_request)
108            .await
109            .map_err(|e| {
110                BlockchainRpcClientError::ClientError(format!("Error fetching name: {e}"))
111            })?;
112        let bytes = decode_hex_response(&encoded_name)?;
113        ERC20::nameCall::abi_decode_returns(&bytes).map_err(|e| {
114            BlockchainRpcClientError::AbiDecodingError(format!(
115                "Error decoding ERC20 contract name with error {e}"
116            ))
117        })
118    }
119
120    /// Fetches the symbol of an ERC20 token.
121    async fn fetch_symbol(
122        &self,
123        token_address: &Address,
124    ) -> Result<String, BlockchainRpcClientError> {
125        let symbol_call = ERC20::symbolCall.abi_encode();
126        let rpc_request = self
127            .client
128            .construct_eth_call(&token_address.to_string(), symbol_call.as_slice());
129        let encoded_symbol = self
130            .client
131            .execute_eth_call::<String>(rpc_request)
132            .await
133            .map_err(|e| {
134                BlockchainRpcClientError::ClientError(format!("Error fetching symbol: {e}"))
135            })?;
136        let bytes = decode_hex_response(&encoded_symbol)?;
137        ERC20::symbolCall::abi_decode_returns(&bytes).map_err(|e| {
138            BlockchainRpcClientError::AbiDecodingError(format!(
139                "Error decoding ERC20 contract symbol with error {e}"
140            ))
141        })
142    }
143
144    /// Fetches the number of decimals used by an ERC20 token.
145    async fn fetch_decimals(
146        &self,
147        token_address: &Address,
148    ) -> Result<u8, BlockchainRpcClientError> {
149        let decimals_call = ERC20::decimalsCall.abi_encode();
150        let rpc_request = self
151            .client
152            .construct_eth_call(&token_address.to_string(), decimals_call.as_slice());
153        let encoded_decimals = self
154            .client
155            .execute_eth_call::<String>(rpc_request)
156            .await
157            .map_err(|e| {
158                BlockchainRpcClientError::ClientError(format!("Error fetching decimals: {e}"))
159            })?;
160        let bytes = decode_hex_response(&encoded_decimals)?;
161        ERC20::decimalsCall::abi_decode_returns(&bytes).map_err(|e| {
162            BlockchainRpcClientError::AbiDecodingError(format!(
163                "Error decoding ERC20 contract decimals with error {e}"
164            ))
165        })
166    }
167}