467 lines
13 KiB
Rust
467 lines
13 KiB
Rust
//! 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<String>,
|
|
/// Metadata (optional JSON)
|
|
pub metadata: Option<String>,
|
|
}
|
|
|
|
/// 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<String>,
|
|
}
|
|
|
|
/// Name registry state
|
|
#[derive(Debug, Default)]
|
|
pub struct NameRegistry {
|
|
/// Name to record mapping
|
|
records: HashMap<String, NameRecord>,
|
|
/// Custom domain to name mapping
|
|
domain_to_name: HashMap<String, String>,
|
|
/// 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<NameRecord> {
|
|
// 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<u64> {
|
|
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<ContentId> {
|
|
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<ContentId> {
|
|
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)));
|
|
}
|
|
}
|