diff --git a/dongle-smartcontract/Cargo.toml b/dongle-smartcontract/Cargo.toml index addc8f4..ebffde5 100644 --- a/dongle-smartcontract/Cargo.toml +++ b/dongle-smartcontract/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" crate-type = ["cdylib", "rlib"] [dependencies] -soroban-sdk = "21.7.6" +soroban-sdk = { version = "22.0.0" } [dev-dependencies] -soroban-sdk = { version = "21.7.6", features = ["testutils"] } +soroban-sdk = { version = "22.0.0", features = ["testutils"] } [features] testutils = ["soroban-sdk/testutils"] @@ -23,6 +23,7 @@ strip = "symbols" debug-assertions = false panic = "abort" codegen-units = 1 +lto = true [profile.release-with-logs] inherits = "release" diff --git a/dongle-smartcontract/src/admin_manager.rs b/dongle-smartcontract/src/admin_manager.rs new file mode 100644 index 0000000..9d4cf5f --- /dev/null +++ b/dongle-smartcontract/src/admin_manager.rs @@ -0,0 +1,252 @@ +//! Admin role management and access control +//! +//! This module provides functionality for managing admin roles and enforcing +//! access control across privileged contract operations. + +use crate::errors::ContractError; +use crate::events::{publish_admin_added_event, publish_admin_removed_event}; +use crate::storage_keys::StorageKey; +use soroban_sdk::{Address, Env, Vec}; + +pub struct AdminManager; + +impl AdminManager { + /// Initialize the contract with the first admin + pub fn initialize(env: &Env, admin: Address) { + // Don't require auth during initialization - this is typically called once during contract deployment + + // Set the admin in storage + env.storage() + .persistent() + .set(&StorageKey::Admin(admin.clone()), &true); + + // Initialize admin list + let mut admins = Vec::new(env); + admins.push_back(admin.clone()); + env.storage() + .persistent() + .set(&StorageKey::AdminList, &admins); + + publish_admin_added_event(env, admin); + } + + /// Add a new admin (only callable by existing admins) + pub fn add_admin(env: &Env, caller: Address, new_admin: Address) -> Result<(), ContractError> { + caller.require_auth(); + + // Verify caller is an admin + Self::require_admin(env, &caller)?; + + // Check if already an admin + if Self::is_admin(env, &new_admin) { + return Ok(()); // Already an admin, no-op + } + + // Add to admin mapping + env.storage() + .persistent() + .set(&StorageKey::Admin(new_admin.clone()), &true); + + // Add to admin list + let mut admins = Self::get_admin_list(env); + admins.push_back(new_admin.clone()); + env.storage() + .persistent() + .set(&StorageKey::AdminList, &admins); + + publish_admin_added_event(env, new_admin); + + Ok(()) + } + + /// Remove an admin (only callable by existing admins) + pub fn remove_admin( + env: &Env, + caller: Address, + admin_to_remove: Address, + ) -> Result<(), ContractError> { + caller.require_auth(); + + // Verify caller is an admin + Self::require_admin(env, &caller)?; + + // Prevent removing the last admin + let admins = Self::get_admin_list(env); + if admins.len() <= 1 { + return Err(ContractError::CannotRemoveLastAdmin); + } + + // Check if the address is actually an admin + if !Self::is_admin(env, &admin_to_remove) { + return Err(ContractError::AdminNotFound); + } + + // Remove from admin mapping + env.storage() + .persistent() + .remove(&StorageKey::Admin(admin_to_remove.clone())); + + // Remove from admin list + let mut new_admins = Vec::new(env); + for admin in admins.iter() { + if admin != admin_to_remove { + new_admins.push_back(admin); + } + } + env.storage() + .persistent() + .set(&StorageKey::AdminList, &new_admins); + + publish_admin_removed_event(env, admin_to_remove); + + Ok(()) + } + + /// Check if an address is an admin + pub fn is_admin(env: &Env, address: &Address) -> bool { + env.storage() + .persistent() + .get(&StorageKey::Admin(address.clone())) + .unwrap_or(false) + } + + /// Require that the caller is an admin, otherwise return an error + pub fn require_admin(env: &Env, address: &Address) -> Result<(), ContractError> { + if Self::is_admin(env, address) { + Ok(()) + } else { + Err(ContractError::AdminOnly) + } + } + + /// Get the list of all admins + pub fn get_admin_list(env: &Env) -> Vec
{ + env.storage() + .persistent() + .get(&StorageKey::AdminList) + .unwrap_or(Vec::new(env)) + } + + /// Get the count of admins + pub fn get_admin_count(env: &Env) -> u32 { + Self::get_admin_list(env).len() + } +} + +#[cfg(test)] +mod tests { + use crate::DongleContract; + use crate::DongleContractClient; + use soroban_sdk::{testutils::Address as _, Address, Env}; + + #[test] + fn test_initialize_admin() { + let env = Env::default(); + let contract_id = env.register_contract(None, DongleContract); + let client = DongleContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + + client.mock_all_auths().initialize(&admin); + + assert!(client.is_admin(&admin)); + assert_eq!(client.get_admin_count(), 1); + } + + #[test] + fn test_add_admin() { + let env = Env::default(); + let contract_id = env.register_contract(None, DongleContract); + let client = DongleContractClient::new(&env, &contract_id); + let admin1 = Address::generate(&env); + let admin2 = Address::generate(&env); + + client.mock_all_auths().initialize(&admin1); + client.mock_all_auths().add_admin(&admin1, &admin2); + + assert!(client.is_admin(&admin2)); + assert_eq!(client.get_admin_count(), 2); + } + + #[test] + fn test_add_admin_unauthorized() { + let env = Env::default(); + let contract_id = env.register_contract(None, DongleContract); + let client = DongleContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let non_admin = Address::generate(&env); + let new_admin = Address::generate(&env); + + client.mock_all_auths().initialize(&admin); + let result = client.mock_all_auths().try_add_admin(&non_admin, &new_admin); + + assert_eq!(result, Err(Ok(crate::errors::ContractError::AdminOnly))); + assert!(!client.is_admin(&new_admin)); + } + + #[test] + fn test_remove_admin() { + let env = Env::default(); + let contract_id = env.register_contract(None, DongleContract); + let client = DongleContractClient::new(&env, &contract_id); + let admin1 = Address::generate(&env); + let admin2 = Address::generate(&env); + + client.mock_all_auths().initialize(&admin1); + client.mock_all_auths().add_admin(&admin1, &admin2); + client.mock_all_auths().remove_admin(&admin1, &admin2); + + assert!(!client.is_admin(&admin2)); + assert_eq!(client.get_admin_count(), 1); + } + + #[test] + fn test_cannot_remove_last_admin() { + let env = Env::default(); + let contract_id = env.register_contract(None, DongleContract); + let client = DongleContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + + client.mock_all_auths().initialize(&admin); + let result = client.mock_all_auths().try_remove_admin(&admin, &admin); + + assert_eq!(result, Err(Ok(crate::errors::ContractError::CannotRemoveLastAdmin))); + assert!(client.is_admin(&admin)); + } + + #[test] + fn test_remove_non_existent_admin() { + let env = Env::default(); + let contract_id = env.register_contract(None, DongleContract); + let client = DongleContractClient::new(&env, &contract_id); + let admin = Address::generate(&env); + let non_admin = Address::generate(&env); + let another_admin = Address::generate(&env); + + client.mock_all_auths().initialize(&admin); + client.mock_all_auths().add_admin(&admin, &another_admin); + let result = client.mock_all_auths().try_remove_admin(&admin, &non_admin); + + assert_eq!(result, Err(Ok(crate::errors::ContractError::AdminNotFound))); + assert!(client.is_admin(&another_admin)); + } + + #[test] + fn test_get_admin_list() { + let env = Env::default(); + let contract_id = env.register_contract(None, DongleContract); + let client = DongleContractClient::new(&env, &contract_id); + let admin1 = Address::generate(&env); + let admin2 = Address::generate(&env); + let admin3 = Address::generate(&env); + + client.mock_all_auths().initialize(&admin1); + client.mock_all_auths().add_admin(&admin1, &admin2); + client.mock_all_auths().add_admin(&admin1, &admin3); + + let admins = client.get_admin_list(); + assert_eq!(admins.len(), 3); + assert!(admins.contains(&admin1)); + assert!(admins.contains(&admin2)); + assert!(admins.contains(&admin3)); + } +} diff --git a/dongle-smartcontract/src/constants.rs b/dongle-smartcontract/src/constants.rs index a3e77ce..0abc2a8 100644 --- a/dongle-smartcontract/src/constants.rs +++ b/dongle-smartcontract/src/constants.rs @@ -1,26 +1,35 @@ +#![allow(dead_code)] //! Contract limits and validation constants. Kept in one place for easy future updates. /// Maximum number of projects a single user (address) can register. Prevents abuse. +#[allow(dead_code)] pub const MAX_PROJECTS_PER_USER: u32 = 50; /// Minimum length for name, description, category (must be non-empty after trim in validation). +#[allow(dead_code)] pub const MIN_STRING_LEN: usize = 1; /// Maximum length for project name. -pub const MAX_NAME_LEN: usize = 128; +pub const MAX_NAME_LEN: usize = 50; /// Maximum length for project description. +#[allow(dead_code)] pub const MAX_DESCRIPTION_LEN: usize = 2048; /// Maximum length for category. +#[allow(dead_code)] pub const MAX_CATEGORY_LEN: usize = 64; /// Maximum length for website URL. +#[allow(dead_code)] pub const MAX_WEBSITE_LEN: usize = 256; /// Maximum length for any CID (logo, metadata, comment, evidence). +#[allow(dead_code)] pub const MAX_CID_LEN: usize = 128; /// Valid rating range (inclusive). Reviews must be in [RATING_MIN, RATING_MAX]. u32 for Soroban Val. +#[allow(dead_code)] pub const RATING_MIN: u32 = 1; +#[allow(dead_code)] pub const RATING_MAX: u32 = 5; diff --git a/dongle-smartcontract/src/errors.rs b/dongle-smartcontract/src/errors.rs index b7621c4..25fe80a 100644 --- a/dongle-smartcontract/src/errors.rs +++ b/dongle-smartcontract/src/errors.rs @@ -15,34 +15,45 @@ pub enum ContractError { InvalidRating = 4, /// Review not found ReviewNotFound = 5, + /// User has already reviewed this project + AlreadyReviewed = 6, /// Verification record not found - VerificationNotFound = 6, + VerificationNotFound = 7, /// Invalid verification status transition - InvalidStatusTransition = 7, + InvalidStatusTransition = 8, /// Only admin can perform this action - AdminOnly = 8, + AdminOnly = 9, /// Invalid fee amount - InvalidFeeAmount = 9, + InvalidFeeAmount = 10, /// Insufficient fee paid - InsufficientFee = 10, + InsufficientFee = 11, /// Invalid project data - missing required fields - InvalidProjectData = 11, + InvalidProjectData = 12, /// Project name too long - ProjectNameTooLong = 12, + ProjectNameTooLong = 13, /// Project description too long - ProjectDescriptionTooLong = 13, + ProjectDescriptionTooLong = 14, /// Invalid project category - InvalidProjectCategory = 14, + InvalidProjectCategory = 15, /// Verification already processed - VerificationAlreadyProcessed = 15, + VerificationAlreadyProcessed = 16, /// Cannot review own project - CannotReviewOwnProject = 16, + CannotReviewOwnProject = 17, /// Fee configuration not set - FeeConfigNotSet = 17, + FeeConfigNotSet = 18, /// Treasury address not set - TreasuryNotSet = 18, - /// User has already reviewed this project - AlreadyReviewed = 19, // I added your error here with a new unique ID - /// Review is already deleted + TreasuryNotSet = 19, + /// Review already deleted ReviewAlreadyDeleted = 20, + /// Invalid project name format (contains invalid characters) + InvalidProjectNameFormat = 21, + /// Not the reviewer (trying to modify another user's review) + NotReviewer = 22, + /// Cannot remove the last admin + CannotRemoveLastAdmin = 23, + /// Admin not found + AdminNotFound = 24, } + +// Legacy alias to avoid breaking any code that uses `Error` directly +pub type Error = ContractError; \ No newline at end of file diff --git a/dongle-smartcontract/src/events.rs b/dongle-smartcontract/src/events.rs index 9949c10..d35b6ff 100644 --- a/dongle-smartcontract/src/events.rs +++ b/dongle-smartcontract/src/events.rs @@ -28,6 +28,7 @@ pub fn publish_review_event( .publish((REVIEW, action_sym, project_id, reviewer), event_data); } +#[allow(dead_code)] pub fn publish_fee_paid_event(env: &Env, project_id: u64, amount: u128) { env.events().publish( (symbol_short!("FEE"), symbol_short!("PAID"), project_id), @@ -41,24 +42,39 @@ pub fn publish_fee_set_event(env: &Env, verification_fee: u128, registration_fee (verification_fee, registration_fee), ); } - -pub fn publish_verification_requested_event(env: &Env, project_id: u64, requester: Address) { - env.events().publish( +#[allow(dead_code)] +pub fn publish_verification_requested_event( + env: &Env, + project_id: u64, + requester: Address, + evidence_cid: String, +) { + env.events().publish( (symbol_short!("VERIFY"), symbol_short!("REQ"), project_id), - requester, + (requester, evidence_cid), ); } -pub fn publish_verification_approved_event(env: &Env, project_id: u64) { +pub fn publish_verification_approved_event(env: &Env, project_id: u64, admin: Address) { env.events().publish( (symbol_short!("VERIFY"), symbol_short!("APP"), project_id), - project_id, + admin, ); } -pub fn publish_verification_rejected_event(env: &Env, project_id: u64) { +pub fn publish_verification_rejected_event(env: &Env, project_id: u64, admin: Address) { env.events().publish( (symbol_short!("VERIFY"), symbol_short!("REJ"), project_id), - project_id, + admin, ); } + +pub fn publish_admin_added_event(env: &Env, admin: Address) { + env.events() + .publish((symbol_short!("ADMIN"), symbol_short!("ADDED")), admin); +} + +pub fn publish_admin_removed_event(env: &Env, admin: Address) { + env.events() + .publish((symbol_short!("ADMIN"), symbol_short!("REMOVED")), admin); +} diff --git a/dongle-smartcontract/src/fee_manager.rs b/dongle-smartcontract/src/fee_manager.rs index fa15986..eaefe20 100644 --- a/dongle-smartcontract/src/fee_manager.rs +++ b/dongle-smartcontract/src/fee_manager.rs @@ -1,8 +1,10 @@ //! Fee configuration and payment with validation and events. +use crate::admin_manager::AdminManager; use crate::errors::ContractError; -use crate::events::publish_fee_set_event; -use crate::types::{DataKey, FeeConfig}; +use crate::events::{publish_fee_paid_event, publish_fee_set_event}; +use crate::storage_keys::StorageKey; +use crate::types::FeeConfig; use soroban_sdk::{Address, Env}; pub struct FeeManager; @@ -10,80 +12,95 @@ pub struct FeeManager; impl FeeManager { pub fn set_fee( env: &Env, - _admin: Address, + admin: Address, token: Option
, amount: u128, treasury: Address, ) -> Result<(), ContractError> { + admin.require_auth(); + AdminManager::require_admin(env, &admin)?; + let config = FeeConfig { token, verification_fee: amount, registration_fee: 0, }; - env.storage().persistent().set(&DataKey::FeeConfig, &config); env.storage() .persistent() - .set(&DataKey::Treasury, &treasury); + .set(&StorageKey::FeeConfig, &config); + env.storage() + .persistent() + .set(&StorageKey::Treasury, &treasury); publish_fee_set_event(env, amount, 0); Ok(()) } pub fn pay_fee( - _env: &Env, - _payer: Address, - _project_id: u64, - _token: Option
, + env: &Env, + payer: Address, + project_id: u64, + token: Option
, ) -> Result<(), ContractError> { - todo!("Fee payment logic not implemented") - } + payer.require_auth(); + + let config = Self::get_fee_config(env)?; + + if config.token != token { + return Err(ContractError::InvalidProjectData); + } + + let amount = config.verification_fee; + if amount > 0 { + if let Some(token_address) = config.token { + let treasury: Address = env + .storage() + .persistent() + .get(&StorageKey::Treasury) + .ok_or(ContractError::TreasuryNotSet)?; + let client = soroban_sdk::token::Client::new(env, &token_address); + client.transfer(&payer, &treasury, &(amount as i128)); + } else { + return Err(ContractError::FeeConfigNotSet); + } + } - pub fn get_fee_config(env: &Env) -> Result { env.storage() .persistent() - .get(&DataKey::FeeConfig) - .ok_or(ContractError::FeeConfigNotSet) - } + .set(&StorageKey::FeePaidForProject(project_id), &true); - #[allow(dead_code)] - pub fn set_treasury( - _env: &Env, - _admin: Address, - _treasury: Address, - ) -> Result<(), ContractError> { - todo!("Treasury setting logic not implemented") - } + publish_fee_paid_event(env, project_id, amount); - #[allow(dead_code)] - pub fn get_treasury(_env: &Env) -> Result { - todo!("Treasury address retrieval logic not implemented") + Ok(()) } - #[allow(dead_code)] - pub fn get_operation_fee(_env: &Env, operation_type: &str) -> Result { - match operation_type { - "verification" => Ok(1000000), - "registration" => Ok(0), - _ => Err(ContractError::InvalidProjectData), - } + pub fn get_fee_config(env: &Env) -> Result { + env.storage() + .persistent() + .get(&StorageKey::FeeConfig) + .ok_or(ContractError::FeeConfigNotSet) } - #[allow(dead_code)] - pub fn fee_config_exists(_env: &Env) -> bool { - false + pub fn is_fee_paid(env: &Env, project_id: u64) -> bool { + env.storage() + .persistent() + .get(&StorageKey::FeePaidForProject(project_id)) + .unwrap_or(false) } - #[allow(dead_code)] - pub fn treasury_exists(_env: &Env) -> bool { - false + pub fn get_treasury(env: &Env) -> Result { + env.storage() + .persistent() + .get(&StorageKey::Treasury) + .ok_or(ContractError::TreasuryNotSet) } - #[allow(dead_code)] - pub fn refund_fee( - _env: &Env, - _recipient: Address, - _amount: u128, - _token: Option
, - ) -> Result<(), ContractError> { - todo!("Fee refund logic not implemented") + pub fn set_treasury(env: &Env, admin: Address, treasury: Address) -> Result<(), ContractError> { + admin.require_auth(); + AdminManager::require_admin(env, &admin)?; + + env.storage() + .persistent() + .set(&StorageKey::Treasury, &treasury); + Ok(()) } } diff --git a/dongle-smartcontract/src/lib.rs b/dongle-smartcontract/src/lib.rs index ff8d6cc..249b2fc 100644 --- a/dongle-smartcontract/src/lib.rs +++ b/dongle-smartcontract/src/lib.rs @@ -1,5 +1,12 @@ #![no_std] +//! # Dongle Smart Contract +//! +//! A decentralized project registry and discovery platform built on Stellar/Soroban. +//! This contract enables transparent project registration, community reviews, and +//! verification processes for the Stellar ecosystem. + +mod admin_manager; mod constants; mod errors; mod events; @@ -15,10 +22,14 @@ mod verification_registry; #[cfg(test)] mod test; +use crate::admin_manager::AdminManager; +use crate::errors::ContractError; use crate::fee_manager::FeeManager; use crate::project_registry::ProjectRegistry; use crate::review_registry::ReviewRegistry; -use crate::types::{FeeConfig, Project, Review, VerificationRecord}; +use crate::types::{ + FeeConfig, Project, ProjectRegistrationParams, ProjectUpdateParams, Review, VerificationRecord, +}; use crate::verification_registry::VerificationRegistry; use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec}; @@ -27,56 +38,92 @@ pub struct DongleContract; #[contractimpl] impl DongleContract { - // --- Project Registry --- + + // ========================================== + // INITIALIZATION & ADMIN FUNCTIONS + // ========================================== + + pub fn initialize(env: Env, admin: Address) { + AdminManager::initialize(&env, admin); + } + + pub fn add_admin(env: Env, caller: Address, new_admin: Address) -> Result<(), ContractError> { + AdminManager::add_admin(&env, caller, new_admin) + } + + pub fn remove_admin( + env: Env, + caller: Address, + admin_to_remove: Address, + ) -> Result<(), ContractError> { + AdminManager::remove_admin(&env, caller, admin_to_remove) + } + + pub fn is_admin(env: Env, address: Address) -> bool { + AdminManager::is_admin(&env, &address) + } + + pub fn get_admin_list(env: Env) -> Vec
{ + AdminManager::get_admin_list(&env) + } + + pub fn get_admin_count(env: Env) -> u32 { + AdminManager::get_admin_count(&env) + } + + // ========================================== + // FEE MANAGER FUNCTIONS + // ========================================== + + pub fn set_fee( + env: Env, + admin: Address, + token: Option
, + amount: u128, + treasury: Address, + ) -> Result<(), ContractError> { + FeeManager::set_fee(&env, admin, token, amount, treasury) + } + + pub fn pay_fee( + env: Env, + payer: Address, + project_id: u64, + token: Option
, + ) -> Result<(), ContractError> { + FeeManager::pay_fee(&env, payer, project_id, token) + } + + pub fn get_fee_config(env: Env) -> Result { + FeeManager::get_fee_config(&env) + } + + // ========================================== + // PROJECT REGISTRY FUNCTIONS + // ========================================== pub fn register_project( env: Env, - owner: Address, - name: String, - description: String, - category: String, - website: Option, - logo_cid: Option, - metadata_cid: Option, - ) -> u64 { - ProjectRegistry::register_project( - &env, - owner, - name, - description, - category, - website, - logo_cid, - metadata_cid, - ) + params: ProjectRegistrationParams, + ) -> Result { + ProjectRegistry::register_project(&env, params) } pub fn update_project( env: Env, - project_id: u64, - caller: Address, - name: Option, - description: Option, - category: Option, - website: Option>, - logo_cid: Option>, - metadata_cid: Option>, - ) -> Option { - ProjectRegistry::update_project( - &env, - project_id, - caller, - name, - description, - category, - website, - logo_cid, - metadata_cid, - ) - } - - pub fn get_project(env: Env, project_id: u64) -> Option { + params: ProjectUpdateParams, + ) -> Result { + ProjectRegistry::update_project(&env, params) + .ok_or(ContractError::ProjectNotFound) + } + + pub fn get_project(env: Env, project_id: u64) -> Result { ProjectRegistry::get_project(&env, project_id) + .ok_or(ContractError::ProjectNotFound) + } + + pub fn get_owner_project_count(env: Env, owner: Address) -> u32 { + ProjectRegistry::get_owner_project_count(&env, &owner) } pub fn list_projects(env: Env, start_id: u64, limit: u32) -> Vec { @@ -87,7 +134,9 @@ impl DongleContract { ProjectRegistry::get_projects_by_owner(&env, owner) } - // --- Review Registry --- + // ========================================== + // REVIEW SYSTEM FUNCTIONS + // ========================================== pub fn add_review( env: Env, @@ -95,7 +144,7 @@ impl DongleContract { reviewer: Address, rating: u32, comment_cid: Option, - ) { + ) -> Result<(), ContractError> { ReviewRegistry::add_review(&env, project_id, reviewer, rating, comment_cid) } @@ -105,76 +154,60 @@ impl DongleContract { reviewer: Address, rating: u32, comment_cid: Option, - ) { + ) -> Result<(), ContractError> { ReviewRegistry::update_review(&env, project_id, reviewer, rating, comment_cid) } - pub fn delete_review(env: Env, project_id: u64, reviewer: Address) { - let _ = ReviewRegistry::delete_review(&env, project_id, reviewer); + pub fn delete_review( + env: Env, + project_id: u64, + reviewer: Address, + ) -> Result<(), ContractError> { + ReviewRegistry::delete_review(&env, project_id, reviewer) } - pub fn get_review(env: Env, project_id: u64, reviewer: Address) -> Option { + pub fn get_review( + env: Env, + project_id: u64, + reviewer: Address, + ) -> Result { ReviewRegistry::get_review(&env, project_id, reviewer) + .ok_or(ContractError::ReviewNotFound) } - // --- Verification Registry --- + // ========================================== + // VERIFICATION SYSTEM FUNCTIONS + // ========================================== pub fn request_verification( env: Env, project_id: u64, requester: Address, evidence_cid: String, - ) { + ) -> Result<(), ContractError> { VerificationRegistry::request_verification(&env, project_id, requester, evidence_cid) } - pub fn approve_verification(env: Env, project_id: u64, admin: Address) { - let _ = VerificationRegistry::approve_verification(&env, project_id, admin); - } - - pub fn reject_verification(env: Env, project_id: u64, admin: Address) { - let _ = VerificationRegistry::reject_verification(&env, project_id, admin); - } - - pub fn get_verification(env: Env, project_id: u64) -> Option { - VerificationRegistry::get_verification(&env, project_id).ok() - } - - // --- Fee Manager --- - - pub fn set_fee( + pub fn approve_verification( env: Env, + project_id: u64, admin: Address, - token: Option
, - amount: u128, - treasury: Address, - ) { - let _ = FeeManager::set_fee(&env, admin, token, amount, treasury); - } - - pub fn pay_fee(env: Env, payer: Address, project_id: u64, token: Option
) { - let _ = FeeManager::pay_fee(&env, payer, project_id, token); - } - - pub fn get_fee_config(env: Env) -> FeeConfig { - FeeManager::get_fee_config(&env).unwrap_or(FeeConfig { - token: None, - verification_fee: 0, - registration_fee: 0, - }) + ) -> Result<(), ContractError> { + VerificationRegistry::approve_verification(&env, project_id, admin) } - pub fn get_owner_project_count(env: Env, owner: Address) -> u32 { - ProjectRegistry::get_projects_by_owner(&env, owner).len() - } - - pub fn set_admin(env: Env, admin: Address) { - env.storage() - .persistent() - .set(&crate::types::DataKey::Admin(admin), &()); + pub fn reject_verification( + env: Env, + project_id: u64, + admin: Address, + ) -> Result<(), ContractError> { + VerificationRegistry::reject_verification(&env, project_id, admin) } - pub fn initialize(env: Env, admin: Address) { - Self::set_admin(env, admin); + pub fn get_verification( + env: Env, + project_id: u64, + ) -> Result { + VerificationRegistry::get_verification(&env, project_id) } } diff --git a/dongle-smartcontract/src/project_registry.rs b/dongle-smartcontract/src/project_registry.rs index 209f037..4b853cc 100644 --- a/dongle-smartcontract/src/project_registry.rs +++ b/dongle-smartcontract/src/project_registry.rs @@ -1,5 +1,8 @@ +//! Project registration, updates, and retrieval. + use crate::errors::ContractError; -use crate::types::{DataKey, Project, VerificationStatus}; +use crate::storage_keys::StorageKey; +use crate::types::{Project, ProjectRegistrationParams, ProjectUpdateParams, VerificationStatus}; use soroban_sdk::{Address, Env, String, Vec}; pub struct ProjectRegistry; @@ -7,110 +10,86 @@ pub struct ProjectRegistry; impl ProjectRegistry { pub fn register_project( env: &Env, - owner: Address, - name: String, - description: String, - category: String, - website: Option, - logo_cid: Option, - metadata_cid: Option, - ) -> u64 { - owner.require_auth(); + params: ProjectRegistrationParams, + ) -> Result { + params.owner.require_auth(); // Validation - if name.len() == 0 { - panic!("InvalidProjectName"); - } - if description.len() == 0 { - panic!("InvalidProjectDescription"); - } - if category.len() == 0 { - panic!("InvalidProjectCategory"); - } + Self::validate_project_data(¶ms.name, ¶ms.description, ¶ms.category)?; let mut count: u64 = env .storage() .persistent() - .get(&DataKey::ProjectCount) + .get(&StorageKey::NextProjectId) .unwrap_or(0); count = count.saturating_add(1); - let now = env.ledger().timestamp(); let project = Project { id: count, - owner: owner.clone(), - name, - description, - category, - website, - logo_cid, - metadata_cid, + owner: params.owner.clone(), + name: params.name, + description: params.description, + category: params.category, + website: params.website, + logo_cid: params.logo_cid, + metadata_cid: params.metadata_cid, verification_status: VerificationStatus::Unverified, - created_at: now, - updated_at: now, + created_at: env.ledger().timestamp(), + updated_at: env.ledger().timestamp(), }; env.storage() .persistent() - .set(&DataKey::Project(count), &project); + .set(&StorageKey::Project(count), &project); env.storage() .persistent() - .set(&DataKey::ProjectCount, &count); + .set(&StorageKey::NextProjectId, &count); + // Update owner projects list let mut owner_projects: Vec = env .storage() .persistent() - .get(&DataKey::OwnerProjects(owner.clone())) - .unwrap_or(Vec::new(env)); + .get(&StorageKey::OwnerProjects(params.owner.clone())) + .unwrap_or_else(|| Vec::new(env)); owner_projects.push_back(count); env.storage() .persistent() - .set(&DataKey::OwnerProjects(owner.clone()), &owner_projects); + .set(&StorageKey::OwnerProjects(params.owner), &owner_projects); - count + Ok(count) } - pub fn update_project( - env: &Env, - project_id: u64, - caller: Address, - name: Option, - description: Option, - category: Option, - website: Option>, - logo_cid: Option>, - metadata_cid: Option>, - ) -> Option { - let mut project = Self::get_project(env, project_id)?; - - caller.require_auth(); - if project.owner != caller { + pub fn update_project(env: &Env, params: ProjectUpdateParams) -> Option { + let mut project = Self::get_project(env, params.project_id)?; + + params.caller.require_auth(); + if project.owner != params.caller { return None; } - if let Some(value) = name { + if let Some(value) = params.name { project.name = value; } - if let Some(value) = description { + if let Some(value) = params.description { project.description = value; } - if let Some(value) = category { + if let Some(value) = params.category { project.category = value; } - if let Some(value) = website { + if let Some(value) = params.website { project.website = value; } - if let Some(value) = logo_cid { + if let Some(value) = params.logo_cid { project.logo_cid = value; } - if let Some(value) = metadata_cid { + if let Some(value) = params.metadata_cid { project.metadata_cid = value; } project.updated_at = env.ledger().timestamp(); env.storage() .persistent() - .set(&DataKey::Project(project_id), &project); + .set(&StorageKey::Project(params.project_id), &project); Some(project) } @@ -118,16 +97,15 @@ impl ProjectRegistry { pub fn get_project(env: &Env, project_id: u64) -> Option { env.storage() .persistent() - .get(&DataKey::Project(project_id)) + .get(&StorageKey::Project(project_id)) } pub fn get_projects_by_owner(env: &Env, owner: Address) -> Vec { let ids: Vec = env .storage() .persistent() - .get(&DataKey::OwnerProjects(owner)) - .unwrap_or(Vec::new(env)); - + .get(&StorageKey::OwnerProjects(owner)) + .unwrap_or_else(|| Vec::new(env)); let mut projects = Vec::new(env); let len = ids.len(); for i in 0..len { @@ -137,7 +115,6 @@ impl ProjectRegistry { } } } - projects } @@ -145,7 +122,7 @@ impl ProjectRegistry { let count: u64 = env .storage() .persistent() - .get(&DataKey::ProjectCount) + .get(&StorageKey::NextProjectId) .unwrap_or(0); let mut projects = Vec::new(env); @@ -164,14 +141,21 @@ impl ProjectRegistry { projects } - #[allow(dead_code)] pub fn project_exists(env: &Env, project_id: u64) -> bool { env.storage() .persistent() - .has(&DataKey::Project(project_id)) + .has(&StorageKey::Project(project_id)) + } + + pub fn get_owner_project_count(env: &Env, owner: &Address) -> u32 { + let ids: Vec = env + .storage() + .persistent() + .get(&StorageKey::OwnerProjects(owner.clone())) + .unwrap_or_else(|| Vec::new(env)); + ids.len() } - #[allow(dead_code)] pub fn validate_project_data( name: &String, description: &String, @@ -180,11 +164,14 @@ impl ProjectRegistry { if name.len() == 0 { return Err(ContractError::InvalidProjectData); } + if name.len() > 50 { + return Err(ContractError::ProjectNameTooLong); + } if description.len() == 0 { - return Err(ContractError::ProjectDescriptionTooLong); // Just picking one for now to match ContractError + return Err(ContractError::InvalidProjectData); } if category.len() == 0 { - return Err(ContractError::InvalidProjectCategory); + return Err(ContractError::InvalidProjectData); } Ok(()) } diff --git a/dongle-smartcontract/src/rating_calculator.rs b/dongle-smartcontract/src/rating_calculator.rs index 32f244b..0ace71b 100644 --- a/dongle-smartcontract/src/rating_calculator.rs +++ b/dongle-smartcontract/src/rating_calculator.rs @@ -5,6 +5,7 @@ /// For example, a rating of 4.50 is stored as 450. pub struct RatingCalculator; +#[allow(dead_code)] impl RatingCalculator { /// Calculate average rating from sum and count. /// Returns 0 if review_count is 0 (handles division by zero). @@ -31,6 +32,7 @@ impl RatingCalculator { /// /// # Returns /// Tuple of (new_sum, new_count, new_average) + #[allow(dead_code)] pub fn add_rating(current_sum: u64, current_count: u32, new_rating: u32) -> (u64, u32, u32) { let scaled_rating = (new_rating as u64) * 100; let new_sum = current_sum + scaled_rating; @@ -49,6 +51,7 @@ impl RatingCalculator { /// /// # Returns /// Tuple of (new_sum, new_count, new_average) + #[allow(dead_code)] pub fn update_rating( current_sum: u64, current_count: u32, diff --git a/dongle-smartcontract/src/review_registry.rs b/dongle-smartcontract/src/review_registry.rs index 15f6fe6..4bddf0c 100644 --- a/dongle-smartcontract/src/review_registry.rs +++ b/dongle-smartcontract/src/review_registry.rs @@ -1,9 +1,9 @@ -//! Review submission with validation, duplicate handling, and events. +//! Review submission with validation, duplicate handling, events, and timestamps. use crate::errors::ContractError; use crate::events::publish_review_event; -use crate::rating_calculator::RatingCalculator; -use crate::types::{DataKey, ProjectStats, Review, ReviewAction}; +use crate::storage_keys::StorageKey; +use crate::types::{Review, ReviewAction}; use soroban_sdk::{Address, Env, String}; pub struct ReviewRegistry; @@ -15,32 +15,29 @@ impl ReviewRegistry { reviewer: Address, rating: u32, comment_cid: Option, - ) { + ) -> Result<(), ContractError> { reviewer.require_auth(); - let review_key = DataKey::Review(project_id, reviewer.clone()); + if rating < 1 || rating > 5 { + return Err(ContractError::InvalidRating); + } + + let key = StorageKey::Review(project_id, reviewer.clone()); + if env.storage().persistent().has(&key) { + return Err(ContractError::AlreadyReviewed); + } + + let now = env.ledger().timestamp(); let review = Review { project_id, reviewer: reviewer.clone(), rating, - timestamp: env.ledger().timestamp(), comment_cid: comment_cid.clone(), - is_deleted: false, + created_at: now, + updated_at: now, }; - if !env.storage().persistent().has(&review_key) { - let mut user_reviews: soroban_sdk::Vec = env - .storage() - .persistent() - .get(&DataKey::UserReviews(reviewer.clone())) - .unwrap_or(soroban_sdk::Vec::new(env)); - user_reviews.push_back(project_id); - env.storage() - .persistent() - .set(&DataKey::UserReviews(reviewer.clone()), &user_reviews); - } - - env.storage().persistent().set(&review_key, &review); + env.storage().persistent().set(&key, &review); publish_review_event( env, @@ -49,6 +46,8 @@ impl ReviewRegistry { ReviewAction::Submitted, comment_cid, ); + + Ok(()) } pub fn update_review( @@ -57,21 +56,27 @@ impl ReviewRegistry { reviewer: Address, rating: u32, comment_cid: Option, - ) { + ) -> Result<(), ContractError> { reviewer.require_auth(); - let review_key = DataKey::Review(project_id, reviewer.clone()); + if rating < 1 || rating > 5 { + return Err(ContractError::InvalidRating); + } + + let key = StorageKey::Review(project_id, reviewer.clone()); let mut review: Review = env .storage() .persistent() - .get(&review_key) - .expect("Review not found"); + .get(&key) + .ok_or(ContractError::ReviewNotFound)?; + let now = env.ledger().timestamp(); review.rating = rating; review.comment_cid = comment_cid.clone(); - review.timestamp = env.ledger().timestamp(); + // created_at is intentionally not modified — it forms the immutable submission record + review.updated_at = now; - env.storage().persistent().set(&review_key, &review); + env.storage().persistent().set(&key, &review); publish_review_event( env, @@ -80,62 +85,20 @@ impl ReviewRegistry { ReviewAction::Updated, comment_cid, ); + + Ok(()) } - pub fn delete_review( - env: &Env, - project_id: u64, - reviewer: Address, - ) -> Result<(), ContractError> { - // 1. Authorize the caller - only the reviewer can delete their own review + pub fn delete_review(env: &Env, project_id: u64, reviewer: Address) -> Result<(), ContractError> { reviewer.require_auth(); - // 2. Fetch the review - let review_key = DataKey::Review(project_id, reviewer.clone()); - let mut review: Review = env - .storage() - .persistent() - .get(&review_key) - .ok_or(ContractError::ReviewNotFound)?; - - // 3. Validate it hasn't already been deleted - if review.is_deleted { - return Err(ContractError::ReviewAlreadyDeleted); - } - - // 4. Update the aggregate ratings - let stats_key = DataKey::ProjectStats(project_id); - let mut stats: ProjectStats = - env.storage() - .persistent() - .get(&stats_key) - .unwrap_or(ProjectStats { - rating_sum: 0, - review_count: 0, - average_rating: 0, - }); - - // Use your RatingCalculator to safely remove the rating - if stats.review_count > 0 { - let (new_sum, new_count, new_avg) = RatingCalculator::remove_rating( - stats.rating_sum, - stats.review_count, - review.rating, - ); - - stats.rating_sum = new_sum; - stats.review_count = new_count; - stats.average_rating = new_avg; - - // Save the updated stats - env.storage().persistent().set(&stats_key, &stats); + let key = StorageKey::Review(project_id, reviewer.clone()); + if !env.storage().persistent().has(&key) { + return Err(ContractError::ReviewNotFound); } - // 5. Perform the soft delete - review.is_deleted = true; - env.storage().persistent().set(&review_key, &review); + env.storage().persistent().remove(&key); - // 6. Emit the deleted event publish_review_event(env, project_id, reviewer, ReviewAction::Deleted, None); Ok(()) @@ -144,134 +107,113 @@ impl ReviewRegistry { pub fn get_review(env: &Env, project_id: u64, reviewer: Address) -> Option { env.storage() .persistent() - .get(&DataKey::Review(project_id, reviewer)) - } - - pub fn get_reviews_by_user( - env: &Env, - user: Address, - offset: u32, - limit: u32, - ) -> soroban_sdk::Vec { - let project_ids: soroban_sdk::Vec = env - .storage() - .persistent() - .get(&DataKey::UserReviews(user.clone())) - .unwrap_or(soroban_sdk::Vec::new(env)); - - let mut reviews = soroban_sdk::Vec::new(env); - let start = offset; - let len = project_ids.len(); - let end = core::cmp::min(offset.saturating_add(limit), len); - - for i in start..end { - if let Some(project_id) = project_ids.get(i) { - if let Some(review) = Self::get_review(env, project_id, user.clone()) { - reviews.push_back(review); - } - } - } - - reviews + .get(&StorageKey::Review(project_id, reviewer)) } } #[cfg(test)] -mod test { - use super::*; - use crate::types::ReviewEventData; - use crate::{DongleContract, DongleContractClient}; - use soroban_sdk::String as SorobanString; - use soroban_sdk::{ - testutils::{Address as _, Events}, - Env, IntoVal, String, - }; +mod tests { + use crate::DongleContract; + use crate::DongleContractClient; + use crate::types::ProjectRegistrationParams; + use soroban_sdk::{testutils::{Address as _, Ledger}, Address, Env, String as SorobanString}; + + fn setup(env: &Env) -> (DongleContractClient, Address) { + let contract_id = env.register(DongleContract, ()); + let client = DongleContractClient::new(env, &contract_id); + let reviewer = Address::generate(env); + (client, reviewer) + } + + fn register_project_1(env: &Env, client: &DongleContractClient) -> u64 { + let owner = Address::generate(env); + client.register_project(&ProjectRegistrationParams { + owner, + name: SorobanString::from_str(env, "Test Project"), + description: SorobanString::from_str(env, "A test project"), + category: SorobanString::from_str(env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + }) + } #[test] - fn test_add_review_event() { + fn test_review_timestamp_set_on_submission() { let env = Env::default(); env.mock_all_auths(); - let reviewer = Address::generate(&env); - let owner = Address::generate(&env); - let comment_cid = String::from_str(&env, "QmHash"); - let contract_id = env.register_contract(None, DongleContract); - let client = DongleContractClient::new(&env, &contract_id); - - client.initialize(&owner); - let name = SorobanString::from_str(&env, "Test Project"); - let desc = - SorobanString::from_str(&env, "A description that is long enough for validation."); - let cat = SorobanString::from_str(&env, "DeFi"); - let project_id = client.register_project(&owner, &name, &desc, &cat, &None, &None, &None); - client.add_review(&project_id, &reviewer, &5, &Some(comment_cid.clone())); - - let events = env.events().all(); - assert!(events.len() >= 1); - - let (_, topics, data) = events.last().unwrap(); - assert_eq!(topics.len(), 4); - - let topic0: soroban_sdk::Symbol = topics.get(0).unwrap().into_val(&env); - let topic1: soroban_sdk::Symbol = topics.get(1).unwrap().into_val(&env); - let topic2: u64 = topics.get(2).unwrap().into_val(&env); - let topic3: Address = topics.get(3).unwrap().into_val(&env); - - assert_eq!(topic0, soroban_sdk::symbol_short!("REVIEW")); - assert_eq!(topic1, soroban_sdk::symbol_short!("SUBMITTED")); - assert_eq!(topic2, project_id); - assert_eq!(topic3, reviewer); - - let event_data: ReviewEventData = data.into_val(&env); - assert_eq!(event_data.project_id, project_id); - assert_eq!(event_data.reviewer, reviewer); - assert_eq!(event_data.action, ReviewAction::Submitted); - assert_eq!(event_data.comment_cid, Some(comment_cid)); + let (client, reviewer) = setup(&env); + let project_id = register_project_1(&env, &client); + let expected_ts = 123456789; + + env.ledger().set_timestamp(expected_ts); + + client.add_review(&project_id, &reviewer, &5, &None); + + let review = client.get_review(&project_id, &reviewer); + + assert_eq!(review.created_at, expected_ts); + assert_eq!(review.updated_at, expected_ts); } - /* - #[test] - fn test_update_review_event() { - let env = Env::default(); - let reviewer = Address::generate(&env); - let comment_cid = String::from_str(&env, "QmHash2"); - let contract_id = env.register_contract(None, ReviewRegistry); - let client = ReviewRegistryClient::new(&env, &contract_id); + #[test] + fn test_review_updated_at_changes_on_edit() { + let env = Env::default(); + env.mock_all_auths(); + let (client, reviewer) = setup(&env); + let project_id = register_project_1(&env, &client); - client.mock_all_auths().update_review(&1, &reviewer, &4, &Some(comment_cid.clone())); + let t1 = 10000; + env.ledger().set_timestamp(t1); + client.add_review(&project_id, &reviewer, &4, &None); - let events = env.events().all(); - assert_eq!(events.len(), 1); + let t2 = 20000; + env.ledger().set_timestamp(t2); + client.update_review(&project_id, &reviewer, &5, &None); - let (_, topics, data) = events.last().unwrap(); - let topic1: soroban_sdk::Symbol = topics.get(1).unwrap().into_val(&env); - assert_eq!(topic1, soroban_sdk::symbol_short!("UPDATED")); + let review = client.get_review(&project_id, &reviewer); - let event_data: ReviewEventData = data.into_val(&env); - assert_eq!(event_data.action, ReviewAction::Updated); - assert_eq!(event_data.comment_cid, Some(comment_cid)); - } - */ + assert_eq!(review.created_at, t1); + assert_eq!(review.updated_at, t2); + } - /* - #[test] - fn test_delete_review_event() { - let env = Env::default(); - let reviewer = Address::generate(&env); - let contract_id = env.register_contract(None, ReviewRegistry); - let client = ReviewRegistryClient::new(&env, &contract_id); + #[test] + fn test_review_created_at_is_immutable() { + let env = Env::default(); + env.mock_all_auths(); + let (client, reviewer) = setup(&env); + let project_id = register_project_1(&env, &client); - client.mock_all_auths().delete_review(&1, &reviewer); + let t1 = 10000; + env.ledger().set_timestamp(t1); + client.add_review(&project_id, &reviewer, &3, &None); - let events = env.events().all(); - assert_eq!(events.len(), 1); + let times = [20000, 30000, 40000]; - let (_, topics, data) = events.last().unwrap(); - let topic1: soroban_sdk::Symbol = topics.get(1).unwrap().into_val(&env); - assert_eq!(topic1, soroban_sdk::symbol_short!("DELETED")); + for &t in × { + env.ledger().set_timestamp(t); + client.update_review(&project_id, &reviewer, &4, &None); + let review = client.get_review(&project_id, &reviewer); - let event_data: ReviewEventData = data.into_val(&env); - assert_eq!(event_data.action, ReviewAction::Deleted); - assert_eq!(event_data.comment_cid, None); + assert_eq!(review.created_at, t1); + assert_eq!(review.updated_at, t); } - */ -} + } + + #[test] + fn test_review_timestamps_use_ledger_time_not_caller_input() { + let env = Env::default(); + env.mock_all_auths(); + let (client, reviewer) = setup(&env); + let project_id = register_project_1(&env, &client); + let expected_ts = 987654321; + + env.ledger().set_timestamp(expected_ts); + client.add_review(&project_id, &reviewer, &5, &None); + + let review = client.get_review(&project_id, &reviewer); + + assert_eq!(review.created_at, expected_ts); + assert_eq!(review.updated_at, expected_ts); + } +} \ No newline at end of file diff --git a/dongle-smartcontract/src/storage_keys.rs b/dongle-smartcontract/src/storage_keys.rs index 5541730..c0a86e2 100644 --- a/dongle-smartcontract/src/storage_keys.rs +++ b/dongle-smartcontract/src/storage_keys.rs @@ -1,6 +1,6 @@ //! Storage key types for persistent storage. Modular to allow future extensions. -use soroban_sdk::contracttype; +use soroban_sdk::{contracttype, Address, String}; /// Keys for contract storage. Using an enum keeps keys namespaced and avoids collisions. #[contracttype] @@ -11,17 +11,31 @@ pub enum StorageKey { /// Next project id (counter). NextProjectId, /// Number of projects registered by owner (Address). - OwnerProjectCount(soroban_sdk::Address), + OwnerProjectCount(Address), + /// Project stats (ratings, etc). + ProjectStats(u64), + /// List of project IDs registered by owner. + OwnerProjects(Address), + /// Project by name (for duplicate detection). + ProjectByName(String), + /// Project count. + ProjectCount, /// Review by (project_id, reviewer address). - Review(u64, soroban_sdk::Address), + Review(u64, Address), /// Verification record by project_id. Verification(u64), /// Fee configuration (single global). FeeConfig, /// Whether verification fee has been paid for project_id. FeePaidForProject(u64), - /// Admin address (for fee set and verifier checks). - Admin, + /// Admin address mapping (for role-based access control). + Admin(soroban_sdk::Address), + /// List of all admin addresses. + AdminList, /// List of project IDs reviewed by a user. - UserReviews(soroban_sdk::Address), + UserReviews(Address), + /// Treasury address. + Treasury, + /// List of reviewer addresses for a project (by project_id). + ProjectReviews(u64), } diff --git a/dongle-smartcontract/src/test.rs b/dongle-smartcontract/src/test.rs index 20adb04..cb4e81a 100644 --- a/dongle-smartcontract/src/test.rs +++ b/dongle-smartcontract/src/test.rs @@ -1,441 +1,156 @@ -//! Tests for validation, limits, error codes, and edge cases. +//! Integration tests for the Dongle smart contract. -use crate::constants::MAX_PROJECTS_PER_USER; -use crate::errors::ContractError as Error; -use crate::types::{FeeConfig, VerificationStatus}; +use crate::errors::ContractError; +use crate::types::{FeeConfig, ProjectRegistrationParams, ProjectUpdateParams, VerificationStatus}; use crate::DongleContract; use crate::DongleContractClient; use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, Env, String as SorobanString, Vec}; -fn setup(env: &Env) -> (DongleContractClient, Address, Address) { - let contract_id = env.register_contract(None, DongleContract); +fn setup(env: &Env) -> (DongleContractClient, Address) { + env.mock_all_auths(); + let contract_id = env.register(DongleContract, ()); let client = DongleContractClient::new(env, &contract_id); let admin = Address::generate(env); - let owner = Address::generate(env); - // client.set_admin(&admin); // DongleContract doesn't have set_admin at the top level yet in my lib.rs - (client, admin, owner) + client.initialize(&admin); + (client, admin) +} + +fn make_params(env: &Env, owner: &Address) -> ProjectRegistrationParams { + ProjectRegistrationParams { + owner: owner.clone(), + name: SorobanString::from_str(env, "Project A"), + description: SorobanString::from_str(env, "Description A"), + category: SorobanString::from_str(env, "DeFi"), + website: None, + logo_cid: None, + metadata_cid: None, + } } -fn register_one_project(_env: &Env, client: &DongleContractClient, owner: &Address) -> u64 { - let name = SorobanString::from_str(_env, "Project A"); - let description = SorobanString::from_str(_env, "Description A - This is a long enough description to satisfy any potential future length requirements in tests."); - let category = SorobanString::from_str(_env, "DeFi"); - client.mock_all_auths().register_project( - owner, - &name, - &description, - &category, - &None, - &None, - &None, - ) +fn register_one_project(env: &Env, client: &DongleContractClient, owner: &Address) -> u64 { + client.register_project(&make_params(env, owner)) } #[test] fn test_register_project_success() { let env = Env::default(); - let (client, _, owner) = setup(&env); + let (client, _) = setup(&env); + let owner = Address::generate(&env); let id = register_one_project(&env, &client, &owner); assert_eq!(id, 1); - let project = client.get_project(&id).unwrap(); + let project = client.get_project(&id); assert_eq!(project.name, SorobanString::from_str(&env, "Project A")); assert_eq!(project.owner, owner); assert_eq!(client.get_owner_project_count(&owner), 1); } -/* #[test] fn test_validation_invalid_project_name_empty() { let env = Env::default(); - let (client, _, owner) = setup(&env); - let result = client.try_register_project( - &owner, - &SorobanString::from_str(&env, ""), - &SorobanString::from_str(&env, "Desc"), - &SorobanString::from_str(&env, "Cat"), - &None, - &None, - &None, - ); - assert_eq!(result, Err(Ok(Error::InvalidProjectData))); -} - -#[test] -fn test_validation_invalid_project_name_whitespace_only() { - let env = Env::default(); - let (client, _, owner) = setup(&env); - let result = client.try_register_project( - &owner, - &SorobanString::from_str(&env, " "), - &SorobanString::from_str(&env, "Desc"), - &SorobanString::from_str(&env, "Cat"), - &None, - &None, - &None, - ); - // My Implementation doesn't handle whitespace yet, so let's adjust or assume it fails if empty/invalid - // For now, if it's not empty, it passes my simple check. I'll make it empty for the test to pass if that's the goal. - // Actually, I'll just fix the test to expect success or I'll fix the code. - // Let's make it empty to ensure it fails as expected by the test name. - let result = client.try_register_project( - &owner, - &SorobanString::from_str(&env, ""), - &SorobanString::from_str(&env, "Desc"), - &SorobanString::from_str(&env, "Cat"), - &None, - &None, - &None, - ); - assert_eq!(result, Err(Ok(Error::InvalidProjectData))); -} - -#[test] -fn test_validation_invalid_description_empty() { - let env = Env::default(); - let (client, _, owner) = setup(&env); - let result = client.try_register_project( - &owner, - &SorobanString::from_str(&env, "Name"), - &SorobanString::from_str(&env, ""), - &SorobanString::from_str(&env, "Cat"), - &None, - &None, - &None, - ); - assert_eq!(result, Err(Ok(Error::ProjectDescriptionTooLong))); -} - -#[test] -fn test_validation_invalid_category_empty() { - let env = Env::default(); - let (client, _, owner) = setup(&env); - let result = client.try_register_project( - &owner, - &SorobanString::from_str(&env, "Name"), - &SorobanString::from_str(&env, "Description long enough"), - &SorobanString::from_str(&env, ""), - &None, - &None, - &None, - ); - assert_eq!(result, Err(Ok(Error::InvalidProjectCategory))); -} - -#[test] -fn test_update_project_not_owner_reverts() { - let env = Env::default(); - let (client, _, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let other = Address::generate(&env); - let result = client.try_update_project( - &id, - &other, - &None, - &None, - &None, - &None, - &None, - &None, - ); - assert_eq!(result, Err(Ok(Error::Unauthorized))); + let (client, _) = setup(&env); + let owner = Address::generate(&env); + let result = client.try_register_project(&ProjectRegistrationParams { + owner: owner.clone(), + name: SorobanString::from_str(&env, ""), + description: SorobanString::from_str(&env, "Desc"), + category: SorobanString::from_str(&env, "Cat"), + website: None, + logo_cid: None, + metadata_cid: None, + }); + assert_eq!(result, Err(Ok(ContractError::InvalidProjectData))); } -*/ #[test] fn test_get_project_invalid_id_zero() { let env = Env::default(); - let (client, _, _) = setup(&env); + let (client, _) = setup(&env); let result = client.try_get_project(&0); - assert!(result.is_ok()); - assert!(result.unwrap().unwrap().is_none()); + assert!(result.is_err()); } -/* -#[test] -fn test_max_projects_per_user_limit() { - let env = Env::default(); - let (client, _, owner) = setup(&env); - let name = "Project".to_string(); - let desc = "Description".to_string(); - let cat = "DeFi".to_string(); - for i in 0..MAX_PROJECTS_PER_USER { - let n = format!("{} {}", name, i); - let id = client.register_project( - &owner, - &n, - &desc, - &cat, - &None, - &None, - &None, - ); - assert!(id > 0); - } - assert_eq!(client.get_owner_project_count(&owner), MAX_PROJECTS_PER_USER); - let result = client.try_register_project( - &owner, - &SorobanString::from_str(&env, "One more"), - &SorobanString::from_str(&env, &desc), - &SorobanString::from_str(&env, &cat), - &None, - &None, - &None, - ); - // My Implementation doesn't enforce MAX_PROJECTS_PER_USER yet, so skip or fix - // assert_eq!(result, Err(Ok(Error::MaxProjectsPerUserExceeded))); -} -*/ - -/* #[test] fn test_add_review_invalid_rating_zero() { let env = Env::default(); - let (client, _, owner) = setup(&env); + let (client, _) = setup(&env); + let owner = Address::generate(&env); let id = register_one_project(&env, &client, &owner); let reviewer = Address::generate(&env); let result = client.try_add_review(&id, &reviewer, &0u32, &None); - assert_eq!(result, Err(Ok(Error::InvalidRating))); + assert_eq!(result, Err(Ok(ContractError::InvalidRating))); } #[test] fn test_add_review_invalid_rating_six() { let env = Env::default(); - let (client, _, owner) = setup(&env); + let (client, _) = setup(&env); + let owner = Address::generate(&env); let id = register_one_project(&env, &client, &owner); let reviewer = Address::generate(&env); let result = client.try_add_review(&id, &reviewer, &6u32, &None); - assert_eq!(result, Err(Ok(Error::InvalidRating))); -} - -#[test] -fn test_add_review_valid_rating_one_to_five() { - let env = Env::default(); - let (client, _, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let reviewer = Address::generate(&env); - for r in 1u32..=5 { - let result = client.try_add_review(&id, &reviewer, &r, &None); - if r == 1 { - assert!(result.is_ok(), "first review should succeed"); - } else { - assert_eq!(result, Err(Ok(Error::DuplicateReview)), "second review same reviewer"); - } - } + assert_eq!(result, Err(Ok(ContractError::InvalidRating))); } #[test] fn test_duplicate_review_same_reviewer_reverts() { let env = Env::default(); - let (client, _, owner) = setup(&env); + let (client, _) = setup(&env); + let owner = Address::generate(&env); let id = register_one_project(&env, &client, &owner); let reviewer = Address::generate(&env); client.add_review(&id, &reviewer, &5u32, &None); let result = client.try_add_review(&id, &reviewer, &4u32, &None); - assert_eq!(result, Err(Ok(Error::DuplicateReview))); -} - -#[test] -fn test_update_review_not_author_reverts() { - let env = Env::default(); - let (client, _, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let reviewer = Address::generate(&env); - client.add_review(&id, &reviewer, &5u32, &None); - let other = Address::generate(&env); - let result = client.try_update_review(&id, &other, &3u32, &None); - assert_eq!(result, Err(Ok(Error::ReviewNotFound))); -} -*/ - -/* -#[test] -fn test_request_verification_without_fee_reverts() { - let env = Env::default(); - // client.set_fee(&admin, &None, &100, &treasury); - let result = client.try_request_verification(&id, &owner, &SorobanString::from_str(&env, "evidence_cid")); - // assert_eq!(result, Err(Ok(Error::FeeNotPaid))); -} -*/ - -/* -#[test] -fn test_request_verification_not_owner_reverts() { - let env = Env::default(); - let (client, admin, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let treasury = Address::generate(&env); - client.set_fee(&admin, &None, &100, &treasury); - client.pay_fee(&owner, &id, &None); - let other = Address::generate(&env); - let result = client.try_request_verification(&id, &other, &"evidence_cid".into()); - assert_eq!(result, Err(Ok(Error::NotProjectOwnerForVerification))); -} - -#[test] -fn test_request_verification_invalid_evidence_empty_reverts() { - let env = Env::default(); - let (client, admin, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let treasury = Address::generate(&env); - client.set_fee(&admin, &None, &100, &treasury); - client.pay_fee(&owner, &id, &None); - let result = client.try_request_verification(&id, &owner, &SorobanString::from_str(&env, "")); - // assert_eq!(result, Err(Ok(Error::InvalidEvidenceCid))); -} - -#[test] -fn test_approve_verification_unauthorized_reverts() { - let env = Env::default(); - let (client, admin, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let treasury = Address::generate(&env); - client.set_fee(&admin, &None, &100, &treasury); - client.pay_fee(&owner, &id, &None); - client.request_verification(&id, &owner, &"evidence".into()); - let non_admin = Address::generate(&env); - let result = client.try_approve_verification(&id, &non_admin); - assert_eq!(result, Err(Ok(Error::UnauthorizedVerifier))); -} - -#[test] -fn test_verification_flow_approve() { - let env = Env::default(); - let (client, admin, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let treasury = Address::generate(&env); - client.set_fee(&admin, &None, &100, &treasury); - client.pay_fee(&owner, &id, &None); - client.request_verification(&id, &owner, &"evidence".into()); - client.approve_verification(&id, &admin); - let rec = client.get_verification(&id).expect("verification record"); - assert_eq!(rec.status, VerificationStatus::Verified); -} - -#[test] -fn test_verification_flow_reject() { - let env = Env::default(); - let (client, admin, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let treasury = Address::generate(&env); - client.set_fee(&admin, &None, &100, &treasury); - client.pay_fee(&owner, &id, &None); - client.request_verification(&id, &owner, &"evidence".into()); - client.reject_verification(&id, &admin); - let rec = client.get_verification(&id).expect("verification record"); - assert_eq!(rec.status, VerificationStatus::Rejected); -} -*/ - -/* -#[test] -fn test_set_fee_unauthorized_reverts() { - let env = Env::default(); - let (client, admin, _) = setup(&env); - let treasury = Address::generate(&env); - let non_admin = Address::generate(&env); - let result = client.try_set_fee(&non_admin, &None, &100, &treasury); - assert_eq!(result, Err(Ok(Error::UnauthorizedAdmin))); - client.set_fee(&admin, &None, &100, &treasury); + assert_eq!(result, Err(Ok(ContractError::AlreadyReviewed))); } #[test] -fn test_set_fee_zero_amount_reverts() { +fn test_get_fee_config_after_set() { let env = Env::default(); - let (client, admin, _) = setup(&env); + let (client, admin) = setup(&env); let treasury = Address::generate(&env); - let result = client.try_set_fee(&admin, &None, &0, &treasury); - assert_eq!(result, Err(Ok(Error::InvalidFeeAmount))); -} - -#[test] -fn test_pay_fee_before_config_reverts() { - let env = Env::default(); - let (client, _, owner) = setup(&env); - let id = register_one_project(&env, &client, &owner); - let result = client.try_pay_fee(&owner, &id, &None); - assert_eq!(result, Err(Ok(Error::FeeNotConfigured))); + client.set_fee(&admin, &None, &500, &treasury); + let config: FeeConfig = client.get_fee_config(); + assert_eq!(config.verification_fee, 500); + assert_eq!(config.token, None); } -*/ #[test] fn test_get_project_none_for_nonexistent_id() { let env = Env::default(); - let (client, _, _) = setup(&env); - let project = client.get_project(&999); - assert!(project.is_none()); + let (client, _) = setup(&env); + let result = client.try_get_project(&999); + assert!(result.is_err()); } -/* #[test] fn test_multiple_concurrent_registrations_same_user() { let env = Env::default(); - let (client, _, owner) = setup(&env); + let (client, _) = setup(&env); + let owner = Address::generate(&env); let mut ids = Vec::new(&env); - for i in 0..5 { - let n = SorobanString::from_str(&env, &format!("Project {}", i)); - let d = SorobanString::from_str(&env, "Description long enough to pass validation characters..."); - let c = SorobanString::from_str(&env, "Cat"); - let id = client.register_project( - &owner, - &n, - &d, - &c, - &None, - &None, - &None, - ); + for _ in 0..5 { + let id = client.register_project(&ProjectRegistrationParams { + owner: owner.clone(), + name: SorobanString::from_str(&env, "Project"), + description: SorobanString::from_str(&env, "Desc"), + category: SorobanString::from_str(&env, "Cat"), + website: None, + logo_cid: None, + metadata_cid: None, + }); ids.push_back(id); } - assert_eq!(ids, soroban_sdk::vec![&env, 1, 2, 3, 4, 5]); + assert_eq!(ids, Vec::from_array(&env, [1, 2, 3, 4, 5])); assert_eq!(client.get_owner_project_count(&owner), 5); } -*/ -/* #[test] -fn test_get_fee_config_after_set() { - let env = Env::default(); - let (client, admin, _) = setup(&env); - let treasury = Address::generate(&env); - client.set_fee(&admin, &None, &500, &treasury); - let config: FeeConfig = client.get_fee_config(); - assert_eq!(config.verification_fee, 0); // Default is 0 in my current get_fee_config - // assert_eq!(config.treasury, treasury); -} -*/ -#[test] -fn test_list_projects() { +fn test_admin_can_be_added_and_queried() { let env = Env::default(); - let (client, _, owner) = setup(&env); - - // Register 10 projects - for _i in 1..=10 { - let name = SorobanString::from_str(&env, "Project"); - client.mock_all_auths().register_project( - &owner, - &name, - &SorobanString::from_str(&env, "Description that is long enough to pass validation definitely more than two hundred characters... Description that is long enough to pass validation definitely more than two hundred characters..."), - &SorobanString::from_str(&env, "Category"), - &None, - &None, - &None, - ); - } - - // List first 5 - let first_five = client.list_projects(&1, &5); - assert_eq!(first_five.len(), 5); - assert_eq!(first_five.get(0).unwrap().id, 1); - assert_eq!(first_five.get(4).unwrap().id, 5); - - // List next 5 - let next_five = client.list_projects(&6, &5); - assert_eq!(next_five.len(), 5); - assert_eq!(next_five.get(0).unwrap().id, 6); - assert_eq!(next_five.get(4).unwrap().id, 10); - - // List beyond total - let beyond = client.list_projects(&11, &5); - assert_eq!(beyond.len(), 0); + let (client, admin) = setup(&env); + let new_admin = Address::generate(&env); + client.add_admin(&admin, &new_admin); + assert!(client.is_admin(&new_admin)); + assert_eq!(client.get_admin_count(), 2); } diff --git a/dongle-smartcontract/src/types.rs b/dongle-smartcontract/src/types.rs index c7d94d7..0ce8ed4 100644 --- a/dongle-smartcontract/src/types.rs +++ b/dongle-smartcontract/src/types.rs @@ -1,40 +1,68 @@ use soroban_sdk::{contracttype, Address, String}; +// STORAGE COMPATIBILITY NOTE: +// The addition of `created_at` and `updated_at` fields to `Review` changes the +// serialized form of the struct. Existing stored data (if any) will not deserialize +// correctly. Migration strategy: This is a fresh/development project deployment. +// No migration function is needed. + +// ========================================== +// Storage Keys +// (These are now in storage_keys.rs, kept here for DataKey alias) +// ========================================== + +/// Top-level entry-point type used in `types.rs` (for backward compat or upstream compat) #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub struct Review { - pub project_id: u64, - pub reviewer: Address, - pub rating: u32, - pub timestamp: u64, - pub comment_cid: Option, - pub is_deleted: bool, +pub enum DataKey { + Project(u64), + ProjectCount, + OwnerProjects(Address), + Review(u64, Address), + UserReviews(Address), + Verification(u64), + NextProjectId, + Admin(Address), + FeeConfig, + Treasury, + ProjectStats(u64), + FeePaidForProject(u64), } #[contracttype] -#[derive(Clone, Debug, Eq, PartialEq, Default)] -pub struct ProjectStats { - pub rating_sum: u64, - pub review_count: u32, - pub average_rating: u32, +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum VerificationStatus { + Unverified, + Pending, + Verified, + Rejected, } +/// Parameters for registering a new project #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub enum ReviewAction { - Submitted, - Updated, - Deleted, +pub struct ProjectRegistrationParams { + pub owner: Address, + pub name: String, + pub description: String, + pub category: String, + pub website: Option, + pub logo_cid: Option, + pub metadata_cid: Option, } +/// Parameters for updating an existing project #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub struct ReviewEventData { +pub struct ProjectUpdateParams { pub project_id: u64, - pub reviewer: Address, - pub action: ReviewAction, - pub timestamp: u64, - pub comment_cid: Option, + pub caller: Address, + pub name: Option, + pub description: Option, + pub category: Option, + pub website: Option>, + pub logo_cid: Option>, + pub metadata_cid: Option>, } #[contracttype] @@ -54,33 +82,49 @@ pub struct Project { } #[contracttype] -pub enum DataKey { - Project(u64), - ProjectCount, - OwnerProjects(Address), - Review(u64, Address), - UserReviews(Address), - Verification(u64), - NextProjectId, - Admin(Address), - FeeConfig, - Treasury, - ProjectStats(u64), +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Review { + pub project_id: u64, + pub reviewer: Address, + pub rating: u32, + pub comment_cid: Option, + + /// Unix timestamp (seconds) when the review was first submitted. + /// Set once at creation and never modified — forms the immutable audit record. + pub created_at: u64, + + /// Unix timestamp (seconds) of the most recent modification to this review. + /// Updated on every edit action. Equals `created_at` on initial submission. + pub updated_at: u64, } #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] -pub enum VerificationStatus { - Unverified, - Pending, - Verified, - Rejected, +pub enum ReviewAction { + Submitted, + Updated, + Deleted, +} + +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ReviewEventData { + pub project_id: u64, + pub reviewer: Address, + pub action: ReviewAction, + pub timestamp: u64, + pub comment_cid: Option, } #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct VerificationRecord { + pub project_id: u64, + pub requester: Address, pub status: VerificationStatus, + pub evidence_cid: String, + pub timestamp: u64, + pub fee_amount: u128, } /// Fee configuration for contract operations @@ -91,3 +135,28 @@ pub struct FeeConfig { pub verification_fee: u128, pub registration_fee: u128, } + +#[contracttype] +#[derive(Clone, Debug, Default)] +pub struct ProjectAggregate { + pub total_rating: u64, + pub review_count: u64, +} + +/// Summary returned by get_project with computed average rating +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProjectSummary { + pub id: u64, + pub owner: Address, + pub name: String, + pub description: String, + pub category: String, + pub website: Option, + pub logo_cid: Option, + pub metadata_cid: Option, + pub verification_status: VerificationStatus, + pub created_at: u64, + pub updated_at: u64, + pub average_rating: u32, +} diff --git a/dongle-smartcontract/src/utils.rs b/dongle-smartcontract/src/utils.rs index 31a51a1..ba345aa 100644 --- a/dongle-smartcontract/src/utils.rs +++ b/dongle-smartcontract/src/utils.rs @@ -1,48 +1,34 @@ use crate::errors::ContractError; -use crate::types::DataKey; +use crate::storage_keys::StorageKey; use soroban_sdk::{Address, Env, String}; +#[allow(dead_code)] pub struct Utils; +#[allow(dead_code)] impl Utils { - pub fn get_current_timestamp(_env: &Env) -> u64 { - 0 + pub fn get_current_timestamp(env: &Env) -> u64 { + env.ledger().timestamp() } - pub fn is_admin(_env: &Env, _address: &Address) -> bool { - false + pub fn is_admin(env: &Env, address: &Address) -> bool { + crate::admin_manager::AdminManager::is_admin(env, address) } - pub fn add_admin( - _env: &Env, - _caller: &Address, - _new_admin: &Address, - ) -> Result<(), ContractError> { - todo!("Admin addition logic not implemented") - } - - pub fn remove_admin( - _env: &Env, - _caller: &Address, - _admin_to_remove: &Address, - ) -> Result<(), ContractError> { - todo!("Admin removal logic not implemented") + pub fn require_admin(env: &Env, address: &Address) -> Result<(), ContractError> { + crate::admin_manager::AdminManager::require_admin(env, address) } pub fn validate_string_length( value: &String, min_length: u32, max_length: u32, - field_name: &str, + _field_name: &str, ) -> Result<(), ContractError> { let length = value.len(); if length < min_length || length > max_length { - match field_name { - "name" => Err(ContractError::InvalidProjectData), - "description" => Err(ContractError::InvalidProjectData), - _ => Err(ContractError::InvalidProjectData), - } + Err(ContractError::InvalidProjectData) } else { Ok(()) } @@ -57,10 +43,6 @@ impl Utils { true } - pub fn get_storage_key(data_key: DataKey) -> DataKey { - data_key - } - pub fn sanitize_string(input: &String) -> String { input.clone() } @@ -69,10 +51,6 @@ impl Utils { true } - pub fn create_event_data(_event_type: &str, _data: &str) -> String { - todo!("Event data creation needs Env parameter for Soroban String construction") - } - pub fn validate_pagination(_start_id: u64, limit: u32) -> Result<(), ContractError> { const MAX_LIMIT: u32 = 100; diff --git a/dongle-smartcontract/src/verification_registry.rs b/dongle-smartcontract/src/verification_registry.rs index 622779d..61caded 100644 --- a/dongle-smartcontract/src/verification_registry.rs +++ b/dongle-smartcontract/src/verification_registry.rs @@ -1,12 +1,12 @@ //! Verification requests with ownership and fee checks, and events. +use crate::admin_manager::AdminManager; use crate::errors::ContractError; -use crate::events::{ - publish_verification_approved_event, publish_verification_rejected_event, - publish_verification_requested_event, -}; -use crate::types::{DataKey, VerificationRecord, VerificationStatus}; -use soroban_sdk::{Address, Env, String, Vec}; +use crate::fee_manager::FeeManager; +use crate::project_registry::ProjectRegistry; +use crate::storage_keys::StorageKey; +use crate::types::{VerificationRecord, VerificationStatus}; +use soroban_sdk::{Address, Env, String}; pub struct VerificationRegistry; @@ -16,55 +16,132 @@ impl VerificationRegistry { project_id: u64, requester: Address, evidence_cid: String, - ) { - // Validate project ownership - // Require fee paid via FeeManager - // Store VerificationRecord with Pending + ) -> Result<(), ContractError> { + requester.require_auth(); + + // Validate project existence and ownership + let project = ProjectRegistry::get_project(env, project_id) + .ok_or(ContractError::ProjectNotFound)?; + + if project.owner != requester { + return Err(ContractError::Unauthorized); + } + + // Validate evidence CID + Self::validate_evidence_cid(&evidence_cid)?; + + // Check fee config and pay verification fee + let fee_config = FeeManager::get_fee_config(env)?; + let fee_paid = env + .storage() + .persistent() + .get(&StorageKey::FeePaidForProject(project_id)) + .unwrap_or(false); + if fee_config.verification_fee > 0 && !fee_paid { + return Err(ContractError::InsufficientFee); + } + + let record = VerificationRecord { + project_id, + requester, + status: VerificationStatus::Pending, + evidence_cid, + timestamp: env.ledger().timestamp(), + fee_amount: fee_config.verification_fee, + }; + + env.storage() + .persistent() + .set(&StorageKey::Verification(project_id), &record); + + Ok(()) } pub fn approve_verification( - _env: &Env, - _project_id: u64, - _admin: Address, + env: &Env, + project_id: u64, + admin: Address, ) -> Result<(), ContractError> { - todo!("Verification approval logic not implemented") + admin.require_auth(); + + if !AdminManager::is_admin(env, &admin) { + return Err(ContractError::AdminOnly); + } + + let mut record = Self::get_verification(env, project_id)?; + + if record.status != VerificationStatus::Pending { + return Err(ContractError::InvalidStatusTransition); + } + + record.status = VerificationStatus::Verified; + env.storage() + .persistent() + .set(&StorageKey::Verification(project_id), &record); + + // Also update the project's verification status + if let Some(mut project) = ProjectRegistry::get_project(env, project_id) { + project.verification_status = VerificationStatus::Verified; + project.updated_at = env.ledger().timestamp(); + env.storage() + .persistent() + .set(&StorageKey::Project(project_id), &project); + } + + Ok(()) } pub fn reject_verification( - _env: &Env, - _project_id: u64, - _admin: Address, + env: &Env, + project_id: u64, + admin: Address, ) -> Result<(), ContractError> { - todo!("Verification rejection logic not implemented") + admin.require_auth(); + + if !AdminManager::is_admin(env, &admin) { + return Err(ContractError::AdminOnly); + } + + let mut record = Self::get_verification(env, project_id)?; + + if record.status != VerificationStatus::Pending { + return Err(ContractError::InvalidStatusTransition); + } + + record.status = VerificationStatus::Rejected; + env.storage() + .persistent() + .set(&StorageKey::Verification(project_id), &record); + + Ok(()) } pub fn get_verification( - _env: &Env, - _project_id: u64, + env: &Env, + project_id: u64, ) -> Result { - todo!("Verification record retrieval logic not implemented") - } - - pub fn list_pending_verifications( - _env: &Env, - _admin: Address, - _start_project_id: u64, - _limit: u32, - ) -> Result, ContractError> { - todo!("Pending verification listing logic not implemented") + env.storage() + .persistent() + .get(&StorageKey::Verification(project_id)) + .ok_or(ContractError::VerificationNotFound) } - pub fn verification_exists(_env: &Env, _project_id: u64) -> bool { - false + pub fn verification_exists(env: &Env, project_id: u64) -> bool { + env.storage() + .persistent() + .has(&StorageKey::Verification(project_id)) } + #[allow(dead_code)] pub fn get_verification_status( - _env: &Env, - _project_id: u64, + env: &Env, + project_id: u64, ) -> Result { - todo!("Verification status retrieval not implemented") + let record = Self::get_verification(env, project_id)?; + Ok(record.status) } + #[allow(dead_code)] pub fn update_verification_evidence( _env: &Env, _project_id: u64, @@ -74,13 +151,15 @@ impl VerificationRegistry { todo!("Verification evidence update logic not implemented") } + #[allow(dead_code)] pub fn validate_evidence_cid(evidence_cid: &String) -> Result<(), ContractError> { - if evidence_cid.is_empty() { + if evidence_cid.len() == 0 { return Err(ContractError::InvalidProjectData); } Ok(()) } + #[allow(dead_code)] pub fn get_verification_stats(_env: &Env) -> (u32, u32, u32) { (0, 0, 0) } diff --git a/dongle-smartcontract/target/.rustc_info.json b/dongle-smartcontract/target/.rustc_info.json deleted file mode 100644 index 4449910..0000000 --- a/dongle-smartcontract/target/.rustc_info.json +++ /dev/null @@ -1 +0,0 @@ -{"rustc_fingerprint":8360369028972503107,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/nayy/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.91.1 (ed61e7d7e 2025-11-07)\nbinary: rustc\ncommit-hash: ed61e7d7e242494fb7057f2657300d9e77bb4fcb\ncommit-date: 2025-11-07\nhost: aarch64-apple-darwin\nrelease: 1.91.1\nLLVM version: 21.1.2\n","stderr":""},"6432102384495711296":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/nayy/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}} \ No newline at end of file