use super::DidMethod;
use crate::did::{Did, DidDocument};
use crate::{DidError, DidResult, Service, VerificationMethod};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ChainNamespace {
Eip155,
Solana,
Bip122,
Cosmos,
Polkadot,
Other(String),
}
impl ChainNamespace {
pub fn parse(s: &str) -> Self {
match s {
"eip155" => Self::Eip155,
"solana" => Self::Solana,
"bip122" => Self::Bip122,
"cosmos" => Self::Cosmos,
"polkadot" => Self::Polkadot,
other => Self::Other(other.to_string()),
}
}
pub fn as_str(&self) -> &str {
match self {
Self::Eip155 => "eip155",
Self::Solana => "solana",
Self::Bip122 => "bip122",
Self::Cosmos => "cosmos",
Self::Polkadot => "polkadot",
Self::Other(s) => s.as_str(),
}
}
pub fn verification_method_type(&self) -> &'static str {
match self {
Self::Eip155 => "EcdsaSecp256k1RecoveryMethod2020",
Self::Solana => "Ed25519VerificationKey2020",
Self::Bip122 => "EcdsaSecp256k1VerificationKey2019",
Self::Cosmos => "EcdsaSecp256k1VerificationKey2019",
Self::Polkadot => "Sr25519VerificationKey2020",
Self::Other(_) => "BlockchainVerificationMethod2021",
}
}
pub fn validate_address(&self, address: &str) -> DidResult<()> {
match self {
Self::Eip155 => validate_eip155_address(address),
Self::Solana => validate_solana_address(address),
Self::Bip122 => validate_bitcoin_address(address),
Self::Cosmos | Self::Polkadot | Self::Other(_) => {
if address.is_empty() {
return Err(DidError::InvalidFormat(
"Account address cannot be empty".to_string(),
));
}
Ok(())
}
}
}
}
fn validate_eip155_address(address: &str) -> DidResult<()> {
if !address.starts_with("0x") && !address.starts_with("0X") {
return Err(DidError::InvalidFormat(
"Ethereum address must start with '0x'".to_string(),
));
}
let hex_part = &address[2..];
if hex_part.len() != 40 {
return Err(DidError::InvalidFormat(format!(
"Ethereum address hex part must be 40 characters, got {}",
hex_part.len()
)));
}
if !hex_part.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(DidError::InvalidFormat(
"Ethereum address must contain only hex characters".to_string(),
));
}
Ok(())
}
fn validate_solana_address(address: &str) -> DidResult<()> {
if address.is_empty() {
return Err(DidError::InvalidFormat(
"Solana address cannot be empty".to_string(),
));
}
let decoded = bs58::decode(address)
.into_vec()
.map_err(|e| DidError::InvalidFormat(format!("Invalid base58 address: {}", e)))?;
if decoded.len() != 32 {
return Err(DidError::InvalidFormat(format!(
"Solana public key must be 32 bytes, got {}",
decoded.len()
)));
}
Ok(())
}
fn validate_bitcoin_address(address: &str) -> DidResult<()> {
if address.is_empty() {
return Err(DidError::InvalidFormat(
"Bitcoin address cannot be empty".to_string(),
));
}
let valid_prefix = address.starts_with('1')
|| address.starts_with('3')
|| address.starts_with("bc1")
|| address.starts_with("tb1");
if !valid_prefix {
return Err(DidError::InvalidFormat(
"Bitcoin address must start with '1', '3', 'bc1', or 'tb1'".to_string(),
));
}
if address.len() < 25 || address.len() > 62 {
return Err(DidError::InvalidFormat(format!(
"Bitcoin address length {} is out of valid range [25, 62]",
address.len()
)));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DidPkh {
pub chain_namespace: ChainNamespace,
pub chain_id: String,
pub account_address: String,
}
impl DidPkh {
pub fn new(chain_namespace: &str, chain_id: &str, account_address: &str) -> DidResult<Self> {
if chain_namespace.is_empty() {
return Err(DidError::InvalidFormat(
"Chain namespace cannot be empty".to_string(),
));
}
if chain_id.is_empty() {
return Err(DidError::InvalidFormat(
"Chain ID cannot be empty".to_string(),
));
}
if account_address.is_empty() {
return Err(DidError::InvalidFormat(
"Account address cannot be empty".to_string(),
));
}
let namespace = ChainNamespace::parse(chain_namespace);
namespace.validate_address(account_address)?;
Ok(Self {
chain_namespace: namespace,
chain_id: chain_id.to_string(),
account_address: account_address.to_string(),
})
}
pub fn ethereum(chain_id: u64, address: &str) -> DidResult<Self> {
Self::new("eip155", &chain_id.to_string(), address)
}
pub fn solana(address: &str) -> DidResult<Self> {
Self::new("solana", "4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ", address)
}
pub fn to_did_string(&self) -> String {
format!(
"did:pkh:{}:{}:{}",
self.chain_namespace.as_str(),
self.chain_id,
self.account_address
)
}
pub fn from_did_string(did: &str) -> DidResult<Self> {
if !did.starts_with("did:pkh:") {
return Err(DidError::InvalidFormat(
"DID must start with 'did:pkh:'".to_string(),
));
}
let rest = &did["did:pkh:".len()..];
let parts: Vec<&str> = rest.splitn(3, ':').collect();
if parts.len() != 3 {
return Err(DidError::InvalidFormat(format!(
"did:pkh must have format did:pkh:<namespace>:<chain_id>:<address>, got: {}",
did
)));
}
Self::new(parts[0], parts[1], parts[2])
}
pub fn to_did(&self) -> DidResult<Did> {
Did::new(&self.to_did_string())
}
pub fn resolve(&self) -> DidResult<DidDocument> {
self.generate_document()
}
fn generate_document(&self) -> DidResult<DidDocument> {
let did_str = self.to_did_string();
let did = Did::new(&did_str)?;
let key_id = format!("{}#blockchainAccountId", did_str);
let vm_type = self.chain_namespace.verification_method_type();
let blockchain_account_id = format!(
"{}:{}:{}",
self.chain_namespace.as_str(),
self.chain_id,
self.account_address
);
let verification_method =
VerificationMethod::blockchain(&key_id, &did_str, vm_type, &blockchain_account_id);
let mut doc = DidDocument::new(did);
doc.context = vec![
"https://www.w3.org/ns/did/v1".to_string(),
"https://w3id.org/security/suites/secp256k1recovery-2020/v2".to_string(),
"https://w3id.org/security/v3-unstable".to_string(),
];
doc.verification_method.push(verification_method);
use crate::did::document::VerificationRelationship;
doc.authentication
.push(VerificationRelationship::Reference(key_id.clone()));
doc.assertion_method
.push(VerificationRelationship::Reference(key_id.clone()));
doc.capability_invocation
.push(VerificationRelationship::Reference(key_id.clone()));
doc.capability_delegation
.push(VerificationRelationship::Reference(key_id));
Ok(doc)
}
pub fn caip10_account_id(&self) -> String {
format!(
"{}:{}:{}",
self.chain_namespace.as_str(),
self.chain_id,
self.account_address
)
}
}
pub struct DidPkhMethod;
impl Default for DidPkhMethod {
fn default() -> Self {
Self::new()
}
}
impl DidPkhMethod {
pub fn new() -> Self {
Self
}
}
#[async_trait]
impl DidMethod for DidPkhMethod {
fn method_name(&self) -> &str {
"pkh"
}
async fn resolve(&self, did: &Did) -> DidResult<DidDocument> {
if !self.supports(did) {
return Err(DidError::UnsupportedMethod(did.method().to_string()));
}
let pkh = DidPkh::from_did_string(did.as_str())?;
pkh.resolve()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_ethereum_did_pkh() {
let pkh = DidPkh::ethereum(1, "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb").unwrap();
assert_eq!(pkh.chain_namespace, ChainNamespace::Eip155);
assert_eq!(pkh.chain_id, "1");
assert_eq!(
pkh.account_address,
"0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb"
);
}
#[test]
fn test_did_pkh_to_string() {
let pkh = DidPkh::ethereum(1, "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb").unwrap();
assert_eq!(
pkh.to_did_string(),
"did:pkh:eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb"
);
}
#[test]
fn test_did_pkh_from_string() {
let did_str = "did:pkh:eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb";
let pkh = DidPkh::from_did_string(did_str).unwrap();
assert_eq!(pkh.chain_namespace, ChainNamespace::Eip155);
assert_eq!(pkh.chain_id, "1");
assert_eq!(
pkh.account_address,
"0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb"
);
assert_eq!(pkh.to_did_string(), did_str);
}
#[test]
fn test_did_pkh_roundtrip() {
let original = "did:pkh:eip155:137:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb";
let pkh = DidPkh::from_did_string(original).unwrap();
assert_eq!(pkh.to_did_string(), original);
}
#[test]
fn test_invalid_ethereum_address() {
assert!(DidPkh::ethereum(1, "0xabc").is_err());
assert!(DidPkh::ethereum(1, "ab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb").is_err());
assert!(DidPkh::ethereum(1, "0xab16a96d").is_err());
}
#[test]
fn test_invalid_did_pkh_format() {
assert!(DidPkh::from_did_string("did:key:z6Mk").is_err());
assert!(DidPkh::from_did_string("did:pkh:eip155:only-two-parts").is_err());
assert!(DidPkh::from_did_string("not-a-did").is_err());
}
#[test]
fn test_resolve_ethereum_did_pkh() {
let pkh = DidPkh::ethereum(1, "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb").unwrap();
let doc = pkh.resolve().unwrap();
assert_eq!(doc.verification_method.len(), 1);
assert!(!doc.authentication.is_empty());
assert!(!doc.assertion_method.is_empty());
}
#[test]
fn test_caip10_account_id() {
let pkh = DidPkh::ethereum(1, "0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb").unwrap();
assert_eq!(
pkh.caip10_account_id(),
"eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb"
);
}
#[tokio::test]
async fn test_did_pkh_method_resolver() {
let method = DidPkhMethod::new();
assert_eq!(method.method_name(), "pkh");
let did = Did::new("did:pkh:eip155:1:0xab16a96d359ec26a11e2c2b3d8f8b8942d5bfcdb").unwrap();
let doc = method.resolve(&did).await.unwrap();
assert!(!doc.verification_method.is_empty());
}
#[tokio::test]
async fn test_did_pkh_wrong_method() {
let method = DidPkhMethod::new();
let did = Did::new("did:key:z6Mk123").unwrap();
assert!(method.resolve(&did).await.is_err());
}
#[test]
fn test_bitcoin_did_pkh() {
let pkh = DidPkh::new(
"bip122",
"000000000019d6689c085ae165831e93",
"1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf",
)
.unwrap();
assert_eq!(pkh.chain_namespace, ChainNamespace::Bip122);
let did_str = pkh.to_did_string();
assert!(did_str.starts_with("did:pkh:bip122:"));
}
#[test]
fn test_chain_namespace_verification_method_type() {
assert_eq!(
ChainNamespace::Eip155.verification_method_type(),
"EcdsaSecp256k1RecoveryMethod2020"
);
assert_eq!(
ChainNamespace::Solana.verification_method_type(),
"Ed25519VerificationKey2020"
);
}
}