//! Name Registry - On-chain name to CID mapping //! //! Manages the registration and resolution of human-readable names //! to content identifiers (CIDs) on the Synor blockchain. use crate::error::{Error, Result}; use crate::validate_name; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use synor_storage::ContentId; /// Address type (32 bytes) pub type Address = [u8; 32]; /// Name record stored on-chain #[derive(Debug, Clone, Serialize, Deserialize)] pub struct NameRecord { /// The registered name pub name: String, /// Owner address pub owner: Address, /// Current CID the name points to pub cid: ContentId, /// Block number when registered pub registered_at: u64, /// Block number when expires (0 = never) pub expires_at: u64, /// Custom domains linked to this name pub custom_domains: Vec, /// Metadata (optional JSON) pub metadata: Option, } /// Request to register a new name #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegistrationRequest { /// Name to register pub name: String, /// Initial CID pub cid: ContentId, /// Registration duration in blocks (0 = permanent) pub duration: u64, /// Optional metadata pub metadata: Option, } /// Name registry state #[derive(Debug, Default)] pub struct NameRegistry { /// Name to record mapping records: HashMap, /// Custom domain to name mapping domain_to_name: HashMap, /// Registration fee (atomic SYNOR) registration_fee: u64, /// Annual renewal fee (atomic SYNOR) renewal_fee: u64, /// Grace period in blocks after expiry grace_period: u64, } impl NameRegistry { /// Create a new name registry pub fn new() -> Self { Self { records: HashMap::new(), domain_to_name: HashMap::new(), registration_fee: 10_000_000_000, // 10 SYNOR renewal_fee: 5_000_000_000, // 5 SYNOR grace_period: 432_000, // ~30 days at 6s blocks } } /// Create registry with custom fees pub fn with_fees(registration_fee: u64, renewal_fee: u64, grace_period: u64) -> Self { Self { records: HashMap::new(), domain_to_name: HashMap::new(), registration_fee, renewal_fee, grace_period, } } /// Register a new name pub fn register( &mut self, request: RegistrationRequest, owner: Address, current_block: u64, ) -> Result { // Validate name validate_name(&request.name)?; // Check if name is available if let Some(existing) = self.records.get(&request.name) { // Check if expired and past grace period if existing.expires_at > 0 && current_block > existing.expires_at + self.grace_period { // Name can be re-registered } else { return Err(Error::NameTaken(request.name)); } } // Calculate expiry let expires_at = if request.duration == 0 { 0 // Permanent } else { current_block + request.duration }; let record = NameRecord { name: request.name.clone(), owner, cid: request.cid, registered_at: current_block, expires_at, custom_domains: Vec::new(), metadata: request.metadata, }; self.records.insert(request.name, record.clone()); Ok(record) } /// Update CID for a name (must be owner) pub fn update( &mut self, name: &str, new_cid: ContentId, caller: Address, current_block: u64, ) -> Result<()> { let record = self.get_record_mut(name, current_block)?; if record.owner != caller { return Err(Error::NotOwner); } record.cid = new_cid; Ok(()) } /// Transfer ownership of a name pub fn transfer( &mut self, name: &str, new_owner: Address, caller: Address, current_block: u64, ) -> Result<()> { let record = self.get_record_mut(name, current_block)?; if record.owner != caller { return Err(Error::NotOwner); } record.owner = new_owner; Ok(()) } /// Renew a name registration pub fn renew( &mut self, name: &str, additional_duration: u64, caller: Address, current_block: u64, ) -> Result { let record = self.get_record_mut(name, current_block)?; if record.owner != caller { return Err(Error::NotOwner); } if record.expires_at == 0 { return Err(Error::InvalidOperation("Name is permanent".into())); } // Extend from current expiry or current block (if in grace period) let base = if record.expires_at > current_block { record.expires_at } else { current_block }; record.expires_at = base + additional_duration; Ok(record.expires_at) } /// Resolve a name to its CID pub fn resolve(&self, name: &str, current_block: u64) -> Option { self.records.get(name).and_then(|record| { if self.is_valid(record, current_block) { Some(record.cid.clone()) } else { None } }) } /// Get full record for a name pub fn get_record(&self, name: &str, current_block: u64) -> Result<&NameRecord> { let record = self .records .get(name) .ok_or_else(|| Error::NameNotFound(name.to_string()))?; if !self.is_valid(record, current_block) { return Err(Error::NameExpired(name.to_string())); } Ok(record) } /// Get mutable record fn get_record_mut(&mut self, name: &str, current_block: u64) -> Result<&mut NameRecord> { let record = self .records .get(name) .ok_or_else(|| Error::NameNotFound(name.to_string()))?; // Check validity before returning mutable reference if !self.is_valid(record, current_block) { return Err(Error::NameExpired(name.to_string())); } Ok(self.records.get_mut(name).unwrap()) } /// Check if a record is valid (not expired or within grace period) fn is_valid(&self, record: &NameRecord, current_block: u64) -> bool { if record.expires_at == 0 { return true; // Permanent } current_block <= record.expires_at + self.grace_period } /// Check if a name is available for registration pub fn is_available(&self, name: &str, current_block: u64) -> bool { match self.records.get(name) { None => validate_name(name).is_ok(), Some(record) => { record.expires_at > 0 && current_block > record.expires_at + self.grace_period } } } /// Add a custom domain to a name pub fn add_custom_domain( &mut self, name: &str, domain: String, caller: Address, current_block: u64, ) -> Result<()> { // Check domain isn't already linked if self.domain_to_name.contains_key(&domain) { return Err(Error::DomainAlreadyLinked(domain)); } let record = self.get_record_mut(name, current_block)?; if record.owner != caller { return Err(Error::NotOwner); } record.custom_domains.push(domain.clone()); self.domain_to_name.insert(domain, name.to_string()); Ok(()) } /// Remove a custom domain from a name pub fn remove_custom_domain( &mut self, name: &str, domain: &str, caller: Address, current_block: u64, ) -> Result<()> { let record = self.get_record_mut(name, current_block)?; if record.owner != caller { return Err(Error::NotOwner); } record.custom_domains.retain(|d| d != domain); self.domain_to_name.remove(domain); Ok(()) } /// Resolve a custom domain to a name pub fn resolve_domain(&self, domain: &str) -> Option<&str> { self.domain_to_name.get(domain).map(|s| s.as_str()) } /// Resolve a custom domain directly to CID pub fn resolve_domain_to_cid(&self, domain: &str, current_block: u64) -> Option { self.resolve_domain(domain) .and_then(|name| self.resolve(name, current_block)) } /// Get registration fee pub fn registration_fee(&self) -> u64 { self.registration_fee } /// Get renewal fee pub fn renewal_fee(&self) -> u64 { self.renewal_fee } /// Get all names owned by an address pub fn names_by_owner(&self, owner: &Address, current_block: u64) -> Vec<&NameRecord> { self.records .values() .filter(|r| r.owner == *owner && self.is_valid(r, current_block)) .collect() } } #[cfg(test)] mod tests { use super::*; fn make_cid(data: &[u8]) -> ContentId { ContentId::from_content(data) } #[test] fn test_register_and_resolve() { let mut registry = NameRegistry::new(); let owner = [1u8; 32]; let cid = make_cid(b"hello world"); let request = RegistrationRequest { name: "myapp".to_string(), cid: cid.clone(), duration: 1000, metadata: None, }; registry.register(request, owner, 0).unwrap(); let resolved = registry.resolve("myapp", 0).unwrap(); assert_eq!(resolved.digest, cid.digest); } #[test] fn test_update_cid() { let mut registry = NameRegistry::new(); let owner = [1u8; 32]; let cid1 = make_cid(b"version 1"); let cid2 = make_cid(b"version 2"); let request = RegistrationRequest { name: "myapp".to_string(), cid: cid1.clone(), duration: 1000, metadata: None, }; registry.register(request, owner, 0).unwrap(); registry.update("myapp", cid2.clone(), owner, 0).unwrap(); let resolved = registry.resolve("myapp", 0).unwrap(); assert_eq!(resolved.digest, cid2.digest); } #[test] fn test_transfer_ownership() { let mut registry = NameRegistry::new(); let owner1 = [1u8; 32]; let owner2 = [2u8; 32]; let cid = make_cid(b"hello"); let request = RegistrationRequest { name: "myapp".to_string(), cid: cid.clone(), duration: 1000, metadata: None, }; registry.register(request, owner1, 0).unwrap(); registry.transfer("myapp", owner2, owner1, 0).unwrap(); let record = registry.get_record("myapp", 0).unwrap(); assert_eq!(record.owner, owner2); } #[test] fn test_name_expiry() { let mut registry = NameRegistry::new(); let owner = [1u8; 32]; let cid = make_cid(b"hello"); let request = RegistrationRequest { name: "myapp".to_string(), cid: cid.clone(), duration: 100, metadata: None, }; registry.register(request, owner, 0).unwrap(); // Should resolve before expiry assert!(registry.resolve("myapp", 50).is_some()); // Should still resolve during grace period assert!(registry.resolve("myapp", 150).is_some()); // Should not resolve after grace period let after_grace = 100 + registry.grace_period + 1; assert!(registry.resolve("myapp", after_grace).is_none()); } #[test] fn test_custom_domain() { let mut registry = NameRegistry::new(); let owner = [1u8; 32]; let cid = make_cid(b"hello"); let request = RegistrationRequest { name: "myapp".to_string(), cid: cid.clone(), duration: 1000, metadata: None, }; registry.register(request, owner, 0).unwrap(); registry .add_custom_domain("myapp", "myapp.com".to_string(), owner, 0) .unwrap(); assert_eq!(registry.resolve_domain("myapp.com"), Some("myapp")); let resolved = registry.resolve_domain_to_cid("myapp.com", 0).unwrap(); assert_eq!(resolved.digest, cid.digest); } #[test] fn test_not_owner_error() { let mut registry = NameRegistry::new(); let owner = [1u8; 32]; let other = [2u8; 32]; let cid = make_cid(b"hello"); let request = RegistrationRequest { name: "myapp".to_string(), cid: cid.clone(), duration: 1000, metadata: None, }; registry.register(request, owner, 0).unwrap(); // Other user cannot update let result = registry.update("myapp", make_cid(b"new"), other, 0); assert!(matches!(result, Err(Error::NotOwner))); } }