use crate::chain_id::ChainId;
use crate::error::Error;
use crate::validation::ValidationRegistry;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
static ACCOUNT_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,32}:[-a-zA-Z0-9]{1,64}$")
.expect("Failed to compile ACCOUNT_ID_REGEX")
});
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AccountId {
chain_id: ChainId,
address: String,
}
impl AccountId {
pub fn new(chain_id: ChainId, address: &str) -> Result<Self, Error> {
Self::validate_address(&chain_id, address)?;
Ok(Self {
chain_id,
address: address.to_string(),
})
}
pub fn chain_id(&self) -> &ChainId {
&self.chain_id
}
pub fn address(&self) -> &str {
&self.address
}
fn validate_address(chain_id: &ChainId, address: &str) -> Result<(), Error> {
if !Regex::new(r"^[-a-zA-Z0-9]{1,64}$")
.expect("Failed to compile address regex")
.is_match(address)
{
return Err(Error::InvalidAddressFormat(
chain_id.to_string(),
address.to_string(),
));
}
let registry = ValidationRegistry::global();
let registry_guard = registry.lock().unwrap();
if let Some(validator) = registry_guard.get_account_validator(chain_id.namespace()) {
validator(address).map_err(|err| {
Error::InvalidAddressFormat(chain_id.to_string(), err.to_string())
})?;
}
Ok(())
}
}
impl FromStr for AccountId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !ACCOUNT_ID_REGEX.is_match(s) {
return Err(Error::InvalidAccountId(s.to_string()));
}
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 3 {
return Err(Error::InvalidAccountId(s.to_string()));
}
let chain_id_str = format!("{}:{}", parts[0], parts[1]);
let chain_id = ChainId::from_str(&chain_id_str)?;
AccountId::new(chain_id, parts[2])
}
}
impl std::fmt::Display for AccountId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}:{}", self.chain_id, self.address)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_account_ids() {
let eth_account =
AccountId::from_str("eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db").unwrap();
assert_eq!(eth_account.chain_id().to_string(), "eip155:1");
assert_eq!(
eth_account.address(),
"0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
);
assert_eq!(
eth_account.to_string(),
"eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
);
let chain_id = ChainId::from_str("eip155:1").unwrap();
let account =
AccountId::new(chain_id, "0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db").unwrap();
assert_eq!(
account.to_string(),
"eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
);
}
#[test]
fn test_invalid_account_ids() {
assert!(AccountId::from_str("").is_err());
assert!(AccountId::from_str("eip1551address").is_err());
assert!(AccountId::from_str(":1:address").is_err());
assert!(AccountId::from_str("eip155::address").is_err());
assert!(AccountId::from_str("eip155:1:").is_err());
let long_address = "a".repeat(65);
assert!(AccountId::from_str(&format!("eip155:1:{}", long_address)).is_err());
}
#[test]
fn test_serialization() {
let account_id =
AccountId::from_str("eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db").unwrap();
let serialized = serde_json::to_string(&account_id).unwrap();
assert!(serialized.contains("chain_id"));
assert!(serialized.contains("address"));
let deserialized: AccountId = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, account_id);
}
#[test]
fn test_display_formatting() {
let account_id =
AccountId::from_str("eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db").unwrap();
assert_eq!(
format!("{}", account_id),
"eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
);
assert_eq!(
account_id.to_string(),
"eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db"
);
}
}