use crate::chain_id::ChainId;
use crate::error::Error;
use crate::validation::ValidationRegistry;
use once_cell::sync::Lazy;
use regex::Regex;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::str::FromStr;
static ASSET_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
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}$")
.expect("Failed to compile ASSET_ID_REGEX")
});
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AssetId {
chain_id: ChainId,
namespace: String,
reference: String,
}
impl AssetId {
pub fn new(chain_id: ChainId, namespace: &str, reference: &str) -> Result<Self, Error> {
Self::validate_namespace(namespace)?;
Self::validate_reference(namespace, reference)?;
let asset_id_str = format!("{}/{namespace}:{reference}", chain_id);
if !ASSET_ID_REGEX.is_match(&asset_id_str) {
return Err(Error::InvalidAssetId(asset_id_str));
}
Ok(Self {
chain_id,
namespace: namespace.to_string(),
reference: reference.to_string(),
})
}
pub fn chain_id(&self) -> &ChainId {
&self.chain_id
}
pub fn namespace(&self) -> &str {
&self.namespace
}
pub fn reference(&self) -> &str {
&self.reference
}
fn validate_namespace(namespace: &str) -> Result<(), Error> {
if !Regex::new(r"^[-a-z0-9]{3,8}$")
.expect("Failed to compile namespace regex")
.is_match(namespace)
{
return Err(Error::InvalidAssetNamespace(namespace.to_string()));
}
Ok(())
}
fn validate_reference(namespace: &str, reference: &str) -> Result<(), Error> {
if !Regex::new(r"^[-a-zA-Z0-9]{1,64}$")
.expect("Failed to compile reference regex")
.is_match(reference)
{
return Err(Error::InvalidAssetReference(reference.to_string()));
}
let registry = ValidationRegistry::global();
let registry_guard = registry.lock().unwrap();
if let Some(validator) = registry_guard.get_asset_validator(namespace) {
validator(reference)
.map_err(|err| Error::InvalidAssetReference(format!("{}: {}", reference, err)))?;
}
Ok(())
}
}
impl FromStr for AssetId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !ASSET_ID_REGEX.is_match(s) {
return Err(Error::InvalidAssetId(s.to_string()));
}
let parts: Vec<&str> = s.split('/').collect();
if parts.len() != 2 {
return Err(Error::InvalidAssetId(s.to_string()));
}
let chain_id = ChainId::from_str(parts[0])?;
let asset_parts: Vec<&str> = parts[1].split(':').collect();
if asset_parts.len() != 2 {
return Err(Error::InvalidAssetId(s.to_string()));
}
AssetId::new(chain_id, asset_parts[0], asset_parts[1])
}
}
impl std::fmt::Display for AssetId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}/{}:{}", self.chain_id, self.namespace, self.reference)
}
}
impl Serialize for AssetId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for AssetId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
AssetId::from_str(&s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialization_format() {
let asset_str = "eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
let asset_id = AssetId::from_str(asset_str).unwrap();
let json = serde_json::to_string(&asset_id).unwrap();
assert_eq!(json, format!("\"{}\"", asset_str));
let json_string = format!("\"{}\"", asset_str);
let result = serde_json::from_str::<AssetId>(&json_string);
assert!(result.is_ok());
let assets = vec![
AssetId::from_str("eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
AssetId::from_str("eip155:1/erc20:0x6B175474E89094C44Da98b954EedeAC495271d0F").unwrap(),
];
let json_array = serde_json::to_string(&assets).unwrap();
assert_eq!(
json_array,
r#"["eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48","eip155:1/erc20:0x6B175474E89094C44Da98b954EedeAC495271d0F"]"#
);
let test_vector_json = r#"["eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"]"#;
let result: Result<Vec<AssetId>, _> = serde_json::from_str(test_vector_json);
assert!(result.is_ok());
}
#[test]
fn test_valid_asset_ids() {
let usdc =
AssetId::from_str("eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
assert_eq!(usdc.chain_id().to_string(), "eip155:1");
assert_eq!(usdc.namespace(), "erc20");
assert_eq!(
usdc.reference(),
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
);
assert_eq!(
usdc.to_string(),
"eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
);
let chain_id = ChainId::from_str("eip155:1").unwrap();
let dai = AssetId::new(
chain_id,
"erc20",
"0x6b175474e89094c44da98b954eedeac495271d0f",
)
.unwrap();
assert_eq!(
dai.to_string(),
"eip155:1/erc20:0x6b175474e89094c44da98b954eedeac495271d0f"
);
}
#[test]
fn test_invalid_asset_ids() {
assert!(AssetId::from_str("").is_err());
assert!(AssetId::from_str("eip1551erc20address").is_err());
assert!(
AssetId::from_str("eip155:1erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").is_err()
);
assert!(AssetId::from_str(":1/:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").is_err());
assert!(AssetId::from_str("eip155:1/erc20:").is_err());
assert!(
AssetId::from_str("eip155:1/er:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").is_err()
);
let long_reference = "a".repeat(65);
assert!(AssetId::from_str(&format!("eip155:1/erc20:{}", long_reference)).is_err());
}
#[test]
fn test_serialization() {
let asset_id =
AssetId::from_str("eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
let serialized = serde_json::to_string(&asset_id).unwrap();
assert_eq!(
serialized,
r#""eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48""#
);
let deserialized: AssetId = serde_json::from_str(&serialized).unwrap();
assert_eq!(deserialized, asset_id);
}
#[test]
fn test_display_formatting() {
let asset_id =
AssetId::from_str("eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48").unwrap();
assert_eq!(
format!("{}", asset_id),
"eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
);
assert_eq!(
asset_id.to_string(),
"eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
);
}
}