ant_node/payment/
wallet.rs1use crate::config::EvmNetworkConfig;
7use crate::error::{Error, Result};
8use evmlib::Network as EvmNetwork;
9use evmlib::RewardsAddress;
10
11#[derive(Debug, Clone)]
13pub struct WalletConfig {
14 pub rewards_address: Option<RewardsAddress>,
16 pub network: EvmNetwork,
18}
19
20impl WalletConfig {
21 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 #[must_use]
43 pub fn has_rewards_address(&self) -> bool {
44 self.rewards_address.is_some()
45 }
46
47 #[must_use]
49 pub fn get_rewards_address(&self) -> Option<&RewardsAddress> {
50 self.rewards_address.as_ref()
51 }
52
53 #[must_use]
55 pub fn is_mainnet(&self) -> bool {
56 matches!(self.network, EvmNetwork::ArbitrumOne)
57 }
58}
59
60pub fn parse_rewards_address(address: &str) -> Result<RewardsAddress> {
70 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 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 let bytes = hex::decode(hex_part)
94 .map_err(|e| Error::Payment(format!("Failed to decode rewards address: {e}")))?;
95
96 let mut address_bytes = [0u8; 20];
98 address_bytes.copy_from_slice(&bytes);
99
100 Ok(RewardsAddress::new(address_bytes))
101}
102
103#[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}