use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct CredentialBundle {
pub did: String,
#[serde(rename = "privateKeyMultibase")]
pub private_key_multibase: String,
#[serde(rename = "vtaDid")]
pub vta_did: String,
#[serde(rename = "vtaUrl", default, skip_serializing_if = "Option::is_none")]
pub vta_url: Option<String>,
}
impl CredentialBundle {
pub fn new(
did: impl Into<String>,
private_key_multibase: impl Into<String>,
vta_did: impl Into<String>,
) -> Self {
Self {
did: did.into(),
private_key_multibase: private_key_multibase.into(),
vta_did: vta_did.into(),
vta_url: None,
}
}
pub fn vta_url(mut self, url: impl Into<String>) -> Self {
self.vta_url = Some(url.into());
self
}
#[cfg(feature = "sealed-transfer")]
pub fn from_ed25519_seed_multibase(
private_key_multibase: &str,
vta_did: &str,
vta_url: Option<&str>,
) -> Result<(Self, String), crate::did_key::DidKeyError> {
let seed = crate::did_key::decode_private_key_multibase(private_key_multibase)?;
let public_key = ed25519_dalek::SigningKey::from_bytes(&seed)
.verifying_key()
.to_bytes();
let admin_did = format!(
"did:key:{}",
crate::did_key::ed25519_multibase_pubkey(&public_key)
);
let bundle = Self {
did: admin_did.clone(),
private_key_multibase: private_key_multibase.to_string(),
vta_did: vta_did.to_string(),
vta_url: vta_url.map(String::from),
};
Ok((bundle, admin_did))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_credential_bundle_full() {
let json = r#"{
"did": "did:key:z6Mk123",
"privateKeyMultibase": "z1234567890",
"vtaDid": "did:key:z6MkVTA",
"vtaUrl": "https://vta.example.com"
}"#;
let bundle: CredentialBundle = serde_json::from_str(json).unwrap();
assert_eq!(bundle.did, "did:key:z6Mk123");
assert_eq!(bundle.private_key_multibase, "z1234567890");
assert_eq!(bundle.vta_did, "did:key:z6MkVTA");
assert_eq!(bundle.vta_url.as_deref(), Some("https://vta.example.com"));
}
#[test]
fn test_credential_bundle_without_url() {
let json = r#"{
"did": "did:key:z6Mk123",
"privateKeyMultibase": "z1234567890",
"vtaDid": "did:key:z6MkVTA"
}"#;
let bundle: CredentialBundle = serde_json::from_str(json).unwrap();
assert!(bundle.vta_url.is_none());
}
#[test]
fn test_credential_bundle_missing_did_fails() {
let json = r#"{
"privateKeyMultibase": "z1234567890",
"vtaDid": "did:key:z6MkVTA"
}"#;
assert!(serde_json::from_str::<CredentialBundle>(json).is_err());
}
#[test]
fn test_serde_json_roundtrip() {
let bundle = CredentialBundle {
did: "did:key:z6Mk123".to_string(),
private_key_multibase: "z1234567890".to_string(),
vta_did: "did:key:z6MkVTA".to_string(),
vta_url: Some("https://vta.example.com".to_string()),
};
let json = serde_json::to_string(&bundle).unwrap();
let decoded: CredentialBundle = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.did, bundle.did);
assert_eq!(decoded.private_key_multibase, bundle.private_key_multibase);
assert_eq!(decoded.vta_did, bundle.vta_did);
assert_eq!(decoded.vta_url, bundle.vta_url);
}
#[test]
fn test_serde_json_roundtrip_without_url() {
let bundle = CredentialBundle {
did: "did:key:z6Mk123".to_string(),
private_key_multibase: "z1234567890".to_string(),
vta_did: "did:key:z6MkVTA".to_string(),
vta_url: None,
};
let json = serde_json::to_string(&bundle).unwrap();
assert!(!json.contains("vtaUrl"));
let decoded: CredentialBundle = serde_json::from_str(&json).unwrap();
assert!(decoded.vta_url.is_none());
}
}