Add synor-hosting crate for decentralized web hosting with: - Name Registry: On-chain name→CID mapping with ownership, expiry, and custom domain linking - Domain Verification: CNAME/TXT DNS verification for custom domains - Hosting Router: Host-based routing with SPA support, redirects, and custom headers - synor.json: Project configuration for build, routes, and error pages Users can deploy to myapp.synor.cc and optionally link custom domains. 23 tests passing.
462 lines
13 KiB
Rust
462 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 synor_storage::ContentId;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
/// 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)));
|
|
}
|
|
}
|