Skip to main content

ant_node/payment/
wallet.rs

1//! EVM wallet management for receiving payments.
2//!
3//! Handles parsing and validation of EVM wallet addresses (rewards addresses)
4//! that nodes use to receive payments for storing data.
5
6use crate::config::EvmNetworkConfig;
7use crate::error::{Error, Result};
8use evmlib::Network as EvmNetwork;
9use evmlib::RewardsAddress;
10
11/// EVM wallet configuration for a node.
12#[derive(Debug, Clone)]
13pub struct WalletConfig {
14    /// The rewards address where payments are received.
15    pub rewards_address: Option<RewardsAddress>,
16    /// The EVM network (Arbitrum One or Sepolia).
17    pub network: EvmNetwork,
18}
19
20impl WalletConfig {
21    /// Create a new wallet configuration.
22    ///
23    /// # Arguments
24    ///
25    /// * `rewards_address` - Optional EVM address string (e.g., "0x...")
26    /// * `evm_network` - The EVM network configuration
27    ///
28    /// # Errors
29    ///
30    /// Returns an error if the address string is invalid.
31    pub fn new(rewards_address: Option<&str>, evm_network: EvmNetworkConfig) -> Result<Self> {
32        let rewards_address = rewards_address.map(parse_rewards_address).transpose()?;
33        let network = evm_network.into_evm_network();
34
35        Ok(Self {
36            rewards_address,
37            network,
38        })
39    }
40
41    /// Check if the wallet has a rewards address configured.
42    #[must_use]
43    pub fn has_rewards_address(&self) -> bool {
44        self.rewards_address.is_some()
45    }
46
47    /// Get the rewards address if configured.
48    #[must_use]
49    pub fn get_rewards_address(&self) -> Option<&RewardsAddress> {
50        self.rewards_address.as_ref()
51    }
52
53    /// Check if this wallet is configured for mainnet.
54    #[must_use]
55    pub fn is_mainnet(&self) -> bool {
56        matches!(self.network, EvmNetwork::ArbitrumOne)
57    }
58}
59
60/// Parse an EVM address string into a `RewardsAddress`.
61///
62/// # Arguments
63///
64/// * `address` - EVM address string (e.g., "0x1234...")
65///
66/// # Errors
67///
68/// Returns an error if the address format is invalid.
69pub fn parse_rewards_address(address: &str) -> Result<RewardsAddress> {
70    // Validate format: should start with 0x and be 42 characters total (0x + 40 hex chars)
71    if !address.starts_with("0x") && !address.starts_with("0X") {
72        return Err(Error::Payment(format!(
73            "Invalid rewards address format: must start with '0x', got: {address}"
74        )));
75    }
76
77    let len = address.len();
78    if len != 42 {
79        return Err(Error::Payment(format!(
80            "Invalid rewards address length: expected 42 characters, got {len}",
81        )));
82    }
83
84    // Validate hex characters
85    let hex_part = &address[2..];
86    if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
87        return Err(Error::Payment(format!(
88            "Invalid rewards address: contains non-hex characters: {address}"
89        )));
90    }
91
92    // Parse into bytes
93    let bytes = hex::decode(hex_part)
94        .map_err(|e| Error::Payment(format!("Failed to decode rewards address: {e}")))?;
95
96    // Convert to fixed-size array
97    let mut address_bytes = [0u8; 20];
98    address_bytes.copy_from_slice(&bytes);
99
100    Ok(RewardsAddress::new(address_bytes))
101}
102
103/// Validate that an EVM address is properly formatted.
104///
105/// # Arguments
106///
107/// * `address` - EVM address string to validate
108///
109/// # Returns
110///
111/// `true` if the address is valid, `false` otherwise.
112#[must_use]
113pub fn is_valid_address(address: &str) -> bool {
114    parse_rewards_address(address).is_ok()
115}
116
117#[cfg(test)]
118#[allow(clippy::expect_used)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn test_parse_valid_address() {
124        let address = "0x742d35Cc6634C0532925a3b844Bc9e7595916Da2";
125        let result = parse_rewards_address(address);
126        assert!(result.is_ok());
127    }
128
129    #[test]
130    fn test_parse_lowercase_address() {
131        let address = "0x742d35cc6634c0532925a3b844bc9e7595916da2";
132        let result = parse_rewards_address(address);
133        assert!(result.is_ok());
134    }
135
136    #[test]
137    fn test_invalid_prefix() {
138        let address = "742d35Cc6634C0532925a3b844Bc9e7595916Da2";
139        let result = parse_rewards_address(address);
140        assert!(result.is_err());
141    }
142
143    #[test]
144    fn test_invalid_length() {
145        let address = "0x742d35Cc6634C0532925a3b844Bc9e7595916Da";
146        let result = parse_rewards_address(address);
147        assert!(result.is_err());
148    }
149
150    #[test]
151    fn test_invalid_hex_chars() {
152        let address = "0x742d35Cc6634C0532925a3b844Bc9e7595916DgZ";
153        let result = parse_rewards_address(address);
154        assert!(result.is_err());
155    }
156
157    #[test]
158    fn test_is_valid_address() {
159        assert!(is_valid_address(
160            "0x742d35Cc6634C0532925a3b844Bc9e7595916Da2"
161        ));
162        assert!(!is_valid_address("invalid"));
163    }
164
165    #[test]
166    fn test_wallet_config_new() {
167        let config = WalletConfig::new(
168            Some("0x742d35Cc6634C0532925a3b844Bc9e7595916Da2"),
169            EvmNetworkConfig::ArbitrumSepolia,
170        );
171        assert!(config.is_ok());
172        let config = config.expect("valid config");
173        assert!(config.has_rewards_address());
174        assert!(!config.is_mainnet());
175    }
176
177    #[test]
178    fn test_wallet_config_no_address() {
179        let config = WalletConfig::new(None, EvmNetworkConfig::ArbitrumOne);
180        assert!(config.is_ok());
181        let config = config.expect("valid config");
182        assert!(!config.has_rewards_address());
183        assert!(config.is_mainnet());
184    }
185
186    #[test]
187    fn test_uppercase_0x_prefix() {
188        let address = "0X742d35Cc6634C0532925a3b844Bc9e7595916Da2";
189        let result = parse_rewards_address(address);
190        assert!(result.is_ok());
191    }
192
193    #[test]
194    fn test_empty_string() {
195        let result = parse_rewards_address("");
196        assert!(result.is_err());
197    }
198
199    #[test]
200    fn test_just_0x_prefix() {
201        let result = parse_rewards_address("0x");
202        assert!(result.is_err());
203        let err_msg = format!("{}", result.expect_err("should fail"));
204        assert!(err_msg.contains("length"));
205    }
206
207    #[test]
208    fn test_address_with_spaces() {
209        let result = parse_rewards_address("0x 742d35Cc6634C0532925a3b844Bc9e7595916Da");
210        assert!(result.is_err());
211    }
212
213    #[test]
214    fn test_get_rewards_address_none() {
215        let config = WalletConfig::new(None, EvmNetworkConfig::ArbitrumOne).expect("valid config");
216        assert!(config.get_rewards_address().is_none());
217    }
218
219    #[test]
220    fn test_get_rewards_address_some() {
221        let config = WalletConfig::new(
222            Some("0x742d35Cc6634C0532925a3b844Bc9e7595916Da2"),
223            EvmNetworkConfig::ArbitrumOne,
224        )
225        .expect("valid config");
226        assert!(config.get_rewards_address().is_some());
227    }
228
229    #[test]
230    fn test_all_zeros_address() {
231        let address = "0x0000000000000000000000000000000000000000";
232        let result = parse_rewards_address(address);
233        assert!(result.is_ok());
234    }
235
236    #[test]
237    fn test_all_ff_address() {
238        let address = "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF";
239        let result = parse_rewards_address(address);
240        assert!(result.is_ok());
241    }
242
243    #[test]
244    fn test_too_long_address() {
245        let address = "0x742d35Cc6634C0532925a3b844Bc9e7595916Da2a";
246        let result = parse_rewards_address(address);
247        assert!(result.is_err());
248        let err_msg = format!("{}", result.expect_err("should fail"));
249        assert!(err_msg.contains("length"));
250    }
251
252    #[test]
253    fn test_wallet_config_invalid_address() {
254        let result = WalletConfig::new(Some("invalid"), EvmNetworkConfig::ArbitrumOne);
255        assert!(result.is_err());
256    }
257}