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