1use crate::chain_id::ChainId;
2use crate::error::Error;
3use crate::validation::ValidationRegistry;
4use once_cell::sync::Lazy;
5use regex::Regex;
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::str::FromStr;
8
9static ASSET_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
11 Regex::new(r"^[-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,64}/[-a-z0-9]{3,8}:[-a-zA-Z0-9]{1,64}$")
12 .expect("Failed to compile ASSET_ID_REGEX")
13});
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
26pub struct AssetId {
27 chain_id: ChainId,
28 namespace: String,
29 reference: String,
30}
31
32impl AssetId {
33 pub fn new(chain_id: ChainId, namespace: &str, reference: &str) -> Result<Self, Error> {
45 Self::validate_namespace(namespace)?;
47
48 Self::validate_reference(namespace, reference)?;
50
51 let asset_id_str = format!("{}/{namespace}:{reference}", chain_id);
53 if !ASSET_ID_REGEX.is_match(&asset_id_str) {
54 return Err(Error::InvalidAssetId(asset_id_str));
55 }
56
57 Ok(Self {
58 chain_id,
59 namespace: namespace.to_string(),
60 reference: reference.to_string(),
61 })
62 }
63
64 pub fn chain_id(&self) -> &ChainId {
66 &self.chain_id
67 }
68
69 pub fn namespace(&self) -> &str {
71 &self.namespace
72 }
73
74 pub fn reference(&self) -> &str {
76 &self.reference
77 }
78
79 fn validate_namespace(namespace: &str) -> Result<(), Error> {
81 if !Regex::new(r"^[-a-z0-9]{3,8}$")
83 .expect("Failed to compile namespace regex")
84 .is_match(namespace)
85 {
86 return Err(Error::InvalidAssetNamespace(namespace.to_string()));
87 }
88
89 Ok(())
90 }
91
92 fn validate_reference(namespace: &str, reference: &str) -> Result<(), Error> {
94 if !Regex::new(r"^[-a-zA-Z0-9]{1,64}$")
96 .expect("Failed to compile reference regex")
97 .is_match(reference)
98 {
99 return Err(Error::InvalidAssetReference(reference.to_string()));
100 }
101
102 let registry = ValidationRegistry::global();
104 let registry_guard = registry.lock().unwrap();
105
106 if let Some(validator) = registry_guard.get_asset_validator(namespace) {
108 validator(reference)
109 .map_err(|err| Error::InvalidAssetReference(format!("{}: {}", reference, err)))?;
110 }
111
112 Ok(())
113 }
114}
115
116impl FromStr for AssetId {
117 type Err = Error;
118
119 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 if !ASSET_ID_REGEX.is_match(s) {
131 return Err(Error::InvalidAssetId(s.to_string()));
132 }
133
134 let parts: Vec<&str> = s.split('/').collect();
136 if parts.len() != 2 {
137 return Err(Error::InvalidAssetId(s.to_string()));
138 }
139
140 let chain_id = ChainId::from_str(parts[0])?;
142
143 let asset_parts: Vec<&str> = parts[1].split(':').collect();
145 if asset_parts.len() != 2 {
146 return Err(Error::InvalidAssetId(s.to_string()));
147 }
148
149 AssetId::new(chain_id, asset_parts[0], asset_parts[1])
151 }
152}
153
154impl std::fmt::Display for AssetId {
158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159 write!(f, "{}/{}:{}", self.chain_id, self.namespace, self.reference)
160 }
161}
162
163impl Serialize for AssetId {
164 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
165 where
166 S: Serializer,
167 {
168 serializer.serialize_str(&self.to_string())
169 }
170}
171
172impl<'de> Deserialize<'de> for AssetId {
173 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174 where
175 D: Deserializer<'de>,
176 {
177 let s = String::deserialize(deserializer)?;
178 AssetId::from_str(&s).map_err(serde::de::Error::custom)
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn test_serialization_format() {
188 let asset_str = "eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
189 let asset_id = AssetId::from_str(asset_str).unwrap();
190
191 let json = serde_json::to_string(&asset_id).unwrap();
193 assert_eq!(json, format!("\"{}\"", asset_str));
194
195 let json_string = format!("\"{}\"", asset_str);
197 let result = serde_json::from_str::<AssetId>(&json_string);
198 assert!(result.is_ok());
199
200 let assets = vec![
202 AssetId::from_str("eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
203 AssetId::from_str("eip155:1/erc20:0x6B175474E89094C44Da98b954EedeAC495271d0F").unwrap(),
204 ];
205 let json_array = serde_json::to_string(&assets).unwrap();
206 assert_eq!(
207 json_array,
208 r#"["eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","eip155:1/erc20:0x6B175474E89094C44Da98b954EedeAC495271d0F"]"#
209 );
210
211 let test_vector_json = r#"["eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"]"#;
213 let result: Result<Vec<AssetId>, _> = serde_json::from_str(test_vector_json);
214 assert!(result.is_ok());
215 }
216
217 #[test]
218 fn test_valid_asset_ids() {
219 let usdc =
221 AssetId::from_str("eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
222 assert_eq!(usdc.chain_id().to_string(), "eip155:1");
223 assert_eq!(usdc.namespace(), "erc20");
224 assert_eq!(
225 usdc.reference(),
226 "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
227 );
228 assert_eq!(
229 usdc.to_string(),
230 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
231 );
232
233 let chain_id = ChainId::from_str("eip155:1").unwrap();
235 let dai = AssetId::new(
236 chain_id,
237 "erc20",
238 "0x6b175474e89094c44da98b954eedeac495271d0f",
239 )
240 .unwrap();
241 assert_eq!(
242 dai.to_string(),
243 "eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f"
244 );
245 }
246
247 #[test]
248 fn test_invalid_asset_ids() {
249 assert!(AssetId::from_str("").is_err());
251
252 assert!(AssetId::from_str("eip1551erc20address").is_err());
254
255 assert!(
257 AssetId::from_str("eip155:1erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").is_err()
258 );
259
260 assert!(AssetId::from_str(":1/:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").is_err());
262
263 assert!(AssetId::from_str("eip155:1/erc20:").is_err());
265
266 assert!(
268 AssetId::from_str("eip155:1/er:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").is_err()
269 );
270
271 let long_reference = "a".repeat(65);
273 assert!(AssetId::from_str(&format!("eip155:1/erc20:{}", long_reference)).is_err());
274 }
275
276 #[test]
277 fn test_serialization() {
278 let asset_id =
279 AssetId::from_str("eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
280 let serialized = serde_json::to_string(&asset_id).unwrap();
281
282 assert_eq!(
284 serialized,
285 r#""eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48""#
286 );
287
288 let deserialized: AssetId = serde_json::from_str(&serialized).unwrap();
289 assert_eq!(deserialized, asset_id);
290 }
291
292 #[test]
293 fn test_display_formatting() {
294 let asset_id =
295 AssetId::from_str("eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
296 assert_eq!(
297 format!("{}", asset_id),
298 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
299 );
300 assert_eq!(
301 asset_id.to_string(),
302 "eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
303 );
304 }
305}