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
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 #[must_use]
47 pub fn has_rewards_address(&self) -> bool {
48 self.rewards_address.is_some()
49 }
50
51 #[must_use]
53 pub fn get_rewards_address(&self) -> Option<&RewardsAddress> {
54 self.rewards_address.as_ref()
55 }
56
57 #[must_use]
59 pub fn is_mainnet(&self) -> bool {
60 matches!(self.network, EvmNetwork::ArbitrumOne)
61 }
62}
63
64pub fn parse_rewards_address(address: &str) -> Result<RewardsAddress> {
74 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 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 let bytes = hex::decode(hex_part)
98 .map_err(|e| Error::Payment(format!("Failed to decode rewards address: {e}")))?;
99
100 let mut address_bytes = [0u8; 20];
102 address_bytes.copy_from_slice(&bytes);
103
104 Ok(RewardsAddress::new(address_bytes))
105}
106
107#[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}