allsource_core/domain/value_objects/
wallet_address.rs1use crate::error::Result;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct WalletAddress(String);
24
25const BASE58_ALPHABET: &str = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
27
28impl WalletAddress {
29 pub fn new(value: String) -> Result<Self> {
46 Self::validate(&value)?;
47 Ok(Self(value))
48 }
49
50 pub(crate) fn new_unchecked(value: String) -> Self {
56 Self(value)
57 }
58
59 pub fn as_str(&self) -> &str {
61 &self.0
62 }
63
64 pub fn into_inner(self) -> String {
66 self.0
67 }
68
69 pub fn anonymized(&self) -> String {
81 if self.0.len() <= 8 {
82 return self.0.clone();
83 }
84 format!("{}...{}", &self.0[..4], &self.0[self.0.len() - 4..])
85 }
86
87 pub fn is_solana(&self) -> bool {
89 self.0.len() >= 32 && self.0.len() <= 44
91 }
92
93 fn validate(value: &str) -> Result<()> {
95 if value.is_empty() {
97 return Err(crate::error::AllSourceError::InvalidInput(
98 "Wallet address cannot be empty".to_string(),
99 ));
100 }
101
102 if value.len() < 32 || value.len() > 44 {
104 return Err(crate::error::AllSourceError::InvalidInput(format!(
105 "Wallet address must be between 32 and 44 characters, got {}",
106 value.len()
107 )));
108 }
109
110 if !value.chars().all(|c| BASE58_ALPHABET.contains(c)) {
112 return Err(crate::error::AllSourceError::InvalidInput(
113 "Wallet address contains invalid base58 characters".to_string(),
114 ));
115 }
116
117 Ok(())
118 }
119}
120
121impl fmt::Display for WalletAddress {
122 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123 write!(f, "{}", self.0)
124 }
125}
126
127impl TryFrom<&str> for WalletAddress {
128 type Error = crate::error::AllSourceError;
129
130 fn try_from(value: &str) -> Result<Self> {
131 WalletAddress::new(value.to_string())
132 }
133}
134
135impl TryFrom<String> for WalletAddress {
136 type Error = crate::error::AllSourceError;
137
138 fn try_from(value: String) -> Result<Self> {
139 WalletAddress::new(value)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 const VALID_SOLANA_ADDRESS: &str = "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
149
150 const VALID_SHORT_ADDRESS: &str = "11111111111111111111111111111111";
152
153 #[test]
154 fn test_create_valid_wallet_address() {
155 let wallet = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string());
156 assert!(wallet.is_ok());
157 assert_eq!(wallet.unwrap().as_str(), VALID_SOLANA_ADDRESS);
158 }
159
160 #[test]
161 fn test_create_short_valid_address() {
162 let wallet = WalletAddress::new(VALID_SHORT_ADDRESS.to_string());
163 assert!(wallet.is_ok());
164 }
165
166 #[test]
167 fn test_reject_empty_address() {
168 let result = WalletAddress::new("".to_string());
169 assert!(result.is_err());
170
171 if let Err(e) = result {
172 assert!(e.to_string().contains("cannot be empty"));
173 }
174 }
175
176 #[test]
177 fn test_reject_too_short_address() {
178 let short_address = "1234567890123456789012345678901";
180 let result = WalletAddress::new(short_address.to_string());
181 assert!(result.is_err());
182
183 if let Err(e) = result {
184 assert!(e.to_string().contains("must be between 32 and 44"));
185 }
186 }
187
188 #[test]
189 fn test_reject_too_long_address() {
190 let long_address = "123456789012345678901234567890123456789012345";
192 let result = WalletAddress::new(long_address.to_string());
193 assert!(result.is_err());
194
195 if let Err(e) = result {
196 assert!(e.to_string().contains("must be between 32 and 44"));
197 }
198 }
199
200 #[test]
201 fn test_reject_invalid_base58_characters() {
202 let invalid = "0WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
204 let result = WalletAddress::new(invalid.to_string());
205 assert!(result.is_err());
206
207 let invalid = "OWzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
209 let result = WalletAddress::new(invalid.to_string());
210 assert!(result.is_err());
211
212 let invalid = "IWzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
214 let result = WalletAddress::new(invalid.to_string());
215 assert!(result.is_err());
216
217 let invalid = "lWzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM";
219 let result = WalletAddress::new(invalid.to_string());
220 assert!(result.is_err());
221
222 if let Err(e) = result {
223 assert!(e.to_string().contains("invalid base58"));
224 }
225 }
226
227 #[test]
228 fn test_anonymized() {
229 let wallet = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
230 let anonymized = wallet.anonymized();
231 assert_eq!(anonymized, "9WzD...AWWM");
232 }
233
234 #[test]
235 fn test_is_solana() {
236 let wallet = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
237 assert!(wallet.is_solana());
238
239 let wallet = WalletAddress::new(VALID_SHORT_ADDRESS.to_string()).unwrap();
240 assert!(wallet.is_solana());
241 }
242
243 #[test]
244 fn test_display_trait() {
245 let wallet = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
246 assert_eq!(format!("{}", wallet), VALID_SOLANA_ADDRESS);
247 }
248
249 #[test]
250 fn test_try_from_str() {
251 let wallet: Result<WalletAddress> = VALID_SOLANA_ADDRESS.try_into();
252 assert!(wallet.is_ok());
253 assert_eq!(wallet.unwrap().as_str(), VALID_SOLANA_ADDRESS);
254
255 let invalid: Result<WalletAddress> = "".try_into();
256 assert!(invalid.is_err());
257 }
258
259 #[test]
260 fn test_try_from_string() {
261 let wallet: Result<WalletAddress> = VALID_SOLANA_ADDRESS.to_string().try_into();
262 assert!(wallet.is_ok());
263
264 let invalid: Result<WalletAddress> = String::new().try_into();
265 assert!(invalid.is_err());
266 }
267
268 #[test]
269 fn test_into_inner() {
270 let wallet = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
271 let inner = wallet.into_inner();
272 assert_eq!(inner, VALID_SOLANA_ADDRESS);
273 }
274
275 #[test]
276 fn test_equality() {
277 let w1 = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
278 let w2 = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
279 let w3 = WalletAddress::new(VALID_SHORT_ADDRESS.to_string()).unwrap();
280
281 assert_eq!(w1, w2);
283 assert_ne!(w1, w3);
284 }
285
286 #[test]
287 fn test_cloning() {
288 let w1 = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
289 let w2 = w1.clone();
290 assert_eq!(w1, w2);
291 }
292
293 #[test]
294 fn test_hash_consistency() {
295 use std::collections::HashSet;
296
297 let w1 = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
298 let w2 = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
299
300 let mut set = HashSet::new();
301 set.insert(w1);
302
303 assert!(set.contains(&w2));
305 }
306
307 #[test]
308 fn test_serde_serialization() {
309 let wallet = WalletAddress::new(VALID_SOLANA_ADDRESS.to_string()).unwrap();
310
311 let json = serde_json::to_string(&wallet).unwrap();
313 assert_eq!(json, format!("\"{}\"", VALID_SOLANA_ADDRESS));
314
315 let deserialized: WalletAddress = serde_json::from_str(&json).unwrap();
317 assert_eq!(deserialized, wallet);
318 }
319
320 #[test]
321 fn test_new_unchecked() {
322 let wallet = WalletAddress::new_unchecked("invalid".to_string());
324 assert_eq!(wallet.as_str(), "invalid");
325 }
326}