diff --git a/quid-contract/contracts/quid-milestone-escrow/src/lib.rs b/quid-contract/contracts/quid-milestone-escrow/src/lib.rs index 8e24890..05e676c 100644 --- a/quid-contract/contracts/quid-milestone-escrow/src/lib.rs +++ b/quid-contract/contracts/quid-milestone-escrow/src/lib.rs @@ -1,9 +1,8 @@ #![no_std] - use soroban_sdk::{contract, contractevent, contractimpl, token, Address, Env, String}; mod error; -pub mod types; +mod types; use error::MilestoneEscrowError; use types::{DataKey, Milestone, MilestoneStatus, Program, ProgramStatus}; @@ -15,7 +14,7 @@ pub struct ProgramCreatedEvent { pub recipient: Address, } -#[contractevent(topics = ["program", "status_changed"])] +#[contractevent(topics = ["program", "status"])] pub struct ProgramStatusChangedEvent { pub program_id: u64, pub status: ProgramStatus, @@ -48,6 +47,43 @@ pub struct QuidMilestoneEscrowContract; #[contractimpl] impl QuidMilestoneEscrowContract { + // ── Status helpers ──────────────────────────────────────────────────── + + pub fn get_program_status(env: Env) -> ProgramStatus { + env.storage() + .instance() + .get(&DataKey::ProgramStatus) + .unwrap_or_default() + } + + pub fn set_program_status(env: Env, status: ProgramStatus) { + env.storage() + .instance() + .set(&DataKey::ProgramStatus, &status); + } + + pub fn get_milestone_status(env: Env) -> MilestoneStatus { + env.storage() + .instance() + .get(&DataKey::MilestoneStatus) + .unwrap_or_default() + } + + pub fn set_milestone_status(env: Env, status: MilestoneStatus) { + env.storage() + .instance() + .set(&DataKey::MilestoneStatus, &status); + } + + pub fn get_program_count(env: Env) -> u64 { + env.storage() + .instance() + .get(&DataKey::ProgramCount) + .unwrap_or(0_u64) + } + + // ── Core methods ────────────────────────────────────────────────────── + pub fn create_program( env: Env, sponsor: Address, @@ -69,8 +105,17 @@ impl QuidMilestoneEscrowContract { &total_amount, ); - let program_id = Self::get_next_program_id(&env); - let status = ProgramStatus::Active; + let mut count: u64 = env + .storage() + .instance() + .get(&DataKey::ProgramCount) + .unwrap_or(0); + count += 1; + env.storage().instance().set(&DataKey::ProgramCount, &count); + + let program_id = count; + let created_at = env.ledger().timestamp(); + let program = Program { id: program_id, sponsor: sponsor.clone(), @@ -82,8 +127,8 @@ impl QuidMilestoneEscrowContract { released_amount: 0, milestone_count: 0, metadata_cid, - created_at: env.ledger().timestamp(), - status, + created_at, + status: ProgramStatus::Active, }; env.storage() @@ -96,7 +141,12 @@ impl QuidMilestoneEscrowContract { recipient, } .publish(&env); - ProgramStatusChangedEvent { program_id, status }.publish(&env); + + ProgramStatusChangedEvent { + program_id, + status: ProgramStatus::Active, + } + .publish(&env); Ok(program_id) } @@ -187,39 +237,34 @@ impl QuidMilestoneEscrowContract { let mut program = Self::get_program(env.clone(), program_id)?; - // #181 – authorize sponsor or optional reviewer without consuming the Option let is_sponsor = approver == program.sponsor; let is_reviewer = program.reviewer.as_ref() == Some(&approver); if !is_sponsor && !is_reviewer { return Err(MilestoneEscrowError::NotAuthorized); } - // #182 – reject non-active programs if program.status != ProgramStatus::Active { return Err(MilestoneEscrowError::InvalidState); } - // Reject milestones not in Pending status let mut milestone = Self::get_milestone(env.clone(), program_id, milestone_id)?; if milestone.status != MilestoneStatus::Pending { return Err(MilestoneEscrowError::InvalidState); } - // Transfer milestone funds from contract to recipient + let paid_amount = milestone.amount; + token::Client::new(&env, &program.token).transfer( &env.current_contract_address(), &program.recipient, - &milestone.amount, + &paid_amount, ); - // Mark the milestone as Paid and persist - let paid_amount = milestone.amount; milestone.status = MilestoneStatus::Paid; env.storage() .persistent() .set(&DataKey::Milestone(program_id, milestone_id), &milestone); - // Increase released_amount program.released_amount = program .released_amount .checked_add(paid_amount) @@ -235,7 +280,6 @@ impl QuidMilestoneEscrowContract { } .publish(&env); - // If all funds are released, move the program to Completed if program.released_amount >= program.total_amount { program.status = ProgramStatus::Completed; ProgramStatusChangedEvent { @@ -261,17 +305,14 @@ impl QuidMilestoneEscrowContract { let mut program = Self::get_program(env.clone(), program_id)?; - // #183 – only the program sponsor may cancel if sponsor != program.sponsor { return Err(MilestoneEscrowError::NotAuthorized); } - // Reject non-active programs if program.status != ProgramStatus::Active { return Err(MilestoneEscrowError::InvalidState); } - // Refund total_amount - released_amount from contract to sponsor let refund_amount = program.total_amount - program.released_amount; if refund_amount > 0 { token::Client::new(&env, &program.token).transfer( @@ -281,7 +322,6 @@ impl QuidMilestoneEscrowContract { ); } - // Mark the program as Cancelled and persist program.status = ProgramStatus::Cancelled; env.storage() .persistent() @@ -302,50 +342,6 @@ impl QuidMilestoneEscrowContract { Ok(()) } - - pub fn get_program_status(env: Env) -> ProgramStatus { - env.storage() - .persistent() - .get(&DataKey::ProgramStatus) - .unwrap_or_default() - } - - pub fn set_program_status(env: Env, status: ProgramStatus) { - env.storage() - .persistent() - .set(&DataKey::ProgramStatus, &status); - } - - pub fn get_milestone_status(env: Env) -> MilestoneStatus { - env.storage() - .persistent() - .get(&DataKey::MilestoneStatus) - .unwrap_or_default() - } - - pub fn set_milestone_status(env: Env, status: MilestoneStatus) { - env.storage() - .persistent() - .set(&DataKey::MilestoneStatus, &status); - } - - pub fn get_program_count(env: Env) -> u64 { - env.storage() - .instance() - .get(&DataKey::ProgramCount) - .unwrap_or(0) - } - - fn get_next_program_id(env: &Env) -> u64 { - let mut count: u64 = env - .storage() - .instance() - .get(&DataKey::ProgramCount) - .unwrap_or(0); - count += 1; - env.storage().instance().set(&DataKey::ProgramCount, &count); - count - } } mod test; diff --git a/quid-contract/contracts/quid-reputation/src/error.rs b/quid-contract/contracts/quid-reputation/src/error.rs index a4d2ab7..fe504e3 100644 --- a/quid-contract/contracts/quid-reputation/src/error.rs +++ b/quid-contract/contracts/quid-reputation/src/error.rs @@ -2,16 +2,10 @@ use soroban_sdk::contracterror; #[contracterror] #[derive(Copy, Clone, Debug, Eq, PartialEq)] - -pub enum QuidError { +pub enum ReputationError { NotAuthorized = 1, - ProfileNotFound = 2, - AdminAlreadySet = 3, - InvalidScore = 4, - AttestationNotFound = 5, - InvalidExpiryTime = 6, - AlreadyRevoked = 7, - AdminNotSet = 8, - InvalidLabel = 9, - InvalidRewardAmount = 10, + InvalidInput = 2, + AlreadyRevoked = 3, + AttestationNotFound = 4, + ProfileNotFound = 5, } diff --git a/quid-contract/contracts/quid-reputation/src/lib.rs b/quid-contract/contracts/quid-reputation/src/lib.rs index ceb4a88..76a7b5e 100644 --- a/quid-contract/contracts/quid-reputation/src/lib.rs +++ b/quid-contract/contracts/quid-reputation/src/lib.rs @@ -1,25 +1,18 @@ #![no_std] - use soroban_sdk::{contract, contractevent, contractimpl, Address, Env, String}; mod error; mod types; -use error::QuidError; +use error::ReputationError; use types::{Attestation, DataKey, Profile}; const PROFILE_TTL_LEDGERS: u32 = 5_184_000; -#[contractevent(topics = ["attestation", "issued"])] -pub struct AttestationIssuedEvent { - pub attestation_id: u64, - pub issuer: Address, - pub subject: Address, -} - -#[contractevent(topics = ["attestation", "revoked"], data_format = "single-value")] +#[contractevent(topics = ["attestation", "revoked"])] pub struct AttestationRevokedEvent { pub attestation_id: u64, + pub revoked_by: Address, } #[contract] @@ -27,63 +20,51 @@ pub struct QuidReputationContract; #[contractimpl] impl QuidReputationContract { - /// Bootstrap admin for the contract - pub fn bootstrap_admin(env: Env, admin: Address) -> Result<(), QuidError> { - if env.storage().persistent().has(&DataKey::Admin) { - return Err(QuidError::AdminAlreadySet); - } + // ------------------------------------------------------------------------- + // Admin bootstrap + // ------------------------------------------------------------------------- - env.storage().persistent().set(&DataKey::Admin, &admin); - env.storage() - .persistent() - .extend_ttl(&DataKey::Admin, 5184000, 5184000); + pub fn initialize(env: Env, admin: Address) -> Result<(), ReputationError> { + admin.require_auth(); + + if env.storage().instance().has(&DataKey::Admin) { + return Err(ReputationError::InvalidInput); + } + env.storage().instance().set(&DataKey::Admin, &admin); Ok(()) } - /// Get the current admin - pub fn get_admin(env: Env) -> Result { + pub fn get_admin(env: Env) -> Result { env.storage() - .persistent() + .instance() .get(&DataKey::Admin) - .ok_or(QuidError::AdminNotSet) + .ok_or(ReputationError::NotAuthorized) } - /// Issue an attestation for a subject + // ------------------------------------------------------------------------- + // Attestations + // ------------------------------------------------------------------------- + pub fn issue_attestation( env: Env, issuer: Address, subject: Address, - kind: String, - label: String, - metadata_cid: Option, - expires_at: Option, - ) -> Result { + attestation_type: String, + data_cid: String, + ) -> Result { issuer.require_auth(); - if label.is_empty() { - return Err(QuidError::InvalidLabel); - } - - if let Some(expiry) = expires_at { - let now = env.ledger().timestamp(); - if expiry <= now { - return Err(QuidError::InvalidExpiryTime); - } - } - let attestation_id = Self::get_next_attestation_id(&env); let issued_at = env.ledger().timestamp(); let attestation = Attestation { id: attestation_id, - issuer: issuer.clone(), - subject: subject.clone(), - kind, - label, - metadata_cid, + issuer, + subject, + attestation_type, + data_cid, issued_at, - expires_at, revoked: false, }; @@ -93,141 +74,133 @@ impl QuidReputationContract { env.storage().persistent().extend_ttl( &DataKey::Attestation(attestation_id), - 5184000, - 5184000, + PROFILE_TTL_LEDGERS, + PROFILE_TTL_LEDGERS, ); - AttestationIssuedEvent { - attestation_id, - issuer, - subject, - } - .publish(&env); - Ok(attestation_id) } - /// Get an attestation by id - pub fn get_attestation(env: Env, attestation_id: u64) -> Result { + pub fn get_attestation(env: Env, attestation_id: u64) -> Result { env.storage() .persistent() .get(&DataKey::Attestation(attestation_id)) - .ok_or(QuidError::AttestationNotFound) + .ok_or(ReputationError::AttestationNotFound) } - /// Revoke an attestation - pub fn revoke_attestation(env: Env, attestation_id: u64) -> Result<(), QuidError> { - let mut attestation = Self::get_attestation(env.clone(), attestation_id)?; + pub fn revoke_attestation( + env: Env, + caller: Address, + attestation_id: u64, + ) -> Result<(), ReputationError> { + caller.require_auth(); - attestation.issuer.require_auth(); + let mut attestation = Self::get_attestation(env.clone(), attestation_id)?; if attestation.revoked { - return Err(QuidError::AlreadyRevoked); + return Err(ReputationError::AlreadyRevoked); } - attestation.revoked = true; + let admin = Self::get_admin(env.clone())?; + if caller != attestation.issuer && caller != admin { + return Err(ReputationError::NotAuthorized); + } + attestation.revoked = true; env.storage() .persistent() .set(&DataKey::Attestation(attestation_id), &attestation); env.storage().persistent().extend_ttl( &DataKey::Attestation(attestation_id), - 5184000, - 5184000, + PROFILE_TTL_LEDGERS, + PROFILE_TTL_LEDGERS, ); - AttestationRevokedEvent { attestation_id }.publish(&env); + AttestationRevokedEvent { + attestation_id, + revoked_by: caller, + } + .publish(&env); Ok(()) } - // ------------------------------------------------------------------------- - // Public profile getter - // ------------------------------------------------------------------------- + pub fn get_attestation_count(env: Env) -> u64 { + env.storage() + .instance() + .get(&DataKey::AttestationCount) + .unwrap_or(0) + } - /// Fetch the reputation profile for `subject`. - pub fn get_profile(env: Env, subject: Address) -> Result { + pub fn attestation_exists(env: Env, attestation_id: u64) -> bool { env.storage() .persistent() - .get(&DataKey::Profile(subject)) - .ok_or(QuidError::ProfileNotFound) + .has(&DataKey::Attestation(attestation_id)) } // ------------------------------------------------------------------------- - // Profile Mutations + // Profiles // ------------------------------------------------------------------------- - /// Record a successful mission for `subject` and add `reward_amount` to their total earnings. - #[allow(deprecated)] - pub fn increment_success( - env: Env, - subject: Address, - reward_amount: i128, - ) -> Result<(), QuidError> { - let admin = Self::get_admin(env.clone())?; - admin.require_auth(); - - if reward_amount < 0 { - return Err(QuidError::InvalidRewardAmount); - } + pub fn get_profile(env: Env, subject: Address) -> Result { + env.storage() + .persistent() + .get(&DataKey::Profile(subject)) + .ok_or(ReputationError::ProfileNotFound) + } - let mut profile = Self::load_or_default(&env, subject.clone()); - profile.successful_missions += 1; - profile.total_earnings += reward_amount; - profile.updated_at = env.ledger().timestamp(); + pub fn set_profile(env: Env, profile: Profile) -> Result<(), ReputationError> { + profile.subject.require_auth(); - Self::store_profile(&env, &profile); + env.storage() + .persistent() + .set(&DataKey::Profile(profile.subject.clone()), &profile); - env.events().publish( - ( - soroban_sdk::symbol_short!("Profile"), - soroban_sdk::symbol_short!("updated"), - ), - profile, + env.storage().persistent().extend_ttl( + &DataKey::Profile(profile.subject), + PROFILE_TTL_LEDGERS, + PROFILE_TTL_LEDGERS, ); Ok(()) } - // ------------------------------------------------------------------------- - // Internal helpers - // ------------------------------------------------------------------------- + pub fn profile_exists(env: Env, subject: Address) -> bool { + env.storage().persistent().has(&DataKey::Profile(subject)) + } fn get_next_attestation_id(env: &Env) -> u64 { - let current: u64 = env + let mut count: u64 = env .storage() - .persistent() + .instance() .get(&DataKey::AttestationCount) .unwrap_or(0); - - let next_id = current + 1; - - env.storage() - .persistent() - .set(&DataKey::AttestationCount, &next_id); - + count += 1; env.storage() - .persistent() - .extend_ttl(&DataKey::AttestationCount, 5184000, 5184000); - - next_id + .instance() + .set(&DataKey::AttestationCount, &count); + count } } +// ------------------------------------------------------------------------- +// Internal helpers used by tests +// ------------------------------------------------------------------------- + #[allow(dead_code)] impl QuidReputationContract { - pub(crate) fn require_admin(env: &Env, caller: &Address) -> Result<(), QuidError> { + pub(crate) fn require_admin(env: &Env, caller: &Address) -> Result<(), ReputationError> { let admin: Address = env .storage() - .persistent() + .instance() .get(&DataKey::Admin) - .ok_or(QuidError::AdminNotSet)?; + .ok_or(ReputationError::NotAuthorized)?; admin.require_auth(); if *caller != admin { - return Err(QuidError::NotAuthorized); + return Err(ReputationError::NotAuthorized); } Ok(()) @@ -248,10 +221,8 @@ impl QuidReputationContract { .unwrap_or(Profile { subject, score: 0, - successful_missions: 0, + missions_completed: 0, missions_created: 0, - total_earnings: 0, - updated_at: env.ledger().timestamp(), }) } } diff --git a/quid-contract/contracts/quid-reputation/src/test.rs b/quid-contract/contracts/quid-reputation/src/test.rs index 6ea92c3..d780681 100644 --- a/quid-contract/contracts/quid-reputation/src/test.rs +++ b/quid-contract/contracts/quid-reputation/src/test.rs @@ -1,223 +1,329 @@ #![cfg(test)] -use super::*; -use soroban_sdk::{testutils::Address as _, Address, Env, String}; +use crate::{types::Profile, QuidReputationContract, QuidReputationContractClient}; +use soroban_sdk::{ + testutils::{Address as _, Events}, + Address, Env, String, +}; -fn setup_test_env() -> (Env, Address, QuidReputationContractClient<'static>) { +fn setup_test_env() -> (Env, Address, Address) { let env = Env::default(); env.mock_all_auths(); - let admin = Address::generate(&env); let contract_id = env.register(QuidReputationContract, ()); - let client = QuidReputationContractClient::new(&env, &contract_id); - - client.bootstrap_admin(&admin); - - (env, admin, client) -} - -#[test] -fn test_bootstrap_admin() { - let env = Env::default(); let admin = Address::generate(&env); - let contract_id = env.register(QuidReputationContract, ()); - let client = QuidReputationContractClient::new(&env, &contract_id); - client.bootstrap_admin(&admin); + let client = QuidReputationContractClient::new(&env, &contract_id); + client.initialize(&admin); - let retrieved_admin = client.get_admin(); - assert_eq!(retrieved_admin, admin); + (env, contract_id, admin) } +// ------------------------------------------------------------------------- +// Admin bootstrap tests +// ------------------------------------------------------------------------- + #[test] -fn test_bootstrap_admin_twice_fails() { +fn test_initialize() { let env = Env::default(); env.mock_all_auths(); - let admin1 = Address::generate(&env); - let admin2 = Address::generate(&env); + let contract_id = env.register(QuidReputationContract, ()); let client = QuidReputationContractClient::new(&env, &contract_id); - client.bootstrap_admin(&admin1); - let result = client.try_bootstrap_admin(&admin2); + let admin = Address::generate(&env); + client.initialize(&admin); - assert!(result.is_err()); + let stored_admin = client.get_admin(); + assert_eq!(stored_admin, admin); } +// ------------------------------------------------------------------------- +// Attestation tests +// ------------------------------------------------------------------------- + #[test] fn test_issue_attestation() { - let (env, _admin, client) = setup_test_env(); + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + let issuer = Address::generate(&env); let subject = Address::generate(&env); - let attestation_id = client.issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "contributor"), - &String::from_str(&env, "Active contributor"), - &Some(String::from_str(&env, "QmExample123")), - &None, - ); + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); + + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); assert_eq!(attestation_id, 1); let attestation = client.get_attestation(&attestation_id); assert_eq!(attestation.issuer, issuer); assert_eq!(attestation.subject, subject); + assert_eq!(attestation.attestation_type, attestation_type); + assert_eq!(attestation.data_cid, data_cid); assert!(!attestation.revoked); } #[test] -fn test_issue_attestation_with_expiry() { - let (env, _admin, client) = setup_test_env(); +fn test_revoke_attestation_by_issuer() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + let issuer = Address::generate(&env); let subject = Address::generate(&env); - let now = env.ledger().timestamp(); - let expiry = now + 86400; // 1 day from now + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); + + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - let attestation_id = client.issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "expert"), - &String::from_str(&env, "Expert reviewer"), - &None, - &Some(expiry), - ); + client.revoke_attestation(&issuer, &attestation_id); let attestation = client.get_attestation(&attestation_id); - assert_eq!(attestation.expires_at, Some(expiry)); + assert!(attestation.revoked); } #[test] -fn test_revoke_attestation() { - let (env, _admin, client) = setup_test_env(); +fn test_revoke_attestation_by_admin() { + let (env, contract_id, admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + let issuer = Address::generate(&env); let subject = Address::generate(&env); - let attestation_id = client.issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "contributor"), - &String::from_str(&env, "Active contributor"), - &None, - &None, - ); + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); + + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - client.revoke_attestation(&attestation_id); + client.revoke_attestation(&admin, &attestation_id); let attestation = client.get_attestation(&attestation_id); assert!(attestation.revoked); } #[test] -fn test_empty_label_fails() { - let (env, _admin, client) = setup_test_env(); +#[should_panic(expected = "Error(Contract, #1)")] +fn test_revoke_attestation_unauthorized() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + let issuer = Address::generate(&env); let subject = Address::generate(&env); + let unauthorized = Address::generate(&env); + + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); - let result = client.try_issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "contributor"), - &String::from_str(&env, ""), - &None, - &None, - ); + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - assert!(result.is_err()); + client.revoke_attestation(&unauthorized, &attestation_id); } #[test] -fn test_invalid_expiry_time_fails() { - let (env, _admin, client) = setup_test_env(); +#[should_panic(expected = "Error(Contract, #3)")] +fn test_revoke_already_revoked_attestation() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + let issuer = Address::generate(&env); let subject = Address::generate(&env); - let now = env.ledger().timestamp(); - let past_expiry = now; // Expiry <= now should fail + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); - let result = client.try_issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "contributor"), - &String::from_str(&env, "Active contributor"), - &None, - &Some(past_expiry), - ); + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - assert!(result.is_err()); + client.revoke_attestation(&issuer, &attestation_id); + client.revoke_attestation(&issuer, &attestation_id); } -// ------------------------------------------------------------------------- -// Profile helper tests -// ------------------------------------------------------------------------- +#[test] +fn test_attestation_count() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + + let issuer = Address::generate(&env); + let subject = Address::generate(&env); + + assert_eq!(client.get_attestation_count(), 0); + + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); + + client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); + assert_eq!(client.get_attestation_count(), 1); + + client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); + assert_eq!(client.get_attestation_count(), 2); +} #[test] -fn test_get_profile_not_found() { - let (env, _admin, client) = setup_test_env(); +fn test_attestation_exists() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + let issuer = Address::generate(&env); let subject = Address::generate(&env); - let result = client.try_get_profile(&subject); - assert_eq!(result, Err(Ok(QuidError::ProfileNotFound))); + + assert!(!client.attestation_exists(&1)); + + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); + + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); + + assert!(client.attestation_exists(&attestation_id)); } +#[test] +fn test_revoke_attestation_publishes_event() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + + let issuer = Address::generate(&env); + let subject = Address::generate(&env); + + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); + + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); + + client.revoke_attestation(&issuer, &attestation_id); + + let events = env.events().all(); + let event = events.last().unwrap(); + + assert_eq!(event.0, contract_id); + + let topics = event.1.clone(); + assert_eq!(topics.len(), 2); +} + +// ------------------------------------------------------------------------- +// Profile tests +// ------------------------------------------------------------------------- + #[test] fn test_store_and_get_profile() { - let (env, _admin, client) = setup_test_env(); + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); + let subject = Address::generate(&env); - client.increment_success(&subject, &0); // ensure profile exists + env.as_contract(&contract_id, || { + let profile = Profile { + subject: subject.clone(), + score: 42, + missions_completed: 3, + missions_created: 1, + }; + QuidReputationContract::store_profile(&env, &profile); + }); let fetched = client.get_profile(&subject); - assert_eq!(fetched.score, 0); - assert_eq!(fetched.successful_missions, 1); - assert_eq!(fetched.missions_created, 0); - assert_eq!(fetched.total_earnings, 0); + assert_eq!(fetched.score, 42); + assert_eq!(fetched.missions_completed, 3); + assert_eq!(fetched.missions_created, 1); } #[test] fn test_load_or_default_returns_zeroed_profile() { - let (env, _admin, client) = setup_test_env(); + let (env, contract_id, _admin) = setup_test_env(); let subject = Address::generate(&env); - // If profile doesn't exist, it should return error. - let result = client.try_get_profile(&subject); - assert_eq!(result, Err(Ok(QuidError::ProfileNotFound))); + env.as_contract(&contract_id, || { + let profile = QuidReputationContract::load_or_default(&env, subject.clone()); + assert_eq!(profile.subject, subject); + assert_eq!(profile.score, 0); + assert_eq!(profile.missions_completed, 0); + assert_eq!(profile.missions_created, 0); + }); } -// ------------------------------------------------------------------------- -// increment_success tests -// ------------------------------------------------------------------------- +#[test] +fn test_create_and_get_profile() { + let (env, _contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &_contract_id); + + let subject = Address::generate(&env); + + let profile = Profile { + subject: subject.clone(), + score: 150, + missions_completed: 5, + missions_created: 2, + }; + + client.set_profile(&profile); + + let retrieved_profile = client.get_profile(&subject); + assert_eq!(retrieved_profile.subject, subject); + assert_eq!(retrieved_profile.score, 150); + assert_eq!(retrieved_profile.missions_completed, 5); + assert_eq!(retrieved_profile.missions_created, 2); +} #[test] -fn test_increment_success() { - let (env, _admin, client) = setup_test_env(); +fn test_update_profile() { + let (env, _contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &_contract_id); let subject = Address::generate(&env); - // First increment - client.increment_success(&subject, &100); + let profile = Profile { + subject: subject.clone(), + score: 100, + missions_completed: 5, + missions_created: 2, + }; - let fetched = client.get_profile(&subject); - assert_eq!(fetched.successful_missions, 1); - assert_eq!(fetched.total_earnings, 100); + client.set_profile(&profile); + + let updated_profile = Profile { + subject: subject.clone(), + score: 225, + missions_completed: 10, + missions_created: 3, + }; - // Second increment - client.increment_success(&subject, &50); + client.set_profile(&updated_profile); - let fetched2 = client.get_profile(&subject); - assert_eq!(fetched2.successful_missions, 2); - assert_eq!(fetched2.total_earnings, 150); + let retrieved_profile = client.get_profile(&subject); + assert_eq!(retrieved_profile.score, 225); + assert_eq!(retrieved_profile.missions_completed, 10); + assert_eq!(retrieved_profile.missions_created, 3); } #[test] -fn test_increment_success_invalid_reward() { - let (env, _admin, client) = setup_test_env(); +fn test_profile_exists() { + let (env, _contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &_contract_id); let subject = Address::generate(&env); - let res = client.try_increment_success(&subject, &-10); - assert_eq!(res, Err(Ok(QuidError::InvalidRewardAmount))); + assert!(!client.profile_exists(&subject)); + + let profile = Profile { + subject: subject.clone(), + score: 0, + missions_completed: 0, + missions_created: 0, + }; + + client.set_profile(&profile); + + assert!(client.profile_exists(&subject)); +} + +#[test] +#[should_panic(expected = "Error(Contract, #5)")] +fn test_get_profile_not_found() { + let (env, _contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &_contract_id); + + let subject = Address::generate(&env); + client.get_profile(&subject); } diff --git a/quid-contract/contracts/quid-reputation/src/types.rs b/quid-contract/contracts/quid-reputation/src/types.rs index 24053ec..d7deea2 100644 --- a/quid-contract/contracts/quid-reputation/src/types.rs +++ b/quid-contract/contracts/quid-reputation/src/types.rs @@ -6,11 +6,9 @@ pub struct Attestation { pub id: u64, pub issuer: Address, pub subject: Address, - pub kind: String, - pub label: String, - pub metadata_cid: Option, + pub attestation_type: String, + pub data_cid: String, pub issued_at: u64, - pub expires_at: Option, pub revoked: bool, } @@ -18,32 +16,17 @@ pub struct Attestation { #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct Profile { - /// The address this profile belongs to. pub subject: Address, - /// Cumulative reputation score (starts at 0). pub score: i64, - /// Total number of successful missions. - pub successful_missions: u32, - /// Total number of missions created (for creators). + pub missions_completed: u32, pub missions_created: u32, - /// Total earnings from completed missions. - pub total_earnings: i128, - /// Last updated timestamp. - pub updated_at: u64, } #[contracttype] pub enum DataKey { - Attestation(u64), - AttestationCount, Admin, /// Per-subject reputation profile. Profile(Address), -} - -#[contracttype] -pub enum AttestationKind { - Contributor, - Expert, - Reviewer, + Attestation(u64), + AttestationCount, }