use regex::Regex;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use crate::error::DIDError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct DID {
pub method: String,
pub id: String,
pub path: Option<String>,
pub query: Option<String>,
pub fragment: Option<String>,
}
impl DID {
pub fn new(method: impl Into<String>, id: impl Into<String>) -> Self {
Self {
method: method.into(),
id: id.into(),
path: None,
query: None,
fragment: None,
}
}
pub fn hanzo(username: impl Into<String>) -> Self {
Self::new("hanzo", username.into())
}
pub fn lux(username: impl Into<String>) -> Self {
Self::new("lux", username.into())
}
pub fn pars(username: impl Into<String>) -> Self {
Self::new("pars", username.into())
}
pub fn zoo(username: impl Into<String>) -> Self {
Self::new("zoo", username.into())
}
pub fn ai(username: impl Into<String>) -> Self {
Self::new("ai", username.into())
}
pub fn sparkle(username: impl Into<String>) -> Self {
Self::new("sparkle", username.into())
}
pub fn spc(username: impl Into<String>) -> Self {
Self::new("spc", username.into())
}
pub fn sparklepony(username: impl Into<String>) -> Self {
Self::new("sparklepony", username.into())
}
pub fn hanzo_local(identifier: impl Into<String>) -> Self {
Self::new("hanzo", format!("local:{}", identifier.into()))
}
pub fn lux_local(identifier: impl Into<String>) -> Self {
Self::new("lux", format!("local:{}", identifier.into()))
}
pub fn pars_local(identifier: impl Into<String>) -> Self {
Self::new("pars", format!("local:{}", identifier.into()))
}
pub fn zoo_local(identifier: impl Into<String>) -> Self {
Self::new("zoo", format!("local:{}", identifier.into()))
}
pub fn sparkle_local(identifier: impl Into<String>) -> Self {
Self::new("sparkle", format!("local:{}", identifier.into()))
}
pub fn hanzo_eth(address: impl Into<String>) -> Self {
Self::new("hanzo", format!("eth:{}", address.into()))
}
pub fn hanzo_sepolia(address: impl Into<String>) -> Self {
Self::new("hanzo", format!("sepolia:{}", address.into()))
}
pub fn hanzo_base(address: impl Into<String>) -> Self {
Self::new("hanzo", format!("base:{}", address.into()))
}
pub fn lux_chain(chain: &str, address: impl Into<String>) -> Self {
Self::new("lux", format!("{}:{}", chain, address.into()))
}
pub fn eth(identifier: impl Into<String>) -> Self {
Self::new("eth", identifier.into())
}
pub fn sepolia(identifier: impl Into<String>) -> Self {
Self::new("sepolia", identifier.into())
}
pub fn base(identifier: impl Into<String>) -> Self {
Self::new("base", identifier.into())
}
pub fn base_sepolia(identifier: impl Into<String>) -> Self {
Self::new("base-sepolia", identifier.into())
}
pub fn polygon(identifier: impl Into<String>) -> Self {
Self::new("polygon", identifier.into())
}
pub fn arbitrum(identifier: impl Into<String>) -> Self {
Self::new("arbitrum", identifier.into())
}
pub fn optimism(identifier: impl Into<String>) -> Self {
Self::new("optimism", identifier.into())
}
pub fn parse(did_string: &str) -> Result<Self, DIDError> {
let regex = Regex::new(r"^did:([a-z0-9]+):([a-zA-Z0-9:._-]+)(/[^?#]*)?(\?[^#]*)?(#.*)?$")
.map_err(|e| DIDError::InvalidFormat(format!("Regex error: {e}")))?;
let captures = regex
.captures(did_string)
.ok_or_else(|| DIDError::InvalidFormat(format!("Invalid DID format: {did_string}")))?;
let method = captures
.get(1)
.ok_or_else(|| DIDError::InvalidFormat("Missing method".to_string()))?
.as_str()
.to_string();
let id = captures
.get(2)
.ok_or_else(|| DIDError::InvalidFormat("Missing method-specific-id".to_string()))?
.as_str()
.to_string();
let path = captures.get(3).map(|m| m.as_str().to_string());
let query = captures.get(4).map(|m| {
m.as_str()
.strip_prefix('?')
.unwrap_or(m.as_str())
.to_string()
});
let fragment = captures.get(5).map(|m| {
m.as_str()
.strip_prefix('#')
.unwrap_or(m.as_str())
.to_string()
});
Ok(Self {
method,
id,
path,
query,
fragment,
})
}
pub fn with_fragment(mut self, fragment: impl Into<String>) -> Self {
self.fragment = Some(fragment.into());
self
}
pub fn with_path(mut self, path: impl Into<String>) -> Self {
self.path = Some(path.into());
self
}
pub fn with_query(mut self, query: impl Into<String>) -> Self {
self.query = Some(query.into());
self
}
pub fn to_string_full(&self) -> String {
let mut result = format!("did:{}:{}", self.method, self.id);
if let Some(path) = &self.path {
result.push_str(path);
}
if let Some(query) = &self.query {
result.push('?');
result.push_str(query);
}
if let Some(fragment) = &self.fragment {
result.push('#');
result.push_str(fragment);
}
result
}
pub fn get_network(&self) -> Option<Network> {
if self.id.contains(':') {
let parts: Vec<&str> = self.id.split(':').collect();
if !parts.is_empty() {
if let Ok(network) = Network::from_str(parts[0]) {
return Some(network);
}
}
}
if let Ok(network) = Network::from_str(&self.method) {
return Some(network);
}
match self.method.as_str() {
"hanzo" | "ai" => Some(Network::Hanzo), "lux" => Some(Network::Lux),
"pars" => Some(Network::Pars),
"zoo" => Some(Network::Zoo),
"sparkle" | "spc" | "sparklepony" => Some(Network::SparklePony), _ => None,
}
}
pub fn get_identifier(&self) -> String {
if self.id.find(':').is_none() {
return self.id.clone();
}
let parts: Vec<&str> = self.id.split(':').collect();
if parts.len() >= 2 {
parts[1..].join(":")
} else {
self.id.clone()
}
}
pub fn is_same_entity(&self, other: &DID) -> bool {
if self == other {
return true;
}
self.get_identifier() == other.get_identifier()
}
pub fn get_omnichain_variants(&self) -> Vec<DID> {
let identifier = self.get_identifier();
vec![
DID::hanzo(identifier.clone()), DID::lux(identifier.clone()), DID::pars(identifier.clone()), DID::zoo(identifier.clone()), DID::ai(identifier.clone()), DID::sparkle(identifier.clone()), DID::spc(identifier.clone()), DID::sparklepony(identifier.clone()), DID::eth(identifier.clone()), DID::base(identifier.clone()), DID::polygon(identifier.clone()), DID::arbitrum(identifier.clone()), DID::optimism(identifier.clone()), DID::hanzo_local(identifier.clone()), DID::lux_local(identifier.clone()), DID::pars_local(identifier.clone()), DID::zoo_local(identifier.clone()), DID::sparkle_local(identifier.clone()), DID::sepolia(identifier.clone()), DID::base_sepolia(identifier.clone()), DID::hanzo_eth(identifier.clone()), DID::hanzo_sepolia(identifier.clone()), DID::hanzo_base(identifier.clone()), DID::lux_chain("fuji", identifier), ]
}
pub fn from_username(username: &str, context: &str) -> DID {
let clean_username = username.strip_prefix('@').unwrap_or(username);
match context.to_lowercase().as_str() {
"hanzo" => DID::hanzo(clean_username),
"lux" => DID::lux(clean_username),
"pars" => DID::pars(clean_username),
"sparkle" | "spc" | "sparklepony" => DID::sparkle(clean_username), "zoo" => DID::zoo(clean_username),
"ai" => DID::ai(clean_username),
_ => DID::hanzo(clean_username), }
}
}
impl fmt::Display for DID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_string_full())
}
}
impl FromStr for DID {
type Err = DIDError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let did_regex =
Regex::new(r"^did:([a-z0-9]+):([^?#]+)(/[^?#]*)?(\?[^#]*)?(#.*)?$").unwrap();
let captures = did_regex
.captures(s)
.ok_or_else(|| DIDError::InvalidFormat(s.to_string()))?;
let method = captures.get(1).unwrap().as_str().to_string();
let id = captures.get(2).unwrap().as_str().to_string();
let path = captures.get(3).map(|m| m.as_str().to_string());
let query = captures.get(4).map(|m| m.as_str()[1..].to_string()); let fragment = captures.get(5).map(|m| m.as_str()[1..].to_string());
Ok(DID {
method,
id,
path,
query,
fragment,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Network {
Hanzo,
Lux,
Pars,
SparklePony,
Zoo,
Local,
Ethereum,
Sepolia,
Base,
BaseSepolia,
Polygon,
Arbitrum,
Optimism,
LuxFuji,
IPFS,
}
impl Network {
pub fn chain_id(&self) -> Option<u64> {
match self {
Network::Hanzo => Some(36963), Network::Lux => Some(96369), Network::Pars => Some(494949), Network::SparklePony => Some(36911), Network::Zoo => Some(200200), Network::Ethereum => Some(1),
Network::Sepolia => Some(11155111),
Network::Base => Some(8453),
Network::BaseSepolia => Some(84532),
Network::Polygon => Some(137),
Network::Arbitrum => Some(42161),
Network::Optimism => Some(10),
Network::LuxFuji => Some(43113), _ => None,
}
}
pub fn is_testnet(&self) -> bool {
matches!(
self,
Network::Sepolia | Network::BaseSepolia | Network::LuxFuji | Network::Local
)
}
pub fn rpc_endpoint(&self) -> Option<&'static str> {
match self {
Network::Hanzo => Some("https://rpc.hanzo.ai"),
Network::Lux => Some("https://api.lux.network/ext/bc/C/rpc"),
Network::Pars => Some("https://rpc.pars.network"),
Network::SparklePony => Some("https://rpc.sparklepony.xyz"),
Network::Zoo => Some("https://rpc.zoo.network"),
Network::Ethereum => Some("https://eth.llamarpc.com"),
Network::Sepolia => Some("https://rpc.sepolia.org"),
Network::Base => Some("https://mainnet.base.org"),
Network::BaseSepolia => Some("https://sepolia.base.org"),
Network::Polygon => Some("https://polygon-rpc.com"),
Network::Arbitrum => Some("https://arb1.arbitrum.io/rpc"),
Network::Optimism => Some("https://mainnet.optimism.io"),
Network::LuxFuji => Some("https://api.lux-test.network/ext/bc/C/rpc"),
_ => None,
}
}
}
impl fmt::Display for Network {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Network::Hanzo => write!(f, "hanzo"),
Network::Lux => write!(f, "lux"),
Network::Pars => write!(f, "pars"),
Network::SparklePony => write!(f, "sparkle"),
Network::Zoo => write!(f, "zoo"),
Network::Local => write!(f, "local"),
Network::Ethereum => write!(f, "eth"),
Network::Sepolia => write!(f, "sepolia"),
Network::Base => write!(f, "base"),
Network::BaseSepolia => write!(f, "base-sepolia"),
Network::Polygon => write!(f, "polygon"),
Network::Arbitrum => write!(f, "arbitrum"),
Network::Optimism => write!(f, "optimism"),
Network::LuxFuji => write!(f, "lux-fuji"),
Network::IPFS => write!(f, "ipfs"),
}
}
}
impl FromStr for Network {
type Err = DIDError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"hanzo" | "ai" => Ok(Network::Hanzo), "lux" => Ok(Network::Lux),
"pars" => Ok(Network::Pars),
"sparkle" | "spc" | "sparklepony" => Ok(Network::SparklePony), "zoo" => Ok(Network::Zoo),
"local" | "localhost" => Ok(Network::Local),
"eth" | "ethereum" => Ok(Network::Ethereum),
"sepolia" => Ok(Network::Sepolia),
"base" => Ok(Network::Base),
"base-sepolia" => Ok(Network::BaseSepolia),
"polygon" | "matic" => Ok(Network::Polygon),
"arbitrum" | "arb" => Ok(Network::Arbitrum),
"optimism" | "op" => Ok(Network::Optimism),
"lux-fuji" | "fuji" => Ok(Network::LuxFuji),
"ipfs" => Ok(Network::IPFS),
_ => Err(DIDError::UnknownChain(s.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_did_parsing() {
let did_str = "did:hanzo:eth:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7";
let did = DID::from_str(did_str).unwrap();
assert_eq!(did.method, "hanzo");
assert_eq!(did.id, "eth:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7");
assert_eq!(did.get_network(), Some(Network::Ethereum));
assert_eq!(
did.get_identifier(),
"0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7"
);
let simple_did = DID::from_str("did:hanzo:zeekay").unwrap();
assert_eq!(simple_did.method, "hanzo");
assert_eq!(simple_did.id, "zeekay");
assert_eq!(simple_did.get_network(), Some(Network::Hanzo));
assert_eq!(simple_did.get_identifier(), "zeekay");
}
#[test]
fn test_did_with_fragment() {
let did_str = "did:hanzo:eth:0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb7#key-1";
let did = DID::from_str(did_str).unwrap();
assert_eq!(did.fragment, Some("key-1".to_string()));
}
#[test]
fn test_omnichain_identity() {
let hanzo_did = DID::hanzo("zeekay");
let lux_did = DID::lux("zeekay");
let hanzo_eth_did = DID::hanzo_eth("zeekay");
assert!(hanzo_did.is_same_entity(&lux_did));
assert!(hanzo_did.is_same_entity(&hanzo_eth_did));
assert!(lux_did.is_same_entity(&hanzo_eth_did));
let other_did = DID::hanzo("alice");
assert!(!hanzo_did.is_same_entity(&other_did));
}
#[test]
fn test_context_aware_resolution() {
let hanzo_context = DID::from_username("@zeekay", "hanzo");
let lux_context = DID::from_username("zeekay", "lux");
assert_eq!(hanzo_context, DID::hanzo("zeekay"));
assert_eq!(lux_context, DID::lux("zeekay"));
let variants = hanzo_context.get_omnichain_variants();
assert!(variants.contains(&DID::hanzo("zeekay")));
assert!(variants.contains(&DID::lux("zeekay")));
assert!(variants.contains(&DID::hanzo_local("zeekay")));
}
#[test]
fn test_native_chain_dids() {
let eth_did = DID::eth("zeekay");
let base_did = DID::base("zeekay");
let polygon_did = DID::polygon("zeekay");
assert_eq!(eth_did.to_string(), "did:eth:zeekay");
assert_eq!(base_did.to_string(), "did:base:zeekay");
assert_eq!(polygon_did.to_string(), "did:polygon:zeekay");
assert_eq!(eth_did.get_network(), Some(Network::Ethereum));
assert_eq!(base_did.get_network(), Some(Network::Base));
assert_eq!(polygon_did.get_network(), Some(Network::Polygon));
assert!(eth_did.is_same_entity(&base_did));
assert!(eth_did.is_same_entity(&polygon_did));
}
#[test]
fn test_network_properties() {
assert_eq!(Network::Ethereum.chain_id(), Some(1));
assert_eq!(Network::Base.chain_id(), Some(8453));
assert_eq!(Network::Polygon.chain_id(), Some(137));
assert!(!Network::Ethereum.is_testnet());
assert!(Network::Sepolia.is_testnet());
assert!(Network::Hanzo.rpc_endpoint().is_some());
assert!(Network::Lux.rpc_endpoint().is_some());
assert!(Network::Ethereum.rpc_endpoint().is_some());
}
#[test]
fn test_pars_zoo_ai_dids() {
let pars_did = DID::pars("cyrus");
assert_eq!(pars_did.to_string(), "did:pars:cyrus");
assert_eq!(pars_did.get_network(), Some(Network::Pars));
assert_eq!(Network::Pars.chain_id(), Some(494949));
let zoo_did = DID::zoo("alice");
assert_eq!(zoo_did.to_string(), "did:zoo:alice");
assert_eq!(zoo_did.get_network(), Some(Network::Zoo));
assert_eq!(Network::Zoo.chain_id(), Some(200200));
let ai_did = DID::ai("bob");
assert_eq!(ai_did.to_string(), "did:ai:bob");
assert_eq!(ai_did.get_network(), Some(Network::Hanzo)); assert_eq!(Network::Hanzo.chain_id(), Some(36963));
let hanzo_did = DID::hanzo("bob");
assert!(ai_did.is_same_entity(&hanzo_did));
}
#[test]
fn test_sparkle_pony_dids() {
let sparkle_did = DID::sparkle("alice");
let spc_did = DID::spc("alice");
let sparklepony_did = DID::sparklepony("alice");
assert_eq!(sparkle_did.to_string(), "did:sparkle:alice");
assert_eq!(spc_did.to_string(), "did:spc:alice");
assert_eq!(sparklepony_did.to_string(), "did:sparklepony:alice");
assert_eq!(sparkle_did.get_network(), Some(Network::SparklePony));
assert_eq!(spc_did.get_network(), Some(Network::SparklePony));
assert_eq!(sparklepony_did.get_network(), Some(Network::SparklePony));
assert_eq!(Network::SparklePony.chain_id(), Some(36911));
assert_eq!(Network::SparklePony.rpc_endpoint(), Some("https://rpc.sparklepony.xyz"));
assert!(sparkle_did.is_same_entity(&spc_did));
assert!(sparkle_did.is_same_entity(&sparklepony_did));
let pars_did = DID::pars("alice");
assert_eq!(pars_did.get_network(), Some(Network::Pars));
assert_eq!(Network::Pars.chain_id(), Some(494949));
assert!(pars_did.is_same_entity(&sparkle_did)); }
#[test]
fn test_omnichain_variants_includes_new_networks() {
let did = DID::hanzo("zeekay");
let variants = did.get_omnichain_variants();
assert!(variants.contains(&DID::pars("zeekay")));
assert!(variants.contains(&DID::zoo("zeekay")));
assert!(variants.contains(&DID::ai("zeekay")));
assert!(variants.contains(&DID::pars_local("zeekay")));
assert!(variants.contains(&DID::zoo_local("zeekay")));
assert!(variants.contains(&DID::sparkle("zeekay")));
assert!(variants.contains(&DID::spc("zeekay")));
assert!(variants.contains(&DID::sparklepony("zeekay")));
assert!(variants.contains(&DID::sparkle_local("zeekay")));
}
#[test]
fn test_network_rpc_endpoints() {
assert_eq!(Network::Pars.rpc_endpoint(), Some("https://rpc.pars.network"));
assert_eq!(Network::SparklePony.rpc_endpoint(), Some("https://rpc.sparklepony.xyz"));
assert_eq!(Network::Zoo.rpc_endpoint(), Some("https://rpc.zoo.network"));
assert_eq!(Network::Hanzo.rpc_endpoint(), Some("https://rpc.hanzo.ai"));
assert_eq!(Network::Lux.rpc_endpoint(), Some("https://api.lux.network/ext/bc/C/rpc"));
}
#[test]
fn test_from_username_context() {
let pars_did = DID::from_username("@alice", "pars");
assert_eq!(pars_did.to_string(), "did:pars:alice");
assert_eq!(pars_did.get_network(), Some(Network::Pars));
let spc_did = DID::from_username("alice", "spc");
assert_eq!(spc_did.to_string(), "did:sparkle:alice");
assert_eq!(spc_did.get_network(), Some(Network::SparklePony));
let sparkle_did = DID::from_username("alice", "sparkle");
assert_eq!(sparkle_did.to_string(), "did:sparkle:alice");
let sparklepony_did = DID::from_username("alice", "sparklepony");
assert_eq!(sparklepony_did.to_string(), "did:sparkle:alice");
assert_eq!(spc_did, sparkle_did);
assert_eq!(spc_did, sparklepony_did);
}
}