From d6f0e4d9a406f2b9c271feb4cd87c77a07806123 Mon Sep 17 00:00:00 2001 From: Amas-01 Date: Fri, 27 Feb 2026 20:18:49 +0100 Subject: [PATCH] feat:fixed failing CI test builds --- dongle-smartcontract/src/admin_manager.rs | 13 +- dongle-smartcontract/src/errors.rs | 41 ++- dongle-smartcontract/src/events.rs | 5 +- dongle-smartcontract/src/fee_manager.rs | 79 ++---- dongle-smartcontract/src/lib.rs | 4 +- dongle-smartcontract/src/project_registry.rs | 2 +- .../src/registration_tests.rs | 8 +- dongle-smartcontract/src/review_registry.rs | 30 ++- dongle-smartcontract/src/test.rs | 26 +- dongle-smartcontract/src/types.rs | 10 +- .../src/verification_registry.rs | 235 +++++------------- .../src/verification_tests.rs | 13 +- .../test/test_add_review_event.1.json | 123 +++++---- 13 files changed, 250 insertions(+), 339 deletions(-) diff --git a/dongle-smartcontract/src/admin_manager.rs b/dongle-smartcontract/src/admin_manager.rs index bd04b99..ed11d2b 100644 --- a/dongle-smartcontract/src/admin_manager.rs +++ b/dongle-smartcontract/src/admin_manager.rs @@ -137,6 +137,7 @@ impl AdminManager { mod tests { use crate::DongleContract; use crate::DongleContractClient; + use crate::errors::ContractError; use soroban_sdk::{testutils::Address as _, Address, Env}; #[test] @@ -177,9 +178,9 @@ mod tests { let new_admin = Address::generate(&env); client.mock_all_auths().initialize(&admin); - client.mock_all_auths().add_admin(&non_admin, &new_admin); + let result = client.mock_all_auths().try_add_admin(&non_admin, &new_admin); - // The new admin should not be added + assert_eq!(result, Err(Ok(ContractError::AdminOnly))); assert!(!client.is_admin(&new_admin)); } @@ -207,9 +208,9 @@ mod tests { let admin = Address::generate(&env); client.mock_all_auths().initialize(&admin); - client.mock_all_auths().remove_admin(&admin, &admin); + let result = client.mock_all_auths().try_remove_admin(&admin, &admin); - // Should still be admin + assert_eq!(result, Err(Ok(ContractError::CannotRemoveLastAdmin))); assert!(client.is_admin(&admin)); } @@ -224,9 +225,9 @@ mod tests { client.mock_all_auths().initialize(&admin); client.mock_all_auths().add_admin(&admin, &another_admin); - client.mock_all_auths().remove_admin(&admin, &non_admin); + let result = client.mock_all_auths().try_remove_admin(&admin, &non_admin); - // another_admin should still be there + assert_eq!(result, Err(Ok(ContractError::AdminNotFound))); assert!(client.is_admin(&another_admin)); } diff --git a/dongle-smartcontract/src/errors.rs b/dongle-smartcontract/src/errors.rs index 43640a3..d627582 100644 --- a/dongle-smartcontract/src/errors.rs +++ b/dongle-smartcontract/src/errors.rs @@ -1,70 +1,65 @@ - use soroban_sdk::contracterror; +use soroban_sdk::contracterror; /// Error types for the Dongle smart contract #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum ContractError { - AlreadyReviewed = 19 /// Project not found ProjectNotFound = 1, - /// Unauthorized access - caller is not the owner Unauthorized = 2, - /// Project already exists ProjectAlreadyExists = 3, - /// Invalid rating - must be between 1 and 5 InvalidRating = 4, - /// Review not found ReviewNotFound = 5, - /// Review already exists ReviewAlreadyExists = 6, - /// Verification record not found VerificationNotFound = 7, - /// Invalid verification status transition InvalidStatusTransition = 8, - /// Only admin can perform this action AdminOnly = 9, - /// Invalid fee amount InvalidFeeAmount = 10, - /// Insufficient fee paid InsufficientFee = 11, - /// Invalid project data - missing required fields InvalidProjectData = 12, - /// Project name too long ProjectNameTooLong = 13, - /// Project description too long ProjectDescriptionTooLong = 14, - /// Invalid project category InvalidProjectCategory = 15, - /// Verification already processed VerificationAlreadyProcessed = 16, - /// Cannot review own project CannotReviewOwnProject = 17, - /// Fee configuration not set FeeConfigNotSet = 18, - /// Treasury address not set TreasuryNotSet = 19, - /// Review already deleted ReviewAlreadyDeleted = 20, - NotReviewer = 22, + /// Invalid project name format InvalidProjectNameFormat = 21, -} \ No newline at end of file + /// Not the reviewer + NotReviewer = 22, + /// Already reviewed + AlreadyReviewed = 23, + /// Cannot remove last admin + CannotRemoveLastAdmin = 24, + /// Admin not found + AdminNotFound = 25, + /// Record not found (generic) + RecordNotFound = 26, + /// Duplicate review error variant (sometimes used separately) + DuplicateReview = 27, +} + +// 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 d35b6ff..ce04093 100644 --- a/dongle-smartcontract/src/events.rs +++ b/dongle-smartcontract/src/events.rs @@ -9,12 +9,15 @@ pub fn publish_review_event( reviewer: Address, action: ReviewAction, comment_cid: Option, + created_at: u64, + updated_at: u64, ) { let event_data = ReviewEventData { project_id, reviewer: reviewer.clone(), action: action.clone(), - timestamp: env.ledger().timestamp(), + created_at, + updated_at, comment_cid, }; diff --git a/dongle-smartcontract/src/fee_manager.rs b/dongle-smartcontract/src/fee_manager.rs index 1125c69..1e16f58 100644 --- a/dongle-smartcontract/src/fee_manager.rs +++ b/dongle-smartcontract/src/fee_manager.rs @@ -9,8 +9,8 @@ use soroban_sdk::{Address, Env}; pub struct FeeManager; -#[allow(dead_code)] impl FeeManager { + /// Configure fees for the contract (admin only) pub fn set_fee( env: &Env, admin: Address, @@ -19,21 +19,8 @@ impl FeeManager { treasury: Address, ) -> Result<(), ContractError> { admin.require_auth(); - - // Verify admin privileges AdminManager::require_admin(env, &admin)?; - // Authorization check - let stored_admin: Address = env - .storage() - .persistent() - .get(&StorageKey::Admin) - .ok_or(ContractError::Unauthorized)?; - if admin != stored_admin { - return Err(ContractError::Unauthorized); - } - admin.require_auth(); - let config = FeeConfig { token, verification_fee: amount, @@ -45,16 +32,17 @@ impl FeeManager { env.storage() .persistent() .set(&StorageKey::Treasury, &treasury); + publish_fee_set_event(env, amount, 0); Ok(()) } + /// Pay the verification fee for a project pub fn pay_fee( env: &Env, payer: Address, project_id: u64, - - token: Option
, + token: Option
, ) -> Result<(), ContractError> { payer.require_auth(); @@ -62,7 +50,7 @@ impl FeeManager { let treasury: Address = env .storage() .persistent() - .get(&DataKey::Treasury) + .get(&StorageKey::Treasury) .ok_or(ContractError::TreasuryNotSet)?; if config.token != token { @@ -71,55 +59,39 @@ impl FeeManager { let amount = config.verification_fee; if amount > 0 { - if let Some(token_address) = config.token { - let client = soroban_sdk::token::Client::new(env, &token_address); - client.transfer(&payer, &treasury, &(amount as i128)); - } else { - // For native token, we use the same token client since it's standardized - // Assuming the contract environment has access to the native asset if token is None - // In Soroban, native asset is also a token. - // However, the FeeConfig doesn't store the native asset address if None. - // We'll require the caller to pass the correct token address if it's not None. - // If config.token is None, it means the contract isn't fully configured for native payments yet - // or we need a standard way to get the native asset address. - return Err(ContractError::FeeConfigNotSet); - } + let token_address = config.token.ok_or(ContractError::FeeConfigNotSet)?; + let client = soroban_sdk::token::Client::new(env, &token_address); + client.transfer(&payer, &treasury, &(amount as i128)); } env.storage() .persistent() - .set(&DataKey::FeePaidForProject(project_id), &true); - - crate::events::publish_fee_paid_event(env, project_id, amount); + .set(&StorageKey::FeePaidForProject(project_id), &true); + publish_fee_paid_event(env, project_id, amount); Ok(()) } + /// Check if the fee has been paid for a project pub fn is_fee_paid(env: &Env, project_id: u64) -> bool { env.storage() .persistent() - .get(&DataKey::FeePaidForProject(project_id)) + .get(&StorageKey::FeePaidForProject(project_id)) .unwrap_or(false) } + /// Consume the fee payment (used during verification request) pub fn consume_fee_payment(env: &Env, project_id: u64) -> Result<(), ContractError> { if !Self::is_fee_paid(env, project_id) { return Err(ContractError::InsufficientFee); } env.storage() .persistent() - .remove(&DataKey::FeePaidForProject(project_id)); - Ok(()) - } - - env.storage() - .persistent() - .set(&StorageKey::FeePaidForProject(project_id), &true); - - publish_fee_paid_event(env, project_id, config.verification_fee); + .remove(&StorageKey::FeePaidForProject(project_id)); Ok(()) } + /// Get current fee configuration pub fn get_fee_config(env: &Env) -> Result { env.storage() .persistent() @@ -127,16 +99,10 @@ impl FeeManager { .ok_or(ContractError::FeeConfigNotSet) } + /// Set the treasury address (admin only) pub fn set_treasury(env: &Env, admin: Address, treasury: Address) -> Result<(), ContractError> { - let stored_admin: Address = env - .storage() - .persistent() - .get(&StorageKey::Admin) - .ok_or(ContractError::Unauthorized)?; - if admin != stored_admin { - return Err(ContractError::Unauthorized); - } admin.require_auth(); + AdminManager::require_admin(env, &admin)?; env.storage() .persistent() @@ -144,20 +110,15 @@ impl FeeManager { Ok(()) } + /// Get the current treasury address pub fn get_treasury(env: &Env) -> Result { env.storage() .persistent() .get(&StorageKey::Treasury) - .ok_or(ContractError::FeeConfigNotSet) // Reusing error or could use a new one - } - - pub fn is_fee_paid(env: &Env, project_id: u64) -> bool { - env.storage() - .persistent() - .get(&StorageKey::FeePaidForProject(project_id)) - .unwrap_or(false) + .ok_or(ContractError::TreasuryNotSet) } + /// Get fee for a specific operation #[allow(dead_code)] pub fn get_operation_fee(env: &Env, operation_type: &str) -> Result { let config = Self::get_fee_config(env)?; diff --git a/dongle-smartcontract/src/lib.rs b/dongle-smartcontract/src/lib.rs index 95c79c2..f376520 100644 --- a/dongle-smartcontract/src/lib.rs +++ b/dongle-smartcontract/src/lib.rs @@ -2,7 +2,7 @@ mod admin_manager; mod constants; -mod errors; +pub mod errors; mod events; mod fee_manager; mod project_registry; @@ -10,7 +10,7 @@ mod rating_calculator; mod review_registry; mod storage_keys; mod verification_registry; -mod types; +pub mod types; #[cfg(test)] mod test; diff --git a/dongle-smartcontract/src/project_registry.rs b/dongle-smartcontract/src/project_registry.rs index fcf9031..173c9e5 100644 --- a/dongle-smartcontract/src/project_registry.rs +++ b/dongle-smartcontract/src/project_registry.rs @@ -145,7 +145,7 @@ impl ProjectRegistry { env.storage() .persistent() .get(&StorageKey::OwnerProjects(owner.clone())) - .unwrap_or_else(|| Vec::new(env)) + .unwrap_or_else(|| Vec::::new(env)) .len() } diff --git a/dongle-smartcontract/src/registration_tests.rs b/dongle-smartcontract/src/registration_tests.rs index ccbbe6e..5a46e10 100644 --- a/dongle-smartcontract/src/registration_tests.rs +++ b/dongle-smartcontract/src/registration_tests.rs @@ -31,7 +31,7 @@ fn test_register_project_success() { metadata_cid: None, }; - let id = client.register_project(¶ms).unwrap(); + let id = client.register_project(¶ms); assert_eq!(id, 1); @@ -60,7 +60,7 @@ fn test_register_duplicate_project_fails() { }; // Register first project - client.register_project(¶ms).unwrap(); + client.register_project(¶ms); // Attempt to register another project with the same name let result = client.try_register_project(¶ms); @@ -87,7 +87,7 @@ fn test_register_different_projects_success() { logo_cid: None, metadata_cid: None, }; - let id1 = client.register_project(¶ms1).unwrap(); + let id1 = client.register_project(¶ms1); assert_eq!(id1, 1); let params2 = ProjectRegistrationParams { @@ -99,6 +99,6 @@ fn test_register_different_projects_success() { logo_cid: None, metadata_cid: None, }; - let id2 = client.register_project(¶ms2).unwrap(); + let id2 = client.register_project(¶ms2); assert_eq!(id2, 2); } diff --git a/dongle-smartcontract/src/review_registry.rs b/dongle-smartcontract/src/review_registry.rs index 05a4f9c..5162439 100644 --- a/dongle-smartcontract/src/review_registry.rs +++ b/dongle-smartcontract/src/review_registry.rs @@ -1,4 +1,4 @@ - //! Review submission with validation, duplicate handling, hard-delete, +//! Review submission with validation, duplicate handling, hard-delete, //! user review index, events, and proper aggregate updates. use crate::errors::ContractError; @@ -30,12 +30,14 @@ impl ReviewRegistry { return Err(ContractError::DuplicateReview); } + let now = env.ledger().timestamp(); let review = Review { project_id, reviewer: reviewer.clone(), rating, - timestamp: env.ledger().timestamp(), comment_cid: comment_cid.clone(), + created_at: now, + updated_at: now, }; env.storage().persistent().set(&review_key, &review); @@ -89,6 +91,8 @@ impl ReviewRegistry { reviewer, ReviewAction::Submitted, comment_cid, + now, + now, ); Ok(()) } @@ -114,9 +118,10 @@ impl ReviewRegistry { .ok_or(ContractError::ReviewNotFound)?; let old_rating = review.rating; + let now = env.ledger().timestamp(); review.rating = rating; review.comment_cid = comment_cid.clone(); - review.timestamp = env.ledger().timestamp(); + review.updated_at = now; env.storage().persistent().set(&review_key, &review); // Update aggregate stats @@ -138,6 +143,8 @@ impl ReviewRegistry { reviewer, ReviewAction::Updated, comment_cid, + review.created_at, + now, ); Ok(()) } @@ -215,7 +222,15 @@ impl ReviewRegistry { .persistent() .set(&StorageKey::ProjectReviews(project_id), &new_project_reviews); - publish_review_event(env, project_id, reviewer, ReviewAction::Deleted, None); + publish_review_event( + env, + project_id, + reviewer, + ReviewAction::Deleted, + None, + review.created_at, + env.ledger().timestamp(), + ); Ok(()) } @@ -275,7 +290,7 @@ mod test { use crate::{DongleContract, DongleContractClient}; use soroban_sdk::String as SorobanString; use soroban_sdk::{ - testutils::{Address as _, Events}, + testutils::{Address as _, Events, Ledger}, Env, IntoVal, String, }; @@ -283,6 +298,7 @@ mod test { fn test_add_review_event() { let env = Env::default(); env.mock_all_auths(); + env.ledger().set_timestamp(1_000_000); // Set non-zero timestamp so created_at > 0 let reviewer = Address::generate(&env); let owner = Address::generate(&env); let comment_cid = String::from_str(&env, "QmHash"); @@ -303,7 +319,7 @@ mod test { logo_cid: None, metadata_cid: None, }; - let project_id = client.register_project(¶ms).unwrap(); + let project_id = client.register_project(¶ms); client.add_review(&project_id, &reviewer, &5, &Some(comment_cid.clone())); let events = env.events().all(); @@ -327,5 +343,7 @@ mod test { assert_eq!(event_data.reviewer, reviewer); assert_eq!(event_data.action, ReviewAction::Submitted); assert_eq!(event_data.comment_cid, Some(comment_cid)); + assert!(event_data.created_at > 0); + assert_eq!(event_data.created_at, event_data.updated_at); } } \ No newline at end of file diff --git a/dongle-smartcontract/src/test.rs b/dongle-smartcontract/src/test.rs index a20a5a4..3dceac8 100644 --- a/dongle-smartcontract/src/test.rs +++ b/dongle-smartcontract/src/test.rs @@ -4,6 +4,8 @@ use crate::DongleContract; use crate::DongleContractClient; use soroban_sdk::testutils::Address as _; use soroban_sdk::{Address, Env, String as SorobanString}; +use crate::types::VerificationStatus; +use crate::errors::ContractError; fn setup(env: &Env) -> (DongleContractClient<'_>, Address, Address) { let contract_id = env.register_contract(None, DongleContract); @@ -27,7 +29,7 @@ fn register_one_project(_env: &Env, client: &DongleContractClient, owner: &Addre logo_cid: None, metadata_cid: None, }; - client.mock_all_auths().register_project(¶ms).unwrap() + client.mock_all_auths().register_project(¶ms) } #[test] @@ -77,7 +79,7 @@ fn test_list_projects() { logo_cid: None, metadata_cid: None, }; - client.mock_all_auths().register_project(¶ms).unwrap(); + client.mock_all_auths().register_project(¶ms); } // List first 5 @@ -115,7 +117,7 @@ fn test_verification_workflow() { // 3. Try request without fee let evidence = SorobanString::from_str(&env, "ipfs://evidence"); let res = client.try_request_verification(&project_id, &owner, &evidence); - assert_eq!(res, Err(Ok(Error::InsufficientFee))); + assert_eq!(res, Err(Ok(crate::errors::ContractError::InsufficientFee))); // 4. Pay fee (mocking token balance for payer in tests) // In Soroban tests with mock_all_auths, we often need to actually register the token contract if we want real transfers to work, @@ -143,29 +145,29 @@ fn test_verification_full_flow_success() { // Use a mock token to satisfy the FeeManager requirement let token_admin = Address::generate(&env); - let token_id = env.register_stellar_asset_contract(token_admin); + let token_id = env.register_stellar_asset_contract_v2(token_admin).address(); let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_id); token_client.mint(&owner, &1000); client.set_fee(&admin, &Some(token_id.clone()), &1000, &treasury); // Pay - client.pay_fee(&owner, &project_id, &Some(token_id.clone())).unwrap(); + client.pay_fee(&owner, &project_id, &Some(token_id.clone())); // Request let evidence = SorobanString::from_str(&env, "ipfs://evidence"); - client.request_verification(&project_id, &owner, &evidence).unwrap(); + client.request_verification(&project_id, &owner, &evidence); let project = client.get_project(&project_id).unwrap(); assert_eq!(project.verification_status, VerificationStatus::Pending); // Approve - client.approve_verification(&project_id, &admin).unwrap(); + client.approve_verification(&project_id, &admin); let project_after = client.get_project(&project_id).unwrap(); assert_eq!(project_after.verification_status, VerificationStatus::Verified); - let record = client.get_verification(&project_id).unwrap(); + let record = client.get_verification(&project_id); assert_eq!(record.status, VerificationStatus::Verified); } @@ -179,18 +181,18 @@ fn test_verification_rejection() { let treasury = Address::generate(&env); let token_admin = Address::generate(&env); - let token_id = env.register_stellar_asset_contract(token_admin); + let token_id = env.register_stellar_asset_contract_v2(token_admin).address(); let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_id); token_client.mint(&owner, &500); client.set_fee(&admin, &Some(token_id.clone()), &500, &treasury); - client.pay_fee(&owner, &project_id, &Some(token_id.clone())).unwrap(); + client.pay_fee(&owner, &project_id, &Some(token_id.clone())); let evidence = SorobanString::from_str(&env, "ipfs://evidence"); - client.request_verification(&project_id, &owner, &evidence).unwrap(); + client.request_verification(&project_id, &owner, &evidence); // Reject - client.reject_verification(&project_id, &admin).unwrap(); + client.reject_verification(&project_id, &admin); let project = client.get_project(&project_id).unwrap(); assert_eq!(project.verification_status, VerificationStatus::Rejected); diff --git a/dongle-smartcontract/src/types.rs b/dongle-smartcontract/src/types.rs index 268b45b..d069a5d 100644 --- a/dongle-smartcontract/src/types.rs +++ b/dongle-smartcontract/src/types.rs @@ -39,8 +39,13 @@ pub struct Review { pub project_id: u64, pub reviewer: Address, pub rating: u32, - pub timestamp: u64, pub comment_cid: Option, + + /// Unix timestamp (seconds) when the review was first submitted. + pub created_at: u64, + + /// Unix timestamp (seconds) of the most recent modification to this review. + pub updated_at: u64, } #[contracttype] @@ -57,8 +62,9 @@ pub struct ReviewEventData { pub project_id: u64, pub reviewer: Address, pub action: ReviewAction, - pub timestamp: u64, pub comment_cid: Option, + pub created_at: u64, + pub updated_at: u64, } #[contracttype] diff --git a/dongle-smartcontract/src/verification_registry.rs b/dongle-smartcontract/src/verification_registry.rs index dfcc0d0..1a74e72 100644 --- a/dongle-smartcontract/src/verification_registry.rs +++ b/dongle-smartcontract/src/verification_registry.rs @@ -2,8 +2,6 @@ use crate::admin_manager::AdminManager; use crate::errors::ContractError; -use crate::events::{publish_verification_approved_event, publish_verification_rejected_event}; -use crate::types::{DataKey, VerificationRecord, VerificationStatus}; use crate::events::{ publish_verification_approved_event, publish_verification_rejected_event, publish_verification_requested_event, @@ -16,7 +14,6 @@ use soroban_sdk::{Address, Env, String, Vec}; pub struct VerificationRegistry; -#[allow(dead_code)] impl VerificationRegistry { pub fn request_verification( env: &Env, @@ -26,40 +23,47 @@ impl VerificationRegistry { ) -> Result<(), ContractError> { requester.require_auth(); - // 1. Validate project ownership - let project = crate::project_registry::ProjectRegistry::get_project(env, project_id) + // 1. Validate project existence and ownership + let mut project = ProjectRegistry::get_project(env, project_id) .ok_or(ContractError::ProjectNotFound)?; + if project.owner != requester { return Err(ContractError::Unauthorized); } - // 2. Consume fee payment - crate::fee_manager::FeeManager::consume_fee_payment(env, project_id)?; + // 2. Check if already verified or pending + if project.verification_status != VerificationStatus::Unverified && project.verification_status != VerificationStatus::Rejected { + return Err(ContractError::InvalidStatusTransition); + } + + // 3. Consume fee payment + FeeManager::consume_fee_payment(env, project_id)?; - // 3. Validate evidence + // 4. Validate evidence Self::validate_evidence_cid(&evidence_cid)?; - // 4. Create record - let config = crate::fee_manager::FeeManager::get_fee_config(env)?; + // 5. Create record + let config = FeeManager::get_fee_config(env)?; + let now = env.ledger().timestamp(); let record = VerificationRecord { project_id, requester: requester.clone(), status: VerificationStatus::Pending, evidence_cid: evidence_cid.clone(), - timestamp: env.ledger().timestamp(), + timestamp: now, fee_amount: config.verification_fee, }; env.storage() .persistent() - .set(&DataKey::Verification(project_id), &record); + .set(&StorageKey::Verification(project_id), &record); - // 5. Update project status to Pending - let mut mut_project = project; - mut_project.verification_status = VerificationStatus::Pending; + // 6. Update project status to Pending + project.verification_status = VerificationStatus::Pending; + project.updated_at = now; env.storage() .persistent() - .set(&DataKey::Project(project_id), &mut_project); + .set(&StorageKey::Project(project_id), &project); publish_verification_requested_event(env, project_id, requester, evidence_cid); Ok(()) @@ -71,56 +75,35 @@ impl VerificationRegistry { admin: Address, ) -> Result<(), ContractError> { admin.require_auth(); - - // Verify admin privileges AdminManager::require_admin(env, &admin)?; + // Get project + let mut project = ProjectRegistry::get_project(env, project_id) + .ok_or(ContractError::ProjectNotFound)?; + // Get verification record - let mut record: VerificationRecord = env - .storage() - .persistent() - .get(&DataKey::Verification(project_id)) - .ok_or(ContractError::VerificationNotFound)?; + let mut record = Self::get_verification(env, project_id)?; if record.status != VerificationStatus::Pending { return Err(ContractError::InvalidStatusTransition); } + let now = env.ledger().timestamp(); + + // Update record record.status = VerificationStatus::Verified; env.storage() .persistent() - .set(&DataKey::Verification(project_id), &record); - -// 1. SECURITY: Authorize the admin (From Incoming) -let stored_admin: Address = env - .storage() - .persistent() - .get(&StorageKey::Admin) - .ok_or(ContractError::Unauthorized)?; + .set(&StorageKey::Verification(project_id), &record); -admin.require_auth(); - -// 2. LOGIC: Update the project (Hybrid) -let mut project = crate::project_registry::ProjectRegistry::get_project(env, project_id) - .ok_or(ContractError::ProjectNotFound)?; - -project.verification_status = VerificationStatus::Verified; -project.updated_at = env.ledger().timestamp(); // Keeps your data current - -env.storage() - .persistent() - .set(&DataKey::Project(project_id), &project); - -// 3. STATE: Create the separate verification record (From Incoming) -let record = VerificationRecord { - status: VerificationStatus::Verified, -}; -env.storage() - .persistent() - .set(&StorageKey::Verification(project_id), &record); + // Update project + project.verification_status = VerificationStatus::Verified; + project.updated_at = now; + env.storage() + .persistent() + .set(&StorageKey::Project(project_id), &project); -// 4. EVENT: Notify the network -publish_verification_approved_event(env, project_id, admin); + publish_verification_approved_event(env, project_id, admin); Ok(()) } @@ -130,65 +113,35 @@ publish_verification_approved_event(env, project_id, admin); admin: Address, ) -> Result<(), ContractError> { admin.require_auth(); + AdminManager::require_admin(env, &admin)?; -// Verify admin privileges -AdminManager::require_admin(env, &admin)?; + // Get project + let mut project = ProjectRegistry::get_project(env, project_id) + .ok_or(ContractError::ProjectNotFound)?; -// Get verification record -let mut record: VerificationRecord = env - .storage() - .persistent() - .get(&StorageKey::Verification(project_id)) // Ensure this matches your key type - .ok_or(ContractError::RecordNotFound)?; - .storage() - .persistent() - .get(&DataKey::Verification(project_id)) - .ok_or(ContractError::VerificationNotFound)?; + // Get verification record + let mut record = Self::get_verification(env, project_id)?; -// Check if already processed -if record.status != VerificationStatus::Pending { - return Err(ContractError::InvalidStatusTransition); -} + if record.status != VerificationStatus::Pending { + return Err(ContractError::InvalidStatusTransition); + } + + let now = env.ledger().timestamp(); -// Update status -record.status = VerificationStatus::Rejected;ected; + // Update record + record.status = VerificationStatus::Rejected; env.storage() .persistent() - .set(&DataKey::Verification(project_id), &record); - -// 1. Authorize Admin (From Incoming) -let stored_admin: Address = env - .storage() - .persistent() - .get(&StorageKey::Admin) - .ok_or(ContractError::Unauthorized)?; - -if admin != stored_admin { - return Err(ContractError::Unauthorized); -} -admin.require_auth(); // This is the most critical security line - -// 2. Update Project Status to Rejected -let mut project = crate::project_registry::ProjectRegistry::get_project(env, project_id) - .ok_or(ContractError::ProjectNotFound)?; - -project.verification_status = VerificationStatus::Rejected; -project.updated_at = env.ledger().timestamp(); // Keeps your data history accurate + .set(&StorageKey::Verification(project_id), &record); -env.storage() - .persistent() - .set(&DataKey::Project(project_id), &project); - -// 3. Update the Verification Record (New required state) -let record = VerificationRecord { - status: VerificationStatus::Rejected, -}; -env.storage() - .persistent() - .set(&StorageKey::Verification(project_id), &record); + // Update project + project.verification_status = VerificationStatus::Rejected; + project.updated_at = now; + env.storage() + .persistent() + .set(&StorageKey::Project(project_id), &project); -// 4. Emit the Event exactly once -publish_verification_rejected_event(env, project_id); + publish_verification_rejected_event(env, project_id, admin); Ok(()) } @@ -198,73 +151,10 @@ publish_verification_rejected_event(env, project_id); ) -> Result { env.storage() .persistent() -.get(&StorageKey::Verification(project_id)) -.ok_or(ContractError::VerificationNotFound) - } - - #[allow(dead_code)] - pub fn list_pending_verifications( - env: &Env, - _admin: Address, - start_project_id: u64, - limit: u32, - ) -> Result, ContractError> { - // Simple implementation for now: iterate projects and collect pending - let count: u64 = env - .storage() - .persistent() - .get(&StorageKey::ProjectCount) - .unwrap_or(0); - let mut pending = Vec::new(env); - let mut checked = 0; - let mut current_id = start_project_id; - - while checked < limit && current_id <= count { - if let Some(record) = env - .storage() - .persistent() - .get::<_, VerificationRecord>(&StorageKey::Verification(current_id)) - { - if record.status == VerificationStatus::Pending { - pending.push_back(record); - checked += 1; - } - } - current_id += 1; - } - - Ok(pending) - } - - #[allow(dead_code)] - 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, - ) -> Result { - let record = Self::get_verification(env, project_id)?; - Ok(record.status) + .get(&StorageKey::Verification(project_id)) + .ok_or(ContractError::VerificationNotFound) } - #[allow(dead_code)] - pub fn update_verification_evidence( - _env: &Env, - _project_id: u64, - _requester: Address, - _new_evidence_cid: String, - ) -> Result<(), ContractError> { - 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() { return Err(ContractError::InvalidProjectData); @@ -272,8 +162,9 @@ publish_verification_rejected_event(env, project_id); Ok(()) } - #[allow(dead_code)] - pub fn get_verification_stats(_env: &Env) -> (u32, u32, u32) { - (0, 0, 0) + pub fn verification_exists(env: &Env, project_id: u64) -> bool { + env.storage() + .persistent() + .has(&StorageKey::Verification(project_id)) } } diff --git a/dongle-smartcontract/src/verification_tests.rs b/dongle-smartcontract/src/verification_tests.rs index 828b009..070822f 100644 --- a/dongle-smartcontract/src/verification_tests.rs +++ b/dongle-smartcontract/src/verification_tests.rs @@ -1,6 +1,7 @@ #[cfg(test)] mod test { use crate::types::{ProjectRegistrationParams, VerificationStatus}; + use crate::errors::ContractError; use crate::DongleContract; use crate::DongleContractClient; use soroban_sdk::{testutils::Address as _, Address, Env, String}; @@ -31,7 +32,7 @@ mod test { logo_cid: None, metadata_cid: None, }; - let project_id = client.register_project(¶ms).unwrap(); + let project_id = client.register_project(¶ms); // 1. Initially unverified let project = client.get_project(&project_id).unwrap(); @@ -42,9 +43,7 @@ mod test { // 3. Pay fee (using owner) let token_admin = Address::generate(&env); - let token_address = env - .register_stellar_asset_contract_v2(token_admin) - .address(); + let token_address = env.register_stellar_asset_contract_v2(token_admin).address(); client.set_fee(&admin, &Some(token_address.clone()), &100, &admin); // Mock token balance for owner @@ -88,13 +87,11 @@ mod test { logo_cid: None, metadata_cid: None, }; - let project_id = client.register_project(¶ms).unwrap(); + let project_id = client.register_project(¶ms); // Set fee and pay let token_admin = Address::generate(&env); - let token_address = env - .register_stellar_asset_contract_v2(token_admin) - .address(); + let token_address = env.register_stellar_asset_contract_v2(token_admin).address(); let token_client = soroban_sdk::token::StellarAssetClient::new(&env, &token_address); token_client.mint(&owner, &100); client.set_fee(&admin, &Some(token_address.clone()), &100, &admin); diff --git a/dongle-smartcontract/test_snapshots/review_registry/test/test_add_review_event.1.json b/dongle-smartcontract/test_snapshots/review_registry/test/test_add_review_event.1.json index 8938406..6d7f425 100644 --- a/dongle-smartcontract/test_snapshots/review_registry/test/test_add_review_event.1.json +++ b/dongle-smartcontract/test_snapshots/review_registry/test/test_add_review_event.1.json @@ -4,25 +4,7 @@ "nonce": 0 }, "auth": [ - [ - [ - "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", - { - "function": { - "contract_fn": { - "contract_address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", - "function_name": "initialize", - "args": [ - { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" - } - ] - } - }, - "sub_invocations": [] - } - ] - ], + [], [ [ "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", @@ -125,7 +107,7 @@ "ledger": { "protocol_version": 21, "sequence_number": 0, - "timestamp": 0, + "timestamp": 1000000, "network_id": "0000000000000000000000000000000000000000000000000000000000000000", "base_reserve": 0, "min_persistent_entry_ttl": 4096, @@ -138,7 +120,7 @@ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", "key": { "ledger_key_nonce": { - "nonce": 1033654523790656264 + "nonce": 5541220902715666415 } }, "durability": "temporary" @@ -153,7 +135,7 @@ "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD2KM", "key": { "ledger_key_nonce": { - "nonce": 1033654523790656264 + "nonce": 5541220902715666415 } }, "durability": "temporary", @@ -201,13 +183,18 @@ [ { "contract_data": { - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", "key": { - "ledger_key_nonce": { - "nonce": 5541220902715666415 - } + "vec": [ + { + "symbol": "Admin" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] }, - "durability": "temporary" + "durability": "persistent" } }, [ @@ -216,19 +203,26 @@ "data": { "contract_data": { "ext": "v0", - "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4", + "contract": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M", "key": { - "ledger_key_nonce": { - "nonce": 5541220902715666415 - } + "vec": [ + { + "symbol": "Admin" + }, + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] }, - "durability": "temporary", - "val": "void" + "durability": "persistent", + "val": { + "bool": true + } } }, "ext": "v0" }, - 6311999 + 4095 ] ], [ @@ -238,7 +232,7 @@ "key": { "vec": [ { - "symbol": "Admin" + "symbol": "AdminList" } ] }, @@ -255,13 +249,17 @@ "key": { "vec": [ { - "symbol": "Admin" + "symbol": "AdminList" } ] }, "durability": "persistent", "val": { - "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + "vec": [ + { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + ] } } }, @@ -369,7 +367,7 @@ "symbol": "created_at" }, "val": { - "u64": 0 + "u64": 1000000 } }, { @@ -421,7 +419,7 @@ "symbol": "updated_at" }, "val": { - "u64": 0 + "u64": 1000000 } }, { @@ -705,6 +703,14 @@ "string": "QmHash" } }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u64": 1000000 + } + }, { "key": { "symbol": "project_id" @@ -731,10 +737,10 @@ }, { "key": { - "symbol": "timestamp" + "symbol": "updated_at" }, "val": { - "u64": 0 + "u64": 1000000 } } ] @@ -877,6 +883,29 @@ }, "failed_call": false }, + { + "event": { + "ext": "v0", + "contract_id": "0000000000000000000000000000000000000000000000000000000000000003", + "type_": "contract", + "body": { + "v0": { + "topics": [ + { + "symbol": "ADMIN" + }, + { + "symbol": "ADDED" + } + ], + "data": { + "address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFCT4" + } + } + } + }, + "failed_call": false + }, { "event": { "ext": "v0", @@ -1080,6 +1109,14 @@ "string": "QmHash" } }, + { + "key": { + "symbol": "created_at" + }, + "val": { + "u64": 1000000 + } + }, { "key": { "symbol": "project_id" @@ -1098,10 +1135,10 @@ }, { "key": { - "symbol": "timestamp" + "symbol": "updated_at" }, "val": { - "u64": 0 + "u64": 1000000 } } ]