1use crate::chain_id::ChainId;
2use crate::error::Error;
3use crate::validation::ValidationRegistry;
4use once_cell::sync::Lazy;
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use std::str::FromStr;
8
9static ACCOUNT_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
11 Regex::new(r"^[-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,32}:[-a-zA-Z0-9]{1,64}$")
12 .expect("Failed to compile ACCOUNT_ID_REGEX")
13});
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub struct AccountId {
26 chain_id: ChainId,
27 address: String,
28}
29
30impl AccountId {
31 pub fn new(chain_id: ChainId, address: &str) -> Result<Self, Error> {
42 Self::validate_address(&chain_id, address)?;
44
45 Ok(Self {
46 chain_id,
47 address: address.to_string(),
48 })
49 }
50
51 pub fn chain_id(&self) -> &ChainId {
53 &self.chain_id
54 }
55
56 pub fn address(&self) -> &str {
58 &self.address
59 }
60
61 fn validate_address(chain_id: &ChainId, address: &str) -> Result<(), Error> {
63 if !Regex::new(r"^[-a-zA-Z0-9]{1,64}$")
65 .expect("Failed to compile address regex")
66 .is_match(address)
67 {
68 return Err(Error::InvalidAddressFormat(
69 chain_id.to_string(),
70 address.to_string(),
71 ));
72 }
73
74 let registry = ValidationRegistry::global();
76 let registry_guard = registry.lock().unwrap();
77
78 if let Some(validator) = registry_guard.get_account_validator(chain_id.namespace()) {
80 validator(address).map_err(|err| {
81 Error::InvalidAddressFormat(chain_id.to_string(), err.to_string())
82 })?;
83 }
84
85 Ok(())
86 }
87}
88
89impl FromStr for AccountId {
90 type Err = Error;
91
92 fn from_str(s: &str) -> Result<Self, Self::Err> {
102 if !ACCOUNT_ID_REGEX.is_match(s) {
104 return Err(Error::InvalidAccountId(s.to_string()));
105 }
106
107 let parts: Vec<&str> = s.split(':').collect();
109 if parts.len() != 3 {
110 return Err(Error::InvalidAccountId(s.to_string()));
111 }
112
113 let chain_id_str = format!("{}:{}", parts[0], parts[1]);
115 let chain_id = ChainId::from_str(&chain_id_str)?;
116
117 AccountId::new(chain_id, parts[2])
119 }
120}
121
122impl std::fmt::Display for AccountId {
126 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127 write!(f, "{}:{}", self.chain_id, self.address)
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn test_valid_account_ids() {
137 let eth_account =
139 AccountId::from_str("eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db").unwrap();
140 assert_eq!(eth_account.chain_id().to_string(), "eip155:1");
141 assert_eq!(
142 eth_account.address(),
143 "0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
144 );
145 assert_eq!(
146 eth_account.to_string(),
147 "eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
148 );
149
150 let chain_id = ChainId::from_str("eip155:1").unwrap();
152 let account =
153 AccountId::new(chain_id, "0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db").unwrap();
154 assert_eq!(
155 account.to_string(),
156 "eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
157 );
158 }
159
160 #[test]
161 fn test_invalid_account_ids() {
162 assert!(AccountId::from_str("").is_err());
164
165 assert!(AccountId::from_str("eip1551address").is_err());
167
168 assert!(AccountId::from_str(":1:address").is_err());
170
171 assert!(AccountId::from_str("eip155::address").is_err());
173
174 assert!(AccountId::from_str("eip155:1:").is_err());
176
177 let long_address = "a".repeat(65);
179 assert!(AccountId::from_str(&format!("eip155:1:{}", long_address)).is_err());
180 }
181
182 #[test]
183 fn test_serialization() {
184 let account_id =
185 AccountId::from_str("eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db").unwrap();
186 let serialized = serde_json::to_string(&account_id).unwrap();
187
188 assert!(serialized.contains("chain_id"));
190 assert!(serialized.contains("address"));
191
192 let deserialized: AccountId = serde_json::from_str(&serialized).unwrap();
193 assert_eq!(deserialized, account_id);
194 }
195
196 #[test]
197 fn test_display_formatting() {
198 let account_id =
199 AccountId::from_str("eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db").unwrap();
200 assert_eq!(
201 format!("{}", account_id),
202 "eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
203 );
204 assert_eq!(
205 account_id.to_string(),
206 "eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
207 );
208 }
209}