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 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 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" => Some(Network::Hanzo),
"lux" => Some(Network::Lux),
_ => 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::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::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),
_ => 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,
Local,
Ethereum,
Sepolia,
Base,
BaseSepolia,
Polygon,
Arbitrum,
Optimism,
LuxFuji,
IPFS,
}
impl Network {
pub fn chain_id(&self) -> Option<u64> {
match self {
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.network"),
Network::Lux => Some("https://api.lux.network/ext/bc/C/rpc"),
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::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" => Ok(Network::Hanzo),
"lux" => Ok(Network::Lux),
"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());
}
}