use candid::CandidType;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use crate::chain_id::ChainId;
use crate::error::{CaipError, Result};
use crate::validation::{ASSET_ID_BASE_REGEX, ASSET_ID_REGEX};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, CandidType)]
pub struct AssetId {
chain_id: ChainId,
asset_namespace: String,
asset_reference: String,
}
impl AssetId {
pub fn new(
chain_id: ChainId,
asset_namespace: impl Into<String>,
asset_reference: impl Into<String>,
) -> Result<Self> {
let asset_id = Self {
chain_id,
asset_namespace: asset_namespace.into(),
asset_reference: asset_reference.into(),
};
asset_id.validate()?;
Ok(asset_id)
}
pub fn chain_id(&self) -> &ChainId {
&self.chain_id
}
pub fn asset_namespace(&self) -> &str {
&self.asset_namespace
}
pub fn asset_reference(&self) -> &str {
&self.asset_reference
}
fn validate(&self) -> Result<()> {
let formatted = self.to_string();
if !ASSET_ID_REGEX.is_match(&formatted) {
return Err(CaipError::InvalidAssetId(formatted));
}
Ok(())
}
}
impl fmt::Display for AssetId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}/{}:{}",
self.chain_id, self.asset_namespace, self.asset_reference
)
}
}
impl FromStr for AssetId {
type Err = CaipError;
fn from_str(s: &str) -> Result<Self> {
let captures = ASSET_ID_REGEX
.captures(s)
.ok_or_else(|| CaipError::InvalidAssetId(s.to_string()))?;
let chain_id = ChainId::new(&captures[1], &captures[2])?;
Ok(AssetId {
chain_id,
asset_namespace: captures[3].to_string(),
asset_reference: captures[4].to_string(),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct AssetIdBase {
asset_namespace: String,
asset_reference: String,
}
impl AssetIdBase {
pub fn new(
asset_namespace: impl Into<String>,
asset_reference: impl Into<String>,
) -> Result<Self> {
let asset_id = Self {
asset_namespace: asset_namespace.into(),
asset_reference: asset_reference.into(),
};
asset_id.validate()?;
Ok(asset_id)
}
pub fn asset_namespace(&self) -> &str {
&self.asset_namespace
}
pub fn asset_reference(&self) -> &str {
&self.asset_reference
}
fn validate(&self) -> Result<()> {
let formatted = self.to_string();
if !ASSET_ID_BASE_REGEX.is_match(&formatted) {
return Err(CaipError::InvalidAssetId(formatted));
}
Ok(())
}
}
impl fmt::Display for AssetIdBase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.asset_namespace, self.asset_reference)
}
}
impl FromStr for AssetIdBase {
type Err = CaipError;
fn from_str(s: &str) -> Result<Self> {
let captures = ASSET_ID_BASE_REGEX
.captures(s)
.ok_or_else(|| CaipError::InvalidAssetId(s.to_string()))?;
Ok(AssetIdBase {
asset_namespace: captures[1].to_string(),
asset_reference: captures[2].to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::CaipError;
use std::str::FromStr;
#[test]
fn test_asset_id_valid() {
let chain_id = ChainId::new("eip155", "1").unwrap();
let erc20_asset = AssetId::new(
chain_id.clone(),
"erc20",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", );
assert!(erc20_asset.is_ok());
let eth_asset = AssetId::new(
chain_id, "slip44", "60", );
assert!(eth_asset.is_ok());
let asset = eth_asset.unwrap();
assert_eq!(asset.asset_namespace(), "slip44");
assert_eq!(asset.asset_reference(), "60");
assert_eq!(asset.chain_id().to_string(), "eip155:1");
assert_eq!(asset.to_string(), "eip155:1/slip44:60");
}
#[test]
fn test_asset_id_invalid() {
let chain_id = ChainId::new("eip155", "1").unwrap();
assert!(AssetId::new(chain_id.clone(), "e", "token").is_err());
assert!(AssetId::new(chain_id.clone(), "toolongnamespace", "token").is_err());
assert!(AssetId::new(chain_id.clone(), "erc!20", "token").is_err());
assert!(AssetId::new(chain_id.clone(), "erc20", "").is_err());
assert!(AssetId::new(chain_id, "erc20", "a".repeat(65)).is_err());
}
#[test]
fn test_asset_id_fromstr() {
assert_eq!(
AssetId::from_str("eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
AssetId::new(
ChainId::new("eip155", "1").unwrap(),
"erc20",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
)
.unwrap()
);
assert!(matches!(
AssetId::from_str("eip155:1/erc20"),
Err(CaipError::InvalidAssetId(_))
));
assert!(matches!(
AssetId::from_str("eip155:1/erc20:"),
Err(CaipError::InvalidAssetId(_))
));
assert!(matches!(
AssetId::from_str("eip155:1erc20:token"),
Err(CaipError::InvalidAssetId(_))
));
}
#[test]
fn test_asset_id_base_valid() {
let erc20_asset = AssetIdBase::new(
"erc20",
"0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", );
assert!(erc20_asset.is_ok());
let slip44_asset = AssetIdBase::new(
"slip44", "60", );
assert!(slip44_asset.is_ok());
let asset = slip44_asset.unwrap();
assert_eq!(asset.asset_namespace(), "slip44");
assert_eq!(asset.asset_reference(), "60");
assert_eq!(asset.to_string(), "slip44:60");
}
#[test]
fn test_asset_id_base_invalid() {
assert!(AssetIdBase::new("e", "token").is_err());
assert!(AssetIdBase::new("toolongnamespace", "token").is_err());
assert!(AssetIdBase::new("erc!20", "token").is_err());
assert!(AssetIdBase::new("erc20", "").is_err());
assert!(AssetIdBase::new("erc20", "a".repeat(65)).is_err());
}
#[test]
fn test_asset_id_base_fromstr() {
assert_eq!(
AssetIdBase::from_str("erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(),
AssetIdBase::new("erc20", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap()
);
assert_eq!(
AssetIdBase::from_str("slip44:60").unwrap(),
AssetIdBase::new("slip44", "60").unwrap()
);
assert!(matches!(
AssetIdBase::from_str("erc20"),
Err(CaipError::InvalidAssetId(_))
));
assert!(matches!(
AssetIdBase::from_str("erc20:"),
Err(CaipError::InvalidAssetId(_))
));
assert!(matches!(
AssetIdBase::from_str(":token"),
Err(CaipError::InvalidAssetId(_))
));
assert!(matches!(
AssetIdBase::from_str("eip155:1/erc20:token"),
Err(CaipError::InvalidAssetId(_))
));
}
}