use crate::auth::VerifiableCertificate;
use crate::overlay::NetworkPreset;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub type CertificateFieldNameUnder50Bytes = String;
pub type OriginatorDomainNameStringUnder250Bytes = String;
pub type PubKeyHex = String;
pub type Base64String = String;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KnownCertificateType {
IdentiCert,
DiscordCert,
PhoneCert,
XCert,
Registrant,
EmailCert,
Anyone,
SelfCert,
CoolCert,
}
impl KnownCertificateType {
pub fn type_id(&self) -> &'static str {
match self {
Self::IdentiCert => "z40BOInXkI8m7f/wBrv4MJ09bZfzZbTj2fJqCtONqCY=",
Self::DiscordCert => "2TgqRC35B1zehGmB21xveZNc7i5iqHc0uxMb+1NMPW4=",
Self::PhoneCert => "mffUklUzxbHr65xLohn0hRL0Tq2GjW1GYF/OPfzqJ6A=",
Self::XCert => "vdDWvftf1H+5+ZprUw123kjHlywH+v20aPQTuXgMpNc=",
Self::Registrant => "YoPsbfR6YQczjzPdHCoGC7nJsOdPQR50+SYqcWpJ0y0=",
Self::EmailCert => "exOl3KM0dIJ04EW5pZgbZmPag6MdJXd3/a1enmUU/BA=",
Self::Anyone => "mfkOMfLDQmrr3SBxBQ5WeE+6Hy3VJRFq6w4A5Ljtlis=",
Self::SelfCert => "Hkge6X5JRxt1cWXtHLCrSTg6dCVTxjQJJ48iOYd7n3g=",
Self::CoolCert => "AGfk/WrT1eBDXpz3mcw386Zww2HmqcIn3uY6x4Af1eo=",
}
}
pub fn name(&self) -> &'static str {
match self {
Self::IdentiCert => "IdentiCert",
Self::DiscordCert => "DiscordCert",
Self::PhoneCert => "PhoneCert",
Self::XCert => "XCert",
Self::Registrant => "Registrant",
Self::EmailCert => "EmailCert",
Self::Anyone => "Anyone",
Self::SelfCert => "Self",
Self::CoolCert => "CoolCert",
}
}
pub fn from_type_id(type_id: &str) -> Option<Self> {
match type_id {
"z40BOInXkI8m7f/wBrv4MJ09bZfzZbTj2fJqCtONqCY=" => Some(Self::IdentiCert),
"2TgqRC35B1zehGmB21xveZNc7i5iqHc0uxMb+1NMPW4=" => Some(Self::DiscordCert),
"mffUklUzxbHr65xLohn0hRL0Tq2GjW1GYF/OPfzqJ6A=" => Some(Self::PhoneCert),
"vdDWvftf1H+5+ZprUw123kjHlywH+v20aPQTuXgMpNc=" => Some(Self::XCert),
"YoPsbfR6YQczjzPdHCoGC7nJsOdPQR50+SYqcWpJ0y0=" => Some(Self::Registrant),
"exOl3KM0dIJ04EW5pZgbZmPag6MdJXd3/a1enmUU/BA=" => Some(Self::EmailCert),
"mfkOMfLDQmrr3SBxBQ5WeE+6Hy3VJRFq6w4A5Ljtlis=" => Some(Self::Anyone),
"Hkge6X5JRxt1cWXtHLCrSTg6dCVTxjQJJ48iOYd7n3g=" => Some(Self::SelfCert),
"AGfk/WrT1eBDXpz3mcw386Zww2HmqcIn3uY6x4Af1eo=" => Some(Self::CoolCert),
_ => None,
}
}
pub fn all() -> &'static [Self] {
&[
Self::IdentiCert,
Self::DiscordCert,
Self::PhoneCert,
Self::XCert,
Self::Registrant,
Self::EmailCert,
Self::Anyone,
Self::SelfCert,
Self::CoolCert,
]
}
}
pub struct DefaultIdentityValues;
impl DefaultIdentityValues {
pub const NAME: &'static str = "Unknown Identity";
pub const AVATAR_URL: &'static str = "XUUB8bbn9fEthk15Ge3zTQXypUShfC94vFjp65v7u5CQ8qkpxzst";
pub const BADGE_ICON_URL: &'static str = "XUUV39HVPkpmMzYNTx7rpKzJvXfeiVyQWg2vfSpjBAuhunTCA9uG";
pub const BADGE_LABEL: &'static str = "Not verified by anyone you trust.";
pub const BADGE_CLICK_URL: &'static str = "https://projectbabbage.com/docs/unknown-identity";
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct DisplayableIdentity {
pub name: String,
pub avatar_url: String,
pub identity_key: String,
pub abbreviated_key: String,
pub badge_icon_url: String,
pub badge_label: String,
pub badge_click_url: String,
}
impl DisplayableIdentity {
pub fn from_key(identity_key: &str) -> Self {
let abbreviated = if identity_key.len() > 10 {
format!(
"{}...{}",
&identity_key[..6],
&identity_key[identity_key.len() - 4..]
)
} else {
identity_key.to_string()
};
Self {
name: abbreviated.clone(),
avatar_url: DefaultIdentityValues::AVATAR_URL.to_string(),
identity_key: identity_key.to_string(),
abbreviated_key: abbreviated,
badge_icon_url: DefaultIdentityValues::BADGE_ICON_URL.to_string(),
badge_label: DefaultIdentityValues::BADGE_LABEL.to_string(),
badge_click_url: DefaultIdentityValues::BADGE_CLICK_URL.to_string(),
}
}
pub fn unknown() -> Self {
Self {
name: DefaultIdentityValues::NAME.to_string(),
avatar_url: DefaultIdentityValues::AVATAR_URL.to_string(),
identity_key: String::new(),
abbreviated_key: String::new(),
badge_icon_url: DefaultIdentityValues::BADGE_ICON_URL.to_string(),
badge_label: DefaultIdentityValues::BADGE_LABEL.to_string(),
badge_click_url: DefaultIdentityValues::BADGE_CLICK_URL.to_string(),
}
}
}
impl Default for DisplayableIdentity {
fn default() -> Self {
Self::unknown()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Contact {
pub identity_key: String,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub avatar_url: Option<String>,
#[serde(default)]
pub added_at: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
impl Contact {
pub fn from_identity(identity: DisplayableIdentity) -> Self {
Self {
identity_key: identity.identity_key,
name: identity.name,
avatar_url: Some(identity.avatar_url),
added_at: crate::auth::current_time_ms(),
notes: None,
tags: Vec::new(),
metadata: None,
}
}
pub fn to_displayable_identity(&self) -> DisplayableIdentity {
let abbreviated = if self.identity_key.len() > 10 {
format!(
"{}...{}",
&self.identity_key[..6],
&self.identity_key[self.identity_key.len() - 4..]
)
} else {
self.identity_key.clone()
};
DisplayableIdentity {
name: self.name.clone(),
avatar_url: self
.avatar_url
.clone()
.unwrap_or_else(|| DefaultIdentityValues::AVATAR_URL.to_string()),
identity_key: self.identity_key.clone(),
abbreviated_key: abbreviated,
badge_icon_url: DefaultIdentityValues::BADGE_ICON_URL.to_string(),
badge_label: "Personal contact".to_string(),
badge_click_url: DefaultIdentityValues::BADGE_CLICK_URL.to_string(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct IdentityQuery {
#[serde(skip_serializing_if = "Option::is_none")]
pub identity_key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certificate_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub certifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<u32>,
}
impl IdentityQuery {
pub fn by_identity_key(identity_key: impl Into<String>) -> Self {
Self {
identity_key: Some(identity_key.into()),
..Default::default()
}
}
pub fn by_attributes(attributes: HashMap<String, String>) -> Self {
Self {
attributes: Some(attributes),
..Default::default()
}
}
pub fn by_attribute(key: impl Into<String>, value: impl Into<String>) -> Self {
let mut attributes = HashMap::new();
attributes.insert(key.into(), value.into());
Self {
attributes: Some(attributes),
..Default::default()
}
}
pub fn with_limit(mut self, limit: u32) -> Self {
self.limit = Some(limit);
self
}
pub fn with_offset(mut self, offset: u32) -> Self {
self.offset = Some(offset);
self
}
pub fn with_certifier(mut self, certifier: impl Into<String>) -> Self {
self.certifier = Some(certifier.into());
self
}
}
#[derive(Debug, Clone)]
pub struct IdentityResolutionResult {
pub identity: DisplayableIdentity,
pub certificates: Vec<VerifiableCertificate>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CertifierInfo {
pub name: String,
pub icon_url: String,
pub description: String,
pub trust: u8,
}
impl Default for CertifierInfo {
fn default() -> Self {
Self {
name: "Unknown Certifier".to_string(),
icon_url: DefaultIdentityValues::BADGE_ICON_URL.to_string(),
description: "No information available".to_string(),
trust: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct IdentityCertificate {
pub certificate: VerifiableCertificate,
pub certifier_info: CertifierInfo,
pub publicly_revealed_keyring: HashMap<String, Vec<u8>>,
pub decrypted_fields: HashMap<String, String>,
}
impl IdentityCertificate {
pub fn type_base64(&self) -> String {
self.certificate.certificate.type_base64()
}
pub fn subject_hex(&self) -> String {
self.certificate.subject().to_hex()
}
pub fn certifier_hex(&self) -> String {
self.certificate.certifier().to_hex()
}
pub fn known_type(&self) -> Option<KnownCertificateType> {
KnownCertificateType::from_type_id(&self.type_base64())
}
}
#[derive(Debug, Clone)]
pub struct IdentityClientConfig {
pub network_preset: NetworkPreset,
pub protocol_id: (u8, String),
pub key_id: String,
pub token_amount: u64,
pub output_index: u32,
pub originator: Option<String>,
}
impl Default for IdentityClientConfig {
fn default() -> Self {
Self {
network_preset: NetworkPreset::Mainnet,
protocol_id: (1, "identity".to_string()),
key_id: "1".to_string(),
token_amount: 1,
output_index: 0,
originator: None,
}
}
}
impl IdentityClientConfig {
pub fn with_originator(originator: impl Into<String>) -> Self {
Self {
originator: Some(originator.into()),
..Default::default()
}
}
pub fn with_network(mut self, network: NetworkPreset) -> Self {
self.network_preset = network;
self
}
pub fn with_token_amount(mut self, amount: u64) -> Self {
self.token_amount = amount;
self
}
}
#[derive(Debug, Clone)]
pub struct ContactsManagerConfig {
pub protocol_id: (u8, String),
pub basket: String,
pub originator: Option<String>,
}
impl Default for ContactsManagerConfig {
fn default() -> Self {
Self {
protocol_id: (2, "contact".to_string()),
basket: "contacts".to_string(),
originator: None,
}
}
}
impl ContactsManagerConfig {
pub fn with_originator(originator: impl Into<String>) -> Self {
Self {
originator: Some(originator.into()),
..Default::default()
}
}
}
pub struct StaticAvatarUrls;
impl StaticAvatarUrls {
pub const EMAIL: &'static str = "XUTZxep7BBghAJbSBwTjNfmcsDdRFs5EaGEgkESGSgjJVYgMEizu";
pub const PHONE: &'static str = "XUTLxtX3ELNUwRhLwL7kWNGbdnFM8WG2eSLv84J7654oH8HaJWrU";
pub const ANYONE: &'static str = "XUT4bpQ6cpBaXi1oMzZsXfpkWGbtp2JTUYAoN7PzhStFJ6wLfoeR";
pub const SELF: &'static str = "XUT9jHGk2qace148jeCX5rDsMftkSGYKmigLwU2PLLBc7Hm63VYR";
}
pub const DEFAULT_SOCIALCERT_CERTIFIER: &str =
"02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BroadcastSuccess {
pub txid: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BroadcastFailure {
pub code: String,
pub description: String,
}
#[derive(Debug, Clone)]
pub enum BroadcastResult {
Success(BroadcastSuccess),
Failure(BroadcastFailure),
}
impl BroadcastResult {
pub fn is_success(&self) -> bool {
matches!(self, Self::Success(_))
}
pub fn txid(&self) -> Option<&str> {
match self {
Self::Success(s) => Some(&s.txid),
Self::Failure(_) => None,
}
}
pub fn into_result(self) -> Result<BroadcastSuccess, BroadcastFailure> {
match self {
Self::Success(s) => Ok(s),
Self::Failure(f) => Err(f),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_known_certificate_type_names() {
assert_eq!(KnownCertificateType::IdentiCert.name(), "IdentiCert");
assert_eq!(KnownCertificateType::DiscordCert.name(), "DiscordCert");
assert_eq!(KnownCertificateType::PhoneCert.name(), "PhoneCert");
assert_eq!(KnownCertificateType::XCert.name(), "XCert");
assert_eq!(KnownCertificateType::Registrant.name(), "Registrant");
assert_eq!(KnownCertificateType::EmailCert.name(), "EmailCert");
assert_eq!(KnownCertificateType::Anyone.name(), "Anyone");
assert_eq!(KnownCertificateType::SelfCert.name(), "Self");
assert_eq!(KnownCertificateType::CoolCert.name(), "CoolCert");
}
#[test]
fn test_known_certificate_type_ids() {
assert_eq!(
KnownCertificateType::IdentiCert.type_id(),
"z40BOInXkI8m7f/wBrv4MJ09bZfzZbTj2fJqCtONqCY="
);
assert_eq!(
KnownCertificateType::XCert.type_id(),
"vdDWvftf1H+5+ZprUw123kjHlywH+v20aPQTuXgMpNc="
);
assert_eq!(
KnownCertificateType::Anyone.type_id(),
"mfkOMfLDQmrr3SBxBQ5WeE+6Hy3VJRFq6w4A5Ljtlis="
);
assert_eq!(
KnownCertificateType::SelfCert.type_id(),
"Hkge6X5JRxt1cWXtHLCrSTg6dCVTxjQJJ48iOYd7n3g="
);
}
#[test]
fn test_known_certificate_type_from_id() {
assert_eq!(
KnownCertificateType::from_type_id("z40BOInXkI8m7f/wBrv4MJ09bZfzZbTj2fJqCtONqCY="),
Some(KnownCertificateType::IdentiCert)
);
assert_eq!(
KnownCertificateType::from_type_id("vdDWvftf1H+5+ZprUw123kjHlywH+v20aPQTuXgMpNc="),
Some(KnownCertificateType::XCert)
);
assert_eq!(KnownCertificateType::from_type_id("unknown"), None);
}
#[test]
fn test_known_certificate_type_roundtrip() {
for cert_type in KnownCertificateType::all() {
let type_id = cert_type.type_id();
let parsed = KnownCertificateType::from_type_id(type_id);
assert_eq!(parsed, Some(*cert_type));
}
}
#[test]
fn test_displayable_identity_from_key() {
let identity = DisplayableIdentity::from_key(
"02abc123def456789012345678901234567890123456789012345678901234abcd",
);
assert!(identity.abbreviated_key.contains("..."));
assert_eq!(identity.identity_key.len(), 66);
assert_eq!(identity.abbreviated_key, "02abc1...abcd");
}
#[test]
fn test_displayable_identity_from_short_key() {
let identity = DisplayableIdentity::from_key("short");
assert_eq!(identity.abbreviated_key, "short");
assert_eq!(identity.identity_key, "short");
}
#[test]
fn test_displayable_identity_serialization() {
let identity = DisplayableIdentity {
name: "Alice".to_string(),
avatar_url: "https://example.com/avatar.png".to_string(),
identity_key: "02abc123".to_string(),
abbreviated_key: "02ab...".to_string(),
badge_icon_url: "https://example.com/badge.png".to_string(),
badge_label: "Verified".to_string(),
badge_click_url: "https://example.com/verify".to_string(),
};
let json = serde_json::to_string(&identity).unwrap();
assert!(json.contains("\"name\":\"Alice\""));
assert!(json.contains("\"avatarUrl\":"));
let decoded: DisplayableIdentity = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.name, "Alice");
assert_eq!(decoded.identity_key, "02abc123");
}
#[test]
fn test_contact_serialization() {
let contact = Contact {
identity_key: "02abc123".to_string(),
name: "Bob".to_string(),
avatar_url: Some("https://example.com/avatar.png".to_string()),
added_at: 1700000000000,
notes: Some("Friend".to_string()),
tags: vec!["work".to_string(), "friend".to_string()],
metadata: None,
};
let json = serde_json::to_string(&contact).unwrap();
assert!(json.contains("\"name\":\"Bob\""));
assert!(json.contains("\"identityKey\":\"02abc123\""));
let decoded: Contact = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.name, "Bob");
assert_eq!(decoded.tags.len(), 2);
}
#[test]
fn test_contact_without_optional_fields() {
let contact = Contact {
identity_key: "02abc123".to_string(),
name: "Charlie".to_string(),
avatar_url: None,
added_at: 0,
notes: None,
tags: Vec::new(),
metadata: None,
};
let json = serde_json::to_string(&contact).unwrap();
assert!(!json.contains("avatarUrl"));
assert!(!json.contains("notes"));
assert!(!json.contains("metadata"));
}
#[test]
fn test_identity_query_default() {
let query = IdentityQuery::default();
assert!(query.identity_key.is_none());
assert!(query.attributes.is_none());
assert!(query.certificate_type.is_none());
assert!(query.certifier.is_none());
assert!(query.limit.is_none());
}
#[test]
fn test_identity_query_by_identity_key() {
let query = IdentityQuery::by_identity_key("02abc123");
assert_eq!(query.identity_key, Some("02abc123".to_string()));
assert!(query.attributes.is_none());
}
#[test]
fn test_identity_query_by_attributes() {
let mut attrs = HashMap::new();
attrs.insert("email".to_string(), "test@example.com".to_string());
let query = IdentityQuery::by_attributes(attrs);
assert!(query.identity_key.is_none());
assert!(query.attributes.is_some());
assert_eq!(
query.attributes.as_ref().unwrap().get("email"),
Some(&"test@example.com".to_string())
);
}
#[test]
fn test_identity_query_builder() {
let query = IdentityQuery::by_attribute("email", "test@example.com")
.with_limit(10)
.with_offset(5)
.with_certifier("02certifier");
assert_eq!(query.limit, Some(10));
assert_eq!(query.offset, Some(5));
assert_eq!(query.certifier, Some("02certifier".to_string()));
}
#[test]
fn test_identity_query_serialization() {
let query = IdentityQuery::by_attribute("email", "test@example.com").with_limit(10);
let json = serde_json::to_string(&query).unwrap();
assert!(json.contains("test@example.com"));
assert!(json.contains("\"limit\":10"));
let decoded: IdentityQuery = serde_json::from_str(&json).unwrap();
assert_eq!(decoded.limit, Some(10));
}
#[test]
fn test_identity_client_config_default() {
let config = IdentityClientConfig::default();
assert_eq!(config.network_preset, NetworkPreset::Mainnet);
assert_eq!(config.protocol_id, (1, "identity".to_string()));
assert_eq!(config.key_id, "1");
assert_eq!(config.token_amount, 1);
assert_eq!(config.output_index, 0);
assert!(config.originator.is_none());
}
#[test]
fn test_identity_client_config_builder() {
let config = IdentityClientConfig::with_originator("myapp.com")
.with_network(NetworkPreset::Testnet)
.with_token_amount(100);
assert_eq!(config.originator, Some("myapp.com".to_string()));
assert_eq!(config.network_preset, NetworkPreset::Testnet);
assert_eq!(config.token_amount, 100);
}
#[test]
fn test_contacts_manager_config_default() {
let config = ContactsManagerConfig::default();
assert_eq!(config.protocol_id, (2, "contact".to_string()));
assert_eq!(config.basket, "contacts");
assert!(config.originator.is_none());
}
#[test]
fn test_certifier_info_default() {
let info = CertifierInfo::default();
assert_eq!(info.name, "Unknown Certifier");
assert_eq!(info.trust, 0);
}
#[test]
fn test_contact_from_identity() {
let identity = DisplayableIdentity::from_key("02abc123");
let contact = Contact::from_identity(identity.clone());
assert_eq!(contact.identity_key, identity.identity_key);
assert_eq!(contact.name, identity.name);
assert!(contact.added_at > 0);
}
#[test]
fn test_contact_to_displayable_identity() {
let contact = Contact {
identity_key: "02abc123def456789012345678901234567890123456789012345678901234567890"
.to_string(),
name: "Test User".to_string(),
avatar_url: Some("https://example.com/avatar.png".to_string()),
added_at: 1700000000000,
notes: None,
tags: Vec::new(),
metadata: None,
};
let identity = contact.to_displayable_identity();
assert_eq!(identity.name, "Test User");
assert_eq!(identity.avatar_url, "https://example.com/avatar.png");
assert!(identity.abbreviated_key.contains("..."));
}
}