tap_caip/
account_id.rs

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
9/// Regular expression pattern for CAIP-10 account ID validation
10static 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/// CAIP-10 Account ID implementation
16///
17/// An Account ID is a string that identifies a blockchain account and follows the format:
18/// `<chainId>:<accountAddress>`
19///
20/// - `chainId`: CAIP-2 Chain ID (e.g., "eip155:1" for Ethereum mainnet)
21/// - `accountAddress`: Chain-specific account address (e.g., "0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db")
22///
23/// Example: "eip155:1:0x4b20993Bc481177ec7E8f571ceCaE8A9e22C02db" for an Ethereum mainnet account
24#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
25pub struct AccountId {
26    chain_id: ChainId,
27    address: String,
28}
29
30impl AccountId {
31    /// Create a new AccountId from a ChainId and address
32    ///
33    /// # Arguments
34    ///
35    /// * `chain_id` - The CAIP-2 Chain ID
36    /// * `address` - The account address on the specified chain
37    ///
38    /// # Returns
39    ///
40    /// * `Result<AccountId, Error>` - An AccountId or an error if validation fails
41    pub fn new(chain_id: ChainId, address: &str) -> Result<Self, Error> {
42        // Validate the address format according to the chain
43        Self::validate_address(&chain_id, address)?;
44
45        Ok(Self {
46            chain_id,
47            address: address.to_string(),
48        })
49    }
50
51    /// Get the chain ID component
52    pub fn chain_id(&self) -> &ChainId {
53        &self.chain_id
54    }
55
56    /// Get the address component
57    pub fn address(&self) -> &str {
58        &self.address
59    }
60
61    /// Validate that the address is valid for the given chain
62    fn validate_address(chain_id: &ChainId, address: &str) -> Result<(), Error> {
63        // Validate basic address format (1-64 characters, alphanumeric with possible hyphens)
64        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        // Get the global validation registry
75        let registry = ValidationRegistry::global();
76        let registry_guard = registry.lock().unwrap();
77
78        // Apply chain-specific validation rules
79        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    /// Parse a string into an AccountId
93    ///
94    /// # Arguments
95    ///
96    /// * `s` - A string in the format "namespace:reference:address"
97    ///
98    /// # Returns
99    ///
100    /// * `Result<AccountId, Error>` - An AccountId or an error if parsing fails
101    fn from_str(s: &str) -> Result<Self, Self::Err> {
102        // Check the overall format first
103        if !ACCOUNT_ID_REGEX.is_match(s) {
104            return Err(Error::InvalidAccountId(s.to_string()));
105        }
106
107        // Split the account ID into its components
108        let parts: Vec<&str> = s.split(':').collect();
109        if parts.len() != 3 {
110            return Err(Error::InvalidAccountId(s.to_string()));
111        }
112
113        // Parse the chain ID (namespace:reference)
114        let chain_id_str = format!("{}:{}", parts[0], parts[1]);
115        let chain_id = ChainId::from_str(&chain_id_str)?;
116
117        // Validate and create the account ID
118        AccountId::new(chain_id, parts[2])
119    }
120}
121
122// Removed the conflicting ToString implementation
123// Let the default implementation from Display be used
124
125impl 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        // Test Ethereum account
138        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        // Test direct creation
151        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        // Invalid: empty string
163        assert!(AccountId::from_str("").is_err());
164
165        // Invalid: missing separators
166        assert!(AccountId::from_str("eip1551address").is_err());
167
168        // Invalid: empty namespace
169        assert!(AccountId::from_str(":1:address").is_err());
170
171        // Invalid: empty reference
172        assert!(AccountId::from_str("eip155::address").is_err());
173
174        // Invalid: empty address
175        assert!(AccountId::from_str("eip155:1:").is_err());
176
177        // Invalid: address too long
178        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        // The JSON representation should include the chain_id and address fields
189        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}