diff --git a/lib-blockchain/src/contracts/dao_registry/core.rs b/lib-blockchain/src/contracts/dao_registry/core.rs new file mode 100644 index 00000000..8ba7eb47 --- /dev/null +++ b/lib-blockchain/src/contracts/dao_registry/core.rs @@ -0,0 +1,127 @@ +use crate::contracts::utils::id_generation; +use crate::contracts::integration::ContractEvent; +use crate::contracts::dao_registry::types::*; +use crate::integration::crypto_integration::PublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// DAO Registry contract +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DAORegistry { + /// Map from dao_id -> DAOEntry + pub registry: HashMap<[u8; 32], DAOEntry>, + /// Index from token_addr -> dao_id for fast lookup + pub token_index: HashMap<[u8; 32], [u8; 32]>, +} + +impl DAORegistry { + /// Create empty registry + pub fn new() -> Self { + Self { + registry: HashMap::new(), + token_index: HashMap::new(), + } + } + + /// Register a new DAO + /// Returns (dao_id, ContractEvent::DaoRegistered) + pub fn register_dao( + &mut self, + token_addr: [u8; 32], + class: String, + metadata_hash: Option<[u8; 32]>, + treasury: PublicKey, + owner: PublicKey, + ) -> Result<([u8; 32], ContractEvent), String> { + if self.token_index.contains_key(&token_addr) { + return Err("DAO for token already registered".to_string()); + } + + // Generate unique DAO id using token_addr + owner + timestamp + let timestamp = crate::utils::time::current_timestamp(); + let dao_id = id_generation::generate_contract_id(&[ + &token_addr, + &owner.as_bytes(), + ×tamp.to_le_bytes(), + ]); + + let entry = DAOEntry { + dao_id, + token_addr, + class: class.clone(), + metadata_hash, + treasury: treasury.clone(), + owner: owner.clone(), + created_at: timestamp, + }; + + self.registry.insert(dao_id, entry); + self.token_index.insert(token_addr, dao_id); + + let event = ContractEvent::DaoRegistered { + dao_id, + token_addr, + owner: owner.clone(), + treasury, + class, + metadata_hash, + }; + + Ok((dao_id, event)) + } + + /// Lookup a DAO by token address + pub fn get_dao(&self, token_addr: [u8; 32]) -> Result { + match self.token_index.get(&token_addr) { + Some(dao_id) => match self.registry.get(dao_id) { + Some(entry) => Ok(DAOMetadata { + dao_id: entry.dao_id, + token_addr: entry.token_addr, + class: entry.class.clone(), + metadata_hash: entry.metadata_hash, + treasury: entry.treasury.clone(), + owner: entry.owner.clone(), + created_at: entry.created_at, + }), + None => Err("DAO entry not found for id".to_string()), + }, + None => Err("DAO not found for token address".to_string()), + } + } + + /// List all DAOs sorted by creation date ascending + /// Note: Pagination not implemented yet; consider adding (cursor, limit) later + pub fn list_daos(&self) -> Vec { + let mut all: Vec = self.registry.values().cloned().collect(); + all.sort_by_key(|e| e.created_at); + all + } + + /// Update metadata hash for a DAO. Only owner can update metadata. + /// Returns ContractEvent::DaoUpdated on success + pub fn update_metadata( + &mut self, + dao_id: [u8; 32], + updater: PublicKey, + metadata_hash: Option<[u8; 32]>, + ) -> Result { + let entry = self + .registry + .get_mut(&dao_id) + .ok_or_else(|| "DAO not found".to_string())?; + + if entry.owner != updater { + return Err("Only DAO owner can update metadata".to_string()); + } + + entry.metadata_hash = metadata_hash; + + let event = ContractEvent::DaoUpdated { + dao_id, + updater, + metadata_hash, + }; + + Ok(event) + } +} diff --git a/lib-blockchain/src/contracts/dao_registry/mod.rs b/lib-blockchain/src/contracts/dao_registry/mod.rs new file mode 100644 index 00000000..e231ac89 --- /dev/null +++ b/lib-blockchain/src/contracts/dao_registry/mod.rs @@ -0,0 +1,5 @@ +pub mod core; +pub mod types; + +pub use core::DAORegistry; +pub use types::{DAOEntry, DAOMetadata}; diff --git a/lib-blockchain/src/contracts/dao_registry/tests.rs b/lib-blockchain/src/contracts/dao_registry/tests.rs new file mode 100644 index 00000000..b2e8b627 --- /dev/null +++ b/lib-blockchain/src/contracts/dao_registry/tests.rs @@ -0,0 +1,65 @@ +use super::*; +use crate::integration::crypto_integration::PublicKey; +use crate::contracts::dao_registry::DAORegistry; + +fn test_public_key(id: u8) -> PublicKey { + PublicKey::new(vec![id; 32]) +} + +#[test] +fn test_register_and_get_dao() { + let mut reg = DAORegistry::new(); + let token = [1u8; 32]; + let owner = test_public_key(1); + let treasury = test_public_key(2); + + let (dao_id, event) = reg + .register_dao(token, "NP".to_string(), None, treasury.clone(), owner.clone()) + .expect("should register"); + + assert_eq!(event.event_type(), "DaoRegistered"); + + let meta = reg.get_dao(token).expect("should find dao"); + assert_eq!(meta.dao_id, dao_id); + assert_eq!(meta.owner, owner); +} + +#[test] +fn test_list_daos_sorted() { + let mut reg = DAORegistry::new(); + let t1 = [1u8; 32]; + let t2 = [2u8; 32]; + let owner = test_public_key(1); + let treasury = test_public_key(2); + + let _ = reg.register_dao(t2, "NP".to_string(), None, treasury.clone(), owner.clone()); + std::thread::sleep(std::time::Duration::from_millis(5)); + let _ = reg.register_dao(t1, "NP".to_string(), None, treasury.clone(), owner.clone()); + + let list = reg.list_daos(); + assert!(list.len() >= 2); + assert!(list[0].created_at <= list[1].created_at); +} + +#[test] +fn test_update_metadata_access_control() { + let mut reg = DAORegistry::new(); + let token = [3u8; 32]; + let owner = test_public_key(1); + let other = test_public_key(9); + let treasury = test_public_key(2); + + let (dao_id, _) = reg + .register_dao(token, "NP".to_string(), None, treasury.clone(), owner.clone()) + .expect("register"); + + // Unauthorized update + let res = reg.update_metadata(dao_id, other.clone(), Some([9u8; 32])); + assert!(res.is_err()); + + // Authorized update + let res = reg.update_metadata(dao_id, owner.clone(), Some([7u8; 32])); + assert!(res.is_ok()); + let event = res.unwrap(); + assert_eq!(event.event_type(), "DaoUpdated"); +} diff --git a/lib-blockchain/src/contracts/dao_registry/types.rs b/lib-blockchain/src/contracts/dao_registry/types.rs new file mode 100644 index 00000000..a3aa5d1f --- /dev/null +++ b/lib-blockchain/src/contracts/dao_registry/types.rs @@ -0,0 +1,26 @@ +use serde::{Deserialize, Serialize}; +use crate::integration::crypto_integration::PublicKey; + +/// DAO entry stored in the registry +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DAOEntry { + pub dao_id: [u8; 32], + pub token_addr: [u8; 32], + pub class: String, + pub metadata_hash: Option<[u8; 32]>, + pub treasury: PublicKey, + pub owner: PublicKey, + pub created_at: u64, +} + +/// Metadata view returned by queries +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DAOMetadata { + pub dao_id: [u8; 32], + pub token_addr: [u8; 32], + pub class: String, + pub metadata_hash: Option<[u8; 32]>, + pub treasury: PublicKey, + pub owner: PublicKey, + pub created_at: u64, +} diff --git a/lib-blockchain/src/contracts/integration/mod.rs b/lib-blockchain/src/contracts/integration/mod.rs index 9481b3e3..7305db31 100644 --- a/lib-blockchain/src/contracts/integration/mod.rs +++ b/lib-blockchain/src/contracts/integration/mod.rs @@ -355,6 +355,21 @@ pub enum ContractEvent { token_fees: u64, treasury_addr: PublicKey, }, + /// New DAO registered + DaoRegistered { + dao_id: [u8; 32], + token_addr: [u8; 32], + owner: PublicKey, + treasury: PublicKey, + class: String, + metadata_hash: Option<[u8; 32]>, + }, + /// DAO metadata updated + DaoUpdated { + dao_id: [u8; 32], + updater: PublicKey, + metadata_hash: Option<[u8; 32]>, + }, } impl ContractEvent { @@ -369,6 +384,8 @@ impl ContractEvent { ContractEvent::SwapExecuted { .. } => "SwapExecuted", ContractEvent::PoolCreated { .. } => "PoolCreated", ContractEvent::FeeCollected { .. } => "FeeCollected", + ContractEvent::DaoRegistered { .. } => "DaoRegistered", + ContractEvent::DaoUpdated { .. } => "DaoUpdated", } } @@ -511,6 +528,37 @@ mod tests { } } + #[test] + fn test_dao_event_serialization() { + let keypair1 = KeyPair::generate().unwrap(); + let owner = keypair1.public_key.clone(); + let treasury = PublicKey::new(vec![3u8; 32]); + + let event = ContractEvent::DaoRegistered { + dao_id: [9u8; 32], + token_addr: [8u8; 32], + owner: owner.clone(), + treasury: treasury.clone(), + class: "NP".to_string(), + metadata_hash: Some([1u8; 32]), + }; + + let serialized = event.to_bytes().unwrap(); + let deserialized = ContractEvent::from_bytes(&serialized).unwrap(); + + match deserialized { + ContractEvent::DaoRegistered { dao_id, token_addr, owner: o, treasury: t, class, metadata_hash } => { + assert_eq!(dao_id, [9u8; 32]); + assert_eq!(token_addr, [8u8; 32]); + assert_eq!(o, owner); + assert_eq!(t, treasury); + assert_eq!(class, "NP".to_string()); + assert_eq!(metadata_hash, Some([1u8; 32])); + } + _ => panic!("Wrong event type"), + } + } + #[test] fn test_blockchain_integration() { let storage = MemoryStorage::default(); diff --git a/lib-blockchain/src/contracts/mod.rs b/lib-blockchain/src/contracts/mod.rs index 691def69..a431d37a 100644 --- a/lib-blockchain/src/contracts/mod.rs +++ b/lib-blockchain/src/contracts/mod.rs @@ -29,6 +29,8 @@ pub mod emergency_reserve; #[cfg(feature = "contracts")] pub mod sov_swap; #[cfg(feature = "contracts")] +pub mod dao_registry; +#[cfg(feature = "contracts")] pub mod utils; #[cfg(feature = "contracts")] pub mod web4; @@ -69,6 +71,8 @@ pub use emergency_reserve::EmergencyReserve; #[cfg(feature = "contracts")] pub use sov_swap::{SovSwapPool, SwapDirection, SwapResult, PoolState, SwapError}; #[cfg(feature = "contracts")] +pub use dao_registry::{DAOEntry, DAORegistry, DAOMetadata}; +#[cfg(feature = "contracts")] pub use utils::*; #[cfg(feature = "contracts")] pub use web4::{Web4Contract, WebsiteContract, WebsiteMetadata, ContentRoute, DomainRecord, WebsiteDeploymentData};