use std::collections::HashMap;
use std::sync::Arc;
use super::contacts_manager::ContactsManager;
use super::types::{
default_identity, Contact, DisplayableIdentity, IdentityClientOptions, KnownIdentityTypes,
};
use crate::services::ServicesError;
use crate::wallet::interfaces::{
CertificateType, DiscoverByAttributesArgs, DiscoverByIdentityKeyArgs, IdentityCertificate,
ProveCertificateArgs,
};
use crate::wallet::types::BooleanDefaultFalse;
use crate::wallet::WalletInterface;
pub struct IdentityClient<W: WalletInterface + ?Sized> {
wallet: Arc<W>,
contacts_manager: ContactsManager<W>,
#[allow(dead_code)]
options: IdentityClientOptions,
originator: Option<String>,
}
impl<W: WalletInterface + ?Sized> IdentityClient<W> {
pub fn new(
wallet: Arc<W>,
options: Option<IdentityClientOptions>,
originator: Option<String>,
) -> Self {
let contacts_manager = ContactsManager::new(wallet.clone(), originator.clone());
IdentityClient {
wallet,
contacts_manager,
options: options.unwrap_or_default(),
originator,
}
}
pub fn contacts_manager(&self) -> &ContactsManager<W> {
&self.contacts_manager
}
pub async fn resolve_by_identity_key(
&self,
args: DiscoverByIdentityKeyArgs,
override_with_contacts: bool,
) -> Result<Vec<DisplayableIdentity>, ServicesError> {
let identity_key_hex = args.identity_key.to_der_hex();
if override_with_contacts {
let contacts = self
.contacts_manager
.get_contacts(Some(&identity_key_hex), false, 1000)
.await
.unwrap_or_default();
if !contacts.is_empty() {
return Ok(contacts.iter().map(|c| c.to_displayable()).collect());
}
}
let result = self
.wallet
.discover_by_identity_key(args, self.originator.as_deref())
.await
.map_err(|e| {
ServicesError::Identity(format!("discover_by_identity_key failed: {}", e))
})?;
let identities: Vec<DisplayableIdentity> = result
.certificates
.iter()
.map(|cert| Self::parse_identity(cert))
.collect();
Ok(identities)
}
pub async fn resolve_by_attributes(
&self,
args: DiscoverByAttributesArgs,
override_with_contacts: bool,
) -> Result<Vec<DisplayableIdentity>, ServicesError> {
let contact_map: HashMap<String, Contact> = if override_with_contacts {
let contacts = self
.contacts_manager
.get_contacts(None, false, 1000)
.await
.unwrap_or_default();
contacts
.into_iter()
.map(|c| (c.identity_key.clone(), c))
.collect()
} else {
HashMap::new()
};
let result = self
.wallet
.discover_by_attributes(args, self.originator.as_deref())
.await
.map_err(|e| {
ServicesError::Identity(format!("discover_by_attributes failed: {}", e))
})?;
let identities: Vec<DisplayableIdentity> = result
.certificates
.iter()
.map(|cert| {
let subject_key = cert.certificate.subject.to_der_hex();
if let Some(contact) = contact_map.get(&subject_key) {
contact.to_displayable()
} else {
Self::parse_identity(cert)
}
})
.collect();
Ok(identities)
}
pub async fn publicly_reveal_attributes(
&self,
certificate: &crate::wallet::interfaces::Certificate,
fields_to_reveal: &[String],
) -> Result<(), ServicesError> {
if fields_to_reveal.is_empty() {
return Err(ServicesError::Identity(
"Public reveal failed: You must reveal at least one field!".to_string(),
));
}
if let Some(fields) = &certificate.fields {
if fields.is_empty() {
return Err(ServicesError::Identity(
"Public reveal failed: Certificate has no fields to reveal!".to_string(),
));
}
} else {
return Err(ServicesError::Identity(
"Public reveal failed: Certificate has no fields to reveal!".to_string(),
));
}
let anyone_public = crate::wallet::anyone_pubkey();
let _prove_result = self
.wallet
.prove_certificate(
ProveCertificateArgs {
certificate: certificate.clone().into(),
fields_to_reveal: fields_to_reveal.to_vec(),
verifier: anyone_public,
privileged: BooleanDefaultFalse(None),
privileged_reason: None,
},
self.originator.as_deref(),
)
.await
.map_err(|e| ServicesError::Identity(format!("Failed to prove certificate: {}", e)))?;
Ok(())
}
pub fn parse_identity(cert: &IdentityCertificate) -> DisplayableIdentity {
let cert_type = cert_type_to_base64(&cert.certificate.cert_type);
let fields = &cert.decrypted_fields;
let certifier = &cert.certifier_info;
let (name, avatar_url, badge_label, badge_icon_url, badge_click_url) =
match cert_type.as_str() {
t if t == KnownIdentityTypes::X_CERT => (
fields.get("userName").cloned().unwrap_or_default(),
fields.get("profilePhoto").cloned().unwrap_or_default(),
format!("X account certified by {}", certifier.name),
certifier.icon_url.clone(),
"https://socialcert.net".to_string(),
),
t if t == KnownIdentityTypes::DISCORD_CERT => (
fields.get("userName").cloned().unwrap_or_default(),
fields.get("profilePhoto").cloned().unwrap_or_default(),
format!("Discord account certified by {}", certifier.name),
certifier.icon_url.clone(),
"https://socialcert.net".to_string(),
),
t if t == KnownIdentityTypes::EMAIL_CERT => (
fields.get("email").cloned().unwrap_or_default(),
"XUTZxep7BBghAJbSBwTjNfmcsDdRFs5EaGEgkESGSgjJVYgMEizu".to_string(),
format!("Email certified by {}", certifier.name),
certifier.icon_url.clone(),
"https://socialcert.net".to_string(),
),
t if t == KnownIdentityTypes::PHONE_CERT => (
fields.get("phoneNumber").cloned().unwrap_or_default(),
"XUTLxtX3ELNUwRhLwL7kWNGbdnFM8WG2eSLv84J7654oH8HaJWrU".to_string(),
format!("Phone certified by {}", certifier.name),
certifier.icon_url.clone(),
"https://socialcert.net".to_string(),
),
t if t == KnownIdentityTypes::IDENTI_CERT => {
let first = fields.get("firstName").cloned().unwrap_or_default();
let last = fields.get("lastName").cloned().unwrap_or_default();
(
format!("{} {}", first, last),
fields.get("profilePhoto").cloned().unwrap_or_default(),
format!("Government ID certified by {}", certifier.name),
certifier.icon_url.clone(),
"https://identicert.me".to_string(),
)
}
t if t == KnownIdentityTypes::REGISTRANT => (
fields.get("name").cloned().unwrap_or_default(),
fields.get("icon").cloned().unwrap_or_default(),
format!("Entity certified by {}", certifier.name),
certifier.icon_url.clone(),
"https://projectbabbage.com/docs/registrant".to_string(),
),
t if t == KnownIdentityTypes::COOL_CERT => {
let is_cool = fields.get("cool").map(|v| v == "true").unwrap_or(false);
let name = if is_cool {
"Cool Person!".to_string()
} else {
"Not cool!".to_string()
};
let di = default_identity();
(
name,
di.avatar_url,
di.badge_label,
di.badge_icon_url,
di.badge_click_url,
)
}
t if t == KnownIdentityTypes::ANYONE => (
"Anyone".to_string(),
"XUT4bpQ6cpBaXi1oMzZsXfpkWGbtp2JTUYAoN7PzhStFJ6wLfoeR".to_string(),
"Represents the ability for anyone to access this information.".to_string(),
"XUUV39HVPkpmMzYNTx7rpKzJvXfeiVyQWg2vfSpjBAuhunTCA9uG".to_string(),
"https://projectbabbage.com/docs/anyone-identity".to_string(),
),
t if t == KnownIdentityTypes::SELF_ => (
"You".to_string(),
"XUT9jHGk2qace148jeCX5rDsMftkSGYKmigLwU2PLLBc7Hm63VYR".to_string(),
"Represents your ability to access this information.".to_string(),
"XUUV39HVPkpmMzYNTx7rpKzJvXfeiVyQWg2vfSpjBAuhunTCA9uG".to_string(),
"https://projectbabbage.com/docs/self-identity".to_string(),
),
_ => Self::try_parse_generic_identity(&cert_type, fields, certifier),
};
let subject_str = cert.certificate.subject.to_der_hex();
let abbreviated_key = if subject_str.len() > 10 {
format!("{}...", &subject_str[..10])
} else if !subject_str.is_empty() {
subject_str.clone()
} else {
String::new()
};
DisplayableIdentity {
name,
avatar_url,
abbreviated_key,
identity_key: subject_str,
badge_icon_url,
badge_label,
badge_click_url,
}
}
fn try_parse_generic_identity(
cert_type: &str,
fields: &HashMap<String, String>,
certifier: &crate::wallet::interfaces::IdentityCertifier,
) -> (String, String, String, String, String) {
let di = default_identity();
let first_name = fields.get("firstName").filter(|v| !v.is_empty());
let last_name = fields.get("lastName").filter(|v| !v.is_empty());
let full_name = match (first_name, last_name) {
(Some(f), Some(l)) => Some(format!("{} {}", f, l)),
(Some(f), None) => Some(f.clone()),
(None, Some(l)) => Some(l.clone()),
(None, None) => None,
};
let name = fields
.get("name")
.filter(|v| !v.is_empty())
.cloned()
.or_else(|| fields.get("userName").filter(|v| !v.is_empty()).cloned())
.or(full_name)
.or_else(|| fields.get("email").filter(|v| !v.is_empty()).cloned())
.unwrap_or_else(|| di.name.clone());
let avatar_url = fields
.get("profilePhoto")
.filter(|v| !v.is_empty())
.or_else(|| fields.get("avatar").filter(|v| !v.is_empty()))
.or_else(|| fields.get("icon").filter(|v| !v.is_empty()))
.or_else(|| fields.get("photo").filter(|v| !v.is_empty()))
.cloned()
.unwrap_or(di.avatar_url);
let badge_label = if !certifier.name.is_empty() {
format!("{} certified by {}", cert_type, certifier.name)
} else {
di.badge_label
};
let badge_icon_url = if !certifier.icon_url.is_empty() {
certifier.icon_url.clone()
} else {
di.badge_icon_url
};
let badge_click_url = di.badge_click_url;
(
name,
avatar_url,
badge_label,
badge_icon_url,
badge_click_url,
)
}
pub async fn get_contacts(
&self,
identity_key: Option<&str>,
force_refresh: bool,
limit: usize,
) -> Result<Vec<Contact>, ServicesError> {
self.contacts_manager
.get_contacts(identity_key, force_refresh, limit)
.await
}
pub async fn save_contact(
&self,
contact: &DisplayableIdentity,
metadata: Option<HashMap<String, serde_json::Value>>,
) -> Result<(), ServicesError> {
let c = Contact::from_displayable(contact, metadata);
self.contacts_manager.add_contact(&c).await
}
pub async fn remove_contact(&self, identity_key: &str) -> Result<(), ServicesError> {
self.contacts_manager.remove_contact(identity_key).await
}
}
fn cert_type_to_base64(ct: &CertificateType) -> String {
base64_encode(ct.bytes())
}
fn base64_encode(data: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::new();
let chunks = data.chunks(3);
for chunk in chunks {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let triple = (b0 << 16) | (b1 << 8) | b2;
result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(CHARS[(triple & 0x3F) as usize] as char);
} else {
result.push('=');
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::wallet::interfaces::IdentityCertifier;
#[test]
fn test_parse_identity_anyone() {
let cert = IdentityCertificate {
certificate: crate::wallet::interfaces::Certificate {
cert_type: crate::wallet::interfaces::CertificateType([0u8; 32]),
serial_number: crate::wallet::interfaces::SerialNumber([0u8; 32]),
subject: crate::primitives::public_key::PublicKey::from_private_key(
&crate::primitives::private_key::PrivateKey::from_random().unwrap(),
),
certifier: crate::primitives::public_key::PublicKey::from_private_key(
&crate::primitives::private_key::PrivateKey::from_random().unwrap(),
),
revocation_outpoint: None,
fields: None,
signature: None,
},
certifier_info: IdentityCertifier {
name: "TestCertifier".to_string(),
icon_url: "https://example.com/icon.png".to_string(),
description: "Test".to_string(),
trust: 1,
},
publicly_revealed_keyring: HashMap::new(),
decrypted_fields: HashMap::new(),
};
let identity = IdentityClient::<crate::wallet::ProtoWallet>::parse_identity(&cert);
assert!(!identity.abbreviated_key.is_empty());
}
#[test]
fn test_parse_identity_generic_with_name() {
let mut fields = HashMap::new();
fields.insert("name".to_string(), "Alice".to_string());
fields.insert(
"profilePhoto".to_string(),
"https://example.com/alice.png".to_string(),
);
let cert = IdentityCertificate {
certificate: crate::wallet::interfaces::Certificate {
cert_type: crate::wallet::interfaces::CertificateType([0u8; 32]),
serial_number: crate::wallet::interfaces::SerialNumber([0u8; 32]),
subject: crate::primitives::public_key::PublicKey::from_private_key(
&crate::primitives::private_key::PrivateKey::from_random().unwrap(),
),
certifier: crate::primitives::public_key::PublicKey::from_private_key(
&crate::primitives::private_key::PrivateKey::from_random().unwrap(),
),
revocation_outpoint: None,
fields: None,
signature: None,
},
certifier_info: IdentityCertifier {
name: "TestCertifier".to_string(),
icon_url: "https://example.com/icon.png".to_string(),
description: "Test".to_string(),
trust: 1,
},
publicly_revealed_keyring: HashMap::new(),
decrypted_fields: fields,
};
let identity = IdentityClient::<crate::wallet::ProtoWallet>::parse_identity(&cert);
assert_eq!(identity.name, "Alice");
assert_eq!(identity.avatar_url, "https://example.com/alice.png");
assert!(identity.badge_label.contains("TestCertifier"));
}
#[test]
fn test_parse_identity_generic_first_last_name() {
let mut fields = HashMap::new();
fields.insert("firstName".to_string(), "Bob".to_string());
fields.insert("lastName".to_string(), "Smith".to_string());
let cert = IdentityCertificate {
certificate: crate::wallet::interfaces::Certificate {
cert_type: crate::wallet::interfaces::CertificateType([0u8; 32]),
serial_number: crate::wallet::interfaces::SerialNumber([0u8; 32]),
subject: crate::primitives::public_key::PublicKey::from_private_key(
&crate::primitives::private_key::PrivateKey::from_random().unwrap(),
),
certifier: crate::primitives::public_key::PublicKey::from_private_key(
&crate::primitives::private_key::PrivateKey::from_random().unwrap(),
),
revocation_outpoint: None,
fields: None,
signature: None,
},
certifier_info: IdentityCertifier {
name: "".to_string(),
icon_url: "".to_string(),
description: "".to_string(),
trust: 0,
},
publicly_revealed_keyring: HashMap::new(),
decrypted_fields: fields,
};
let identity = IdentityClient::<crate::wallet::ProtoWallet>::parse_identity(&cert);
assert_eq!(identity.name, "Bob Smith");
assert_eq!(identity.badge_label, "Not verified by anyone you trust.");
}
}