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::{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: &str,
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(&self, token_address: &str) -> Result<String, BlockchainRpcClientError> {
98        let name_call = ERC20::nameCall.abi_encode();
99        let rpc_request = self
100            .client
101            .construct_eth_call(token_address, name_call.as_slice());
102        let encoded_name = self
103            .client
104            .execute_eth_call::<String>(rpc_request)
105            .await
106            .map_err(|e| {
107                BlockchainRpcClientError::ClientError(format!("Error fetching name: {e}"))
108            })?;
109        let bytes = decode_hex_response(&encoded_name)?;
110        ERC20::nameCall::abi_decode_returns(&bytes).map_err(|e| {
111            BlockchainRpcClientError::AbiDecodingError(format!(
112                "Error decoding ERC20 contract name with error {e}"
113            ))
114        })
115    }
116
117    /// Fetches the symbol of an ERC20 token.
118    async fn fetch_symbol(&self, token_address: &str) -> Result<String, BlockchainRpcClientError> {
119        let symbol_call = ERC20::symbolCall.abi_encode();
120        let rpc_request = self
121            .client
122            .construct_eth_call(token_address, symbol_call.as_slice());
123        let encoded_symbol = self
124            .client
125            .execute_eth_call::<String>(rpc_request)
126            .await
127            .map_err(|e| {
128                BlockchainRpcClientError::ClientError(format!("Error fetching symbol: {e}"))
129            })?;
130        let bytes = decode_hex_response(&encoded_symbol)?;
131        ERC20::symbolCall::abi_decode_returns(&bytes).map_err(|e| {
132            BlockchainRpcClientError::AbiDecodingError(format!(
133                "Error decoding ERC20 contract symbol with error {e}"
134            ))
135        })
136    }
137
138    /// Fetches the number of decimals used by an ERC20 token.
139    async fn fetch_decimals(&self, token_address: &str) -> Result<u8, BlockchainRpcClientError> {
140        let decimals_call = ERC20::decimalsCall.abi_encode();
141        let rpc_request = self
142            .client
143            .construct_eth_call(token_address, decimals_call.as_slice());
144        let encoded_decimals = self
145            .client
146            .execute_eth_call::<String>(rpc_request)
147            .await
148            .map_err(|e| {
149                BlockchainRpcClientError::ClientError(format!("Error fetching decimals: {e}"))
150            })?;
151        let bytes = decode_hex_response(&encoded_decimals)?;
152        ERC20::decimalsCall::abi_decode_returns(&bytes).map_err(|e| {
153            BlockchainRpcClientError::AbiDecodingError(format!(
154                "Error decoding ERC20 contract decimals with error {e}"
155            ))
156        })
157    }
158}