From fcd071f7422db2c17278ea14ee8e3aa86afaf9ba Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Mon, 1 Jun 2026 07:07:19 +0100 Subject: [PATCH 1/5] revoke attestation authorization flow --- .../contracts/quid-reputation/src/lib.rs | 15 ++++++++- .../contracts/quid-reputation/src/test.rs | 33 ++++++++++++++++++- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/quid-contract/contracts/quid-reputation/src/lib.rs b/quid-contract/contracts/quid-reputation/src/lib.rs index 51c59e5..703fdd5 100644 --- a/quid-contract/contracts/quid-reputation/src/lib.rs +++ b/quid-contract/contracts/quid-reputation/src/lib.rs @@ -1,5 +1,5 @@ #![no_std] -use soroban_sdk::{contract, contractimpl, Address, Env, String}; +use soroban_sdk::{contract, contractevent, contractimpl, Address, Env, String}; mod error; mod types; @@ -7,6 +7,12 @@ mod types; use error::ReputationError; use types::{Attestation, DataKey, Profile}; +#[contractevent(topics = ["attestation", "revoked"])] +pub struct AttestationRevokedEvent { + pub attestation_id: u64, + pub revoked_by: Address, +} + #[contract] pub struct QuidReputationContract; @@ -107,6 +113,13 @@ impl QuidReputationContract { 5184000, ); + // Publish AttestationRevokedEvent + AttestationRevokedEvent { + attestation_id, + revoked_by: caller, + } + .publish(&env); + Ok(()) } diff --git a/quid-contract/contracts/quid-reputation/src/test.rs b/quid-contract/contracts/quid-reputation/src/test.rs index f8c1732..ccf472d 100644 --- a/quid-contract/contracts/quid-reputation/src/test.rs +++ b/quid-contract/contracts/quid-reputation/src/test.rs @@ -1,7 +1,10 @@ #![cfg(test)] use super::*; -use soroban_sdk::{testutils::Address as _, Address, Env, String}; +use soroban_sdk::{ + testutils::{Address as _, Events}, + Address, Env, String, +}; use types::Profile; fn setup_test_env() -> (Env, Address, Address) { @@ -277,3 +280,31 @@ fn test_get_profile_not_found() { let subject = Address::generate(&env); client.get_profile(&subject); } + +#[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); + + // Revoke the attestation + client.revoke_attestation(&issuer, &attestation_id); + + // Verify the event was published + let events = env.events().all(); + let event = events.last().unwrap(); + + // Check that the event contains the attestation_id and revoked_by + assert_eq!(event.0, contract_id); + + // The event topics should contain "attestation" and "revoked" + let topics = event.1.clone(); + assert_eq!(topics.len(), 2); +} From fd211efbea4547bf93e7ba8dcafa564321aa63c5 Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Mon, 1 Jun 2026 15:16:34 +0100 Subject: [PATCH 2/5] clippy --- quid-contract/contracts/quid-milestone-escrow/src/test.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/quid-contract/contracts/quid-milestone-escrow/src/test.rs b/quid-contract/contracts/quid-milestone-escrow/src/test.rs index ffb06d5..46e4fe5 100644 --- a/quid-contract/contracts/quid-milestone-escrow/src/test.rs +++ b/quid-contract/contracts/quid-milestone-escrow/src/test.rs @@ -91,7 +91,7 @@ fn test_create_program_funds_and_stores_active_program() { #[test] fn test_add_milestone_stores_and_updates_program_totals() { - let (env, contract_id, sponsor, token_address) = setup_test_env(); + let (env, contract_id, sponsor, token_address, _) = setup_test_env(); let client = QuidMilestoneEscrowContractClient::new(&env, &contract_id); let recipient = Address::generate(&env); let total_amount = 500; @@ -136,7 +136,7 @@ fn test_add_milestone_stores_and_updates_program_totals() { #[test] #[should_panic(expected = "Error(Contract, #2)")] fn test_add_milestone_rejects_over_allocation() { - let (env, contract_id, sponsor, token_address) = setup_test_env(); + let (env, contract_id, sponsor, token_address, _) = setup_test_env(); let client = QuidMilestoneEscrowContractClient::new(&env, &contract_id); let recipient = Address::generate(&env); let total_amount = 500; @@ -161,7 +161,7 @@ fn test_add_milestone_rejects_over_allocation() { #[test] #[should_panic(expected = "Error(Contract, #4)")] fn test_get_milestone_not_found() { - let (env, contract_id, _sponsor, _token_address) = setup_test_env(); + let (env, contract_id, _sponsor, _token_address, _) = setup_test_env(); let client = QuidMilestoneEscrowContractClient::new(&env, &contract_id); let _ = client.get_milestone(&999, &1); From 8f8d721e854b9906b348bc3d5f8c28af208eed7c Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Tue, 2 Jun 2026 05:08:53 +0100 Subject: [PATCH 3/5] fixed compile issues --- .../contracts/quid-reputation/src/test.rs | 60 ++++++------------- .../contracts/quid-reputation/src/types.rs | 4 +- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/quid-contract/contracts/quid-reputation/src/test.rs b/quid-contract/contracts/quid-reputation/src/test.rs index 0dda53f..e24828a 100644 --- a/quid-contract/contracts/quid-reputation/src/test.rs +++ b/quid-contract/contracts/quid-reputation/src/test.rs @@ -1,14 +1,10 @@ #![cfg(test)] -use super::*; +use crate::{types::Profile, QuidReputationContract, QuidReputationContractClient}; use soroban_sdk::{ testutils::{Address as _, Events}, Address, Env, String, }; -use crate::{ - error::ReputationError, types::Profile, QuidReputationContract, QuidReputationContractClient, -}; -use types::Profile; fn setup_test_env() -> (Env, Address, Address) { let env = Env::default(); @@ -226,28 +222,21 @@ fn test_create_and_get_profile() { let client = QuidReputationContractClient::new(&env, &_contract_id); let subject = Address::generate(&env); - let updated_at = env.ledger().timestamp(); let profile = Profile { subject: subject.clone(), - successful_missions: 5, - rejected_submissions: 2, - reviewer_score: 100, - founder_score: 50, - total_earnings: 1000, - updated_at, + 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.successful_missions, 5); - assert_eq!(retrieved_profile.rejected_submissions, 2); - assert_eq!(retrieved_profile.reviewer_score, 100); - assert_eq!(retrieved_profile.founder_score, 50); - assert_eq!(retrieved_profile.total_earnings, 1000); - assert_eq!(retrieved_profile.updated_at, updated_at); + assert_eq!(retrieved_profile.score, 150); + assert_eq!(retrieved_profile.missions_completed, 5); + assert_eq!(retrieved_profile.missions_created, 2); } #[test] @@ -259,12 +248,9 @@ fn test_update_profile() { let profile = Profile { subject: subject.clone(), - successful_missions: 5, - rejected_submissions: 2, - reviewer_score: 100, - founder_score: 50, - total_earnings: 1000, - updated_at: env.ledger().timestamp(), + score: 100, + missions_completed: 5, + missions_created: 2, }; client.set_profile(&profile); @@ -272,22 +258,17 @@ fn test_update_profile() { // Update the profile let updated_profile = Profile { subject: subject.clone(), - successful_missions: 10, - rejected_submissions: 3, - reviewer_score: 150, - founder_score: 75, - total_earnings: 2000, - updated_at: env.ledger().timestamp(), + score: 225, + missions_completed: 10, + missions_created: 3, }; client.set_profile(&updated_profile); let retrieved_profile = client.get_profile(&subject); - assert_eq!(retrieved_profile.successful_missions, 10); - assert_eq!(retrieved_profile.rejected_submissions, 3); - assert_eq!(retrieved_profile.reviewer_score, 150); - assert_eq!(retrieved_profile.founder_score, 75); - assert_eq!(retrieved_profile.total_earnings, 2000); + assert_eq!(retrieved_profile.score, 225); + assert_eq!(retrieved_profile.missions_completed, 10); + assert_eq!(retrieved_profile.missions_created, 3); } #[test] @@ -301,12 +282,9 @@ fn test_profile_exists() { let profile = Profile { subject: subject.clone(), - successful_missions: 0, - rejected_submissions: 0, - reviewer_score: 0, - founder_score: 0, - total_earnings: 0, - updated_at: env.ledger().timestamp(), + score: 0, + missions_completed: 0, + missions_created: 0, }; client.set_profile(&profile); diff --git a/quid-contract/contracts/quid-reputation/src/types.rs b/quid-contract/contracts/quid-reputation/src/types.rs index 791df82..235be0b 100644 --- a/quid-contract/contracts/quid-reputation/src/types.rs +++ b/quid-contract/contracts/quid-reputation/src/types.rs @@ -29,10 +29,8 @@ pub struct Profile { #[contracttype] pub enum DataKey { Admin, + /// Per-subject reputation profile. Profile(Address), Attestation(u64), AttestationCount, - Admin, - /// Per-subject reputation profile. - Profile(Address), } From 1726a9e17d5db6a3814346c98772d21565ea4cee Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Sat, 6 Jun 2026 07:58:39 +0100 Subject: [PATCH 4/5] fixed errors --- .../quid-milestone-escrow/src/lib.rs | 296 ++++++--------- .../contracts/quid-reputation/src/error.rs | 15 +- .../contracts/quid-reputation/src/lib.rs | 197 +++++----- .../contracts/quid-reputation/src/test.rs | 345 ++++++------------ .../contracts/quid-reputation/src/types.rs | 30 +- 5 files changed, 332 insertions(+), 551 deletions(-) diff --git a/quid-contract/contracts/quid-milestone-escrow/src/lib.rs b/quid-contract/contracts/quid-milestone-escrow/src/lib.rs index ece3896..6844867 100644 --- a/quid-contract/contracts/quid-milestone-escrow/src/lib.rs +++ b/quid-contract/contracts/quid-milestone-escrow/src/lib.rs @@ -1,57 +1,29 @@ #![no_std] -use soroban_sdk::{contract, contractevent, contractimpl, contracttype, Address, Env}; +use soroban_sdk::{contract, contractevent, contractimpl, token, Address, Env, String}; mod error; -use error::MilestoneEscrowError; - -#[contracttype] -pub enum ProgramStatus { - Active, - Completed, - Cancelled, -} - -#[contracttype] -pub enum DataKey { - Program(u64), - Milestone(u64, u32), -} +mod types; -#[contracttype] -pub struct Program { - pub id: u64, - pub owner: Address, - pub status: ProgramStatus, - pub escrow_balance: i128, -} - -#[contracttype] -pub struct Milestone { - pub program_id: u64, - pub milestone_id: u32, - pub amount: i128, - pub pending: bool, -} +use error::MilestoneEscrowError; +use types::{DataKey, Milestone, MilestoneStatus, Program, ProgramStatus}; #[contractevent(topics = ["program", "created"])] pub struct ProgramCreatedEvent { pub program_id: u64, - pub owner: Address, + pub sponsor: Address, + pub recipient: Address, } -#[contractevent(topics = ["milestone", "added"])] -pub struct MilestoneAddedEvent { +#[contractevent(topics = ["program", "status"])] +pub struct ProgramStatusChangedEvent { pub program_id: u64, - pub milestone_id: u32, - pub amount: i128, + pub status: ProgramStatus, } -#[contractevent(topics = ["milestone", "paid"])] -pub struct MilestonePaidEvent { +#[contractevent(topics = ["milestone", "added"])] +pub struct MilestoneAddedEvent { pub program_id: u64, - pub milestone_id: u32, - pub payer: Address, - pub payee: Address, + pub milestone_id: u64, pub amount: i128, } @@ -71,27 +43,95 @@ pub struct ProgramCancelledEvent { } #[contract] -pub struct MilestoneEscrowContract; +pub struct QuidMilestoneEscrowContract; #[contractimpl] -impl MilestoneEscrowContract { +impl QuidMilestoneEscrowContract { + // ── Status helpers (used by tests for default/roundtrip) ────────────── + + 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); + } + + // ── Program counter ─────────────────────────────────────────────────── + + pub fn get_program_count(env: Env) -> u64 { + env.storage() + .instance() + .get(&DataKey::ProgramCount) + .unwrap_or(0_u64) + } + + // ── Core contract methods ───────────────────────────────────────────── + pub fn create_program( env: Env, - owner: Address, - program_id: u64, - initial_escrow: i128, + sponsor: Address, + recipient: Address, + token: Address, + total_amount: i128, + reviewer: Option
, + metadata_cid: Option, ) -> Result { - owner.require_auth(); + sponsor.require_auth(); - if initial_escrow <= 0 { + if total_amount <= 0 { return Err(MilestoneEscrowError::InvalidAmount); } + // Transfer funds from sponsor into the contract + token::Client::new(&env, &token).transfer( + &sponsor, + env.current_contract_address(), + &total_amount, + ); + + 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, - owner: owner.clone(), + sponsor: sponsor.clone(), + recipient: recipient.clone(), + reviewer, + token, + total_amount, + allocated_amount: 0, + released_amount: 0, + milestone_count: 0, + metadata_cid, + created_at, status: ProgramStatus::Active, - escrow_balance: initial_escrow, }; env.storage() @@ -104,7 +144,12 @@ impl MilestoneEscrowContract { recipient, } .publish(&env); - ProgramStatusChangedEvent { program_id, status }.publish(&env); + + ProgramStatusChangedEvent { + program_id, + status: ProgramStatus::Active, + } + .publish(&env); Ok(program_id) } @@ -171,47 +216,19 @@ impl MilestoneEscrowContract { } .publish(&env); - Ok(()) + Ok(milestone_id) } - pub fn pay_milestone( + pub fn get_milestone( env: Env, - payer: Address, program_id: u64, - milestone_id: u32, - payee: Address, - amount: i128, - ) -> Result<(), MilestoneEscrowError> { - payer.require_auth(); - - let mut program: Program = env - .storage() - .persistent() - .get(&DataKey::Program(program_id)) - .ok_or(MilestoneEscrowError::ProgramNotFound)?; - - if program.status != ProgramStatus::Active { - return Err(MilestoneEscrowError::ProgramClosed); - } - - let mut milestone: Milestone = env - .storage() + milestone_id: u64, + ) -> Result { + env.storage() .persistent() .get(&DataKey::Milestone(program_id, milestone_id)) - .ok_or(MilestoneEscrowError::MilestoneNotFound)?; - - if !milestone.pending { - return Err(MilestoneEscrowError::MilestoneNotPending); - } - if amount != milestone.amount { - return Err(MilestoneEscrowError::InvalidAmount); - } - if amount > program.escrow_balance { - return Err(MilestoneEscrowError::AmountExceedsEscrow); - } - - milestone.pending = false; - program.escrow_balance -= amount; + .ok_or(MilestoneEscrowError::MilestoneNotFound) + } pub fn approve_milestone( env: Env, @@ -223,39 +240,34 @@ impl MilestoneEscrowContract { 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) @@ -271,7 +283,6 @@ impl MilestoneEscrowContract { } .publish(&env); - // If all funds are released, move the program to Completed if program.released_amount >= program.total_amount { program.status = ProgramStatus::Completed; ProgramStatusChangedEvent { @@ -297,17 +308,14 @@ impl MilestoneEscrowContract { 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( @@ -317,7 +325,6 @@ impl MilestoneEscrowContract { ); } - // Mark the program as Cancelled and persist program.status = ProgramStatus::Cancelled; env.storage() .persistent() @@ -338,95 +345,6 @@ impl MilestoneEscrowContract { Ok(()) } - - pub fn get_program_status(env: Env) -> ProgramStatus { - env.storage() - .persistent() - .set(&DataKey::Milestone(program_id, milestone_id), &milestone); - env.storage() - .persistent() - .set(&DataKey::Program(program_id), &program); - - MilestonePaidEvent { - program_id, - milestone_id, - payer: payer.clone(), - payee: payee.clone(), - amount, - } - .publish(&env); - - Ok(()) - } - - pub fn cancel_program( - env: Env, - owner: Address, - program_id: u64, - ) -> Result<(), MilestoneEscrowError> { - owner.require_auth(); - - let mut program: Program = env - .storage() - .persistent() - .get(&DataKey::Program(program_id)) - .ok_or(MilestoneEscrowError::ProgramNotFound)?; - - if program.owner != owner { - return Err(MilestoneEscrowError::NotAuthorized); - } - if program.status != ProgramStatus::Active { - return Err(MilestoneEscrowError::ProgramClosed); - } - - program.status = ProgramStatus::Cancelled; - env.storage() - .persistent() - .set(&DataKey::Program(program_id), &program); - - ProgramCancelledEvent { - program_id, - cancelled_by: owner, - } - .publish(&env); - - Ok(()) - } - - pub fn change_program_status( - env: Env, - owner: Address, - program_id: u64, - new_status: ProgramStatus, - ) -> Result<(), MilestoneEscrowError> { - owner.require_auth(); - - let mut program: Program = env - .storage() - .persistent() - .get(&DataKey::Program(program_id)) - .ok_or(MilestoneEscrowError::ProgramNotFound)?; - - if program.owner != owner { - return Err(MilestoneEscrowError::NotAuthorized); - } - if program.status == ProgramStatus::Cancelled && new_status != ProgramStatus::Cancelled { - return Err(MilestoneEscrowError::ProgramClosed); - } - - let old_status = program.status; - program.status = new_status; - env.storage() - .persistent() - .set(&DataKey::Program(program_id), &program); - - ProgramStatusChangedEvent { - program_id, - old_status, - new_status: program.status, - } - .publish(&env); - - Ok(()) - } } + +mod test; diff --git a/quid-contract/contracts/quid-reputation/src/error.rs b/quid-contract/contracts/quid-reputation/src/error.rs index 72cff04..fe504e3 100644 --- a/quid-contract/contracts/quid-reputation/src/error.rs +++ b/quid-contract/contracts/quid-reputation/src/error.rs @@ -2,15 +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 = 2, - InvalidExpiryTime = 3, - AlreadyRevoked = 4, - AdminNotSet = 5, - InvalidLabel = 6, + 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 7110696..198eae8 100644 --- a/quid-contract/contracts/quid-reputation/src/lib.rs +++ b/quid-contract/contracts/quid-reputation/src/lib.rs @@ -1,23 +1,15 @@ #![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, @@ -28,138 +20,114 @@ pub struct QuidReputationContract; #[contractimpl] impl QuidReputationContract { - /// Bootstrap admin for the contract - pub fn bootstrap_admin(env: Env, admin: Address) -> Result<(), QuidError> { - // Only allow setting admin if not already set - if env.storage().persistent().has(&DataKey::Admin) { - return Err(QuidError::NotAuthorized); - } + // ------------------------------------------------------------------------- + // Admin bootstrap + // ------------------------------------------------------------------------- - env.storage().persistent().set(&DataKey::Admin, &admin); + /// Initialize the contract with an admin address. May only be called once. + pub fn initialize(env: Env, admin: Address) -> Result<(), ReputationError> { + admin.require_auth(); - env.storage() - .persistent() - .extend_ttl(&DataKey::Admin, 5184000, 5184000); + 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 { + /// Get the admin address. + 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 + // ------------------------------------------------------------------------- + + /// Issue a new attestation. pub fn issue_attestation( env: Env, issuer: Address, subject: Address, - kind: String, - label: String, - metadata_cid: Option, - expires_at: Option, - ) -> Result { - // Require issuer auth + attestation_type: String, + data_cid: String, + ) -> Result { issuer.require_auth(); - // Validate label is not empty - if label.is_empty() { - return Err(QuidError::InvalidLabel); - } - - // Validate expiry time if provided - if let Some(expiry) = expires_at { - let now = env.ledger().timestamp(); - if expiry <= now { - return Err(QuidError::InvalidExpiryTime); - } - } - - // Get the next attestation id 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, }; - // Store the attestation - 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, ); - // Publish AttestationIssuedEvent - 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 { + /// Get an attestation by ID. + 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)?; + /// Revoke an attestation (issuer or admin only). + pub fn revoke_attestation( + env: Env, + caller: Address, + attestation_id: u64, + ) -> Result<(), ReputationError> { + caller.require_auth(); - // Require issuer auth - attestation.issuer.require_auth(); + let mut attestation = Self::get_attestation(env.clone(), attestation_id)?; - // Check if already revoked if attestation.revoked { - return Err(QuidError::AlreadyRevoked); + return Err(ReputationError::AlreadyRevoked); } - // Mark as revoked - attestation.revoked = true; + let admin = Self::get_admin(env.clone())?; + if caller != attestation.issuer && caller != admin { + return Err(ReputationError::NotAuthorized); + } - // Store updated attestation + 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, ); - // Publish AttestationRevokedEvent AttestationRevokedEvent { attestation_id, revoked_by: caller, } .publish(&env); - AttestationRevokedEvent { attestation_id }.publish(&env); Ok(()) } @@ -178,7 +146,7 @@ impl QuidReputationContract { .has(&DataKey::Attestation(attestation_id)) } - /// Get a profile by subject address + /// Get a profile by subject address. pub fn get_profile(env: Env, subject: Address) -> Result { env.storage() .persistent() @@ -186,7 +154,7 @@ impl QuidReputationContract { .ok_or(ReputationError::ProfileNotFound) } - /// Create or update a profile + /// Create or update a profile. pub fn set_profile(env: Env, profile: Profile) -> Result<(), ReputationError> { profile.subject.require_auth(); @@ -194,36 +162,75 @@ impl QuidReputationContract { .persistent() .set(&DataKey::Profile(profile.subject.clone()), &profile); - env.storage() - .persistent() - .extend_ttl(&DataKey::Profile(profile.subject), 5184000, 5184000); + env.storage().persistent().extend_ttl( + &DataKey::Profile(profile.subject), + PROFILE_TTL_LEDGERS, + PROFILE_TTL_LEDGERS, + ); Ok(()) } - /// Check if a profile exists + /// Check if a profile exists. pub fn profile_exists(env: Env, subject: Address) -> bool { env.storage().persistent().has(&DataKey::Profile(subject)) } - // Private helper function to get the next attestation ID 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); + count += 1; + env.storage() + .instance() + .set(&DataKey::AttestationCount, &count); + count + } +} - let next_id = current + 1; +// ------------------------------------------------------------------------- +// Internal helpers +// ------------------------------------------------------------------------- +#[allow(dead_code)] +impl QuidReputationContract { + pub(crate) fn require_admin(env: &Env, caller: &Address) -> Result<(), ReputationError> { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(ReputationError::NotAuthorized)?; + + admin.require_auth(); + + if *caller != admin { + return Err(ReputationError::NotAuthorized); + } + + Ok(()) + } + + pub(crate) fn store_profile(env: &Env, profile: &Profile) { + let key = DataKey::Profile(profile.subject.clone()); + env.storage().persistent().set(&key, profile); env.storage() .persistent() - .set(&DataKey::AttestationCount, &next_id); + .extend_ttl(&key, PROFILE_TTL_LEDGERS, PROFILE_TTL_LEDGERS); + } + pub(crate) fn load_or_default(env: &Env, subject: Address) -> Profile { env.storage() .persistent() - .extend_ttl(&DataKey::AttestationCount, 5184000, 5184000); - - next_id + .get(&DataKey::Profile(subject.clone())) + .unwrap_or(Profile { + subject, + score: 0, + missions_completed: 0, + missions_created: 0, + }) } } + +mod test; diff --git a/quid-contract/contracts/quid-reputation/src/test.rs b/quid-contract/contracts/quid-reputation/src/test.rs index 50f4a78..e83146c 100644 --- a/quid-contract/contracts/quid-reputation/src/test.rs +++ b/quid-contract/contracts/quid-reputation/src/test.rs @@ -8,274 +8,190 @@ use soroban_sdk::{ fn setup_test_env() -> (Env, Address, Address) { let env = Env::default(); - let admin = Address::random(&env); + env.mock_all_auths(); - let result = QuidReputationContract::bootstrap_admin(env.clone(), admin.clone()); + let contract_id = env.register(QuidReputationContract, ()); + let admin = Address::generate(&env); - assert!(result.is_ok()); + let client = QuidReputationContractClient::new(&env, &contract_id); + client.initialize(&admin); - let retrieved_admin = QuidReputationContract::get_admin(env).unwrap(); - assert_eq!(retrieved_admin, admin); + (env, contract_id, admin) } #[test] -fn test_bootstrap_admin() { +fn test_initialize() { let env = Env::default(); - let admin = Address::random(&env); + env.mock_all_auths(); + + let contract_id = env.register(QuidReputationContract, ()); + let client = QuidReputationContractClient::new(&env, &contract_id); - let result = QuidReputationContract::bootstrap_admin(env.clone(), admin.clone()); + let admin = Address::generate(&env); - assert!(result.is_ok()); + client.initialize(&admin); - let retrieved_admin = QuidReputationContract::get_admin(env).unwrap(); - assert_eq!(retrieved_admin, admin); + let stored_admin = client.get_admin(); + assert_eq!(stored_admin, admin); } #[test] -fn test_bootstrap_admin_twice_fails() { - let env = Env::default(); - let admin1 = Address::random(&env); - let admin2 = Address::random(&env); +fn test_issue_attestation() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); - QuidReputationContract::bootstrap_admin(env.clone(), admin1).unwrap(); - let result = QuidReputationContract::bootstrap_admin(env, admin2); + let issuer = Address::generate(&env); + let subject = Address::generate(&env); - assert!(result.is_err()); -} + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); -#[test] -fn test_issue_attestation() { - let env = Env::default(); - let issuer = Address::random(&env); - let subject = Address::random(&env); - - let attestation_id = QuidReputationContract::issue_attestation( - env.clone(), - issuer.clone(), - subject.clone(), - String::from_slice(&env, "contributor"), - String::from_slice(&env, "Active contributor"), - Some(String::from_slice(&env, "QmExample123")), - None, - ) - .unwrap(); + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); assert_eq!(attestation_id, 1); - let attestation = QuidReputationContract::get_attestation(env, attestation_id).unwrap(); + let attestation = client.get_attestation(&attestation_id); assert_eq!(attestation.issuer, issuer); assert_eq!(attestation.subject, subject); - assert_eq!(attestation.revoked, false); + 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 = Env::default(); - let issuer = Address::random(&env); - let subject = Address::random(&env); - - let now = env.ledger().timestamp(); - let expiry = now + 86400; // 1 day from now - - let attestation_id = QuidReputationContract::issue_attestation( - env.clone(), - issuer.clone(), - subject.clone(), - String::from_slice(&env, "expert"), - String::from_slice(&env, "Expert reviewer"), - None, - Some(expiry), - ) - .unwrap(); - - let attestation = QuidReputationContract::get_attestation(env, attestation_id).unwrap(); - assert_eq!(attestation.expires_at, Some(expiry)); -} +fn test_revoke_attestation_by_issuer() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); -#[test] -fn test_revoke_attestation() { - let env = Env::default(); - let issuer = Address::random(&env); - let subject = Address::random(&env); - - let attestation_id = QuidReputationContract::issue_attestation( - env.clone(), - issuer.clone(), - subject.clone(), - String::from_slice(&env, "contributor"), - String::from_slice(&env, "Active contributor"), - None, - None, - ) - .unwrap(); - - let result = QuidReputationContract::revoke_attestation(env.clone(), attestation_id); - assert!(result.is_ok()); - - let attestation = QuidReputationContract::get_attestation(env, attestation_id).unwrap(); - assert_eq!(attestation.revoked, true); -} + let issuer = Address::generate(&env); + let subject = Address::generate(&env); -#[test] -fn test_empty_label_fails() { - let env = Env::default(); - let issuer = Address::random(&env); - let subject = Address::random(&env); - - let result = QuidReputationContract::issue_attestation( - env, - issuer, - subject, - String::from_slice(&env, "contributor"), - String::from_slice(&env, ""), - None, - None, - ); - - assert!(result.is_err()); + 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 attestation = client.get_attestation(&attestation_id); + assert!(attestation.revoked); } #[test] -fn test_invalid_expiry_time_fails() { - let env = Env::default(); - let issuer = Address::random(&env); - let subject = Address::random(&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_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)); - let now = env.ledger().timestamp(); - let past_expiry = now - 1000; // In the past - - let result = QuidReputationContract::issue_attestation( - env, - issuer, - subject, - String::from_slice(&env, "contributor"), - String::from_slice(&env, "Active contributor"), - None, - Some(past_expiry), - ); - - assert!(result.is_err()); + client.revoke_attestation(&admin, &attestation_id); + + let attestation = client.get_attestation(&attestation_id); + assert!(attestation.revoked); } -<<<<<<<<< Temporary merge branch 1 #[test] -fn test_create_and_get_profile() { - let (env, _contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &_contract_id); +#[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 updated_at = env.ledger().timestamp(); + let unauthorized = Address::generate(&env); - let profile = Profile { - subject: subject.clone(), - successful_missions: 5, - rejected_submissions: 2, - reviewer_score: 100, - founder_score: 50, - total_earnings: 1000, - updated_at, - }; + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); - client.set_profile(&profile); + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - let retrieved_profile = client.get_profile(&subject); - assert_eq!(retrieved_profile.subject, subject); - assert_eq!(retrieved_profile.successful_missions, 5); - assert_eq!(retrieved_profile.rejected_submissions, 2); - assert_eq!(retrieved_profile.reviewer_score, 100); - assert_eq!(retrieved_profile.founder_score, 50); - assert_eq!(retrieved_profile.total_earnings, 1000); - assert_eq!(retrieved_profile.updated_at, updated_at); + client.revoke_attestation(&unauthorized, &attestation_id); } #[test] -fn test_update_profile() { - let (env, _contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &_contract_id); +#[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 profile = Profile { - subject: subject.clone(), - successful_missions: 5, - rejected_submissions: 2, - reviewer_score: 100, - founder_score: 50, - total_earnings: 1000, - updated_at: env.ledger().timestamp(), - }; - - client.set_profile(&profile); - - // Update the profile - let updated_profile = Profile { - subject: subject.clone(), - successful_missions: 10, - rejected_submissions: 3, - reviewer_score: 150, - founder_score: 75, - total_earnings: 2000, - updated_at: env.ledger().timestamp(), - }; + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); - client.set_profile(&updated_profile); + let attestation_id = client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); - let retrieved_profile = client.get_profile(&subject); - assert_eq!(retrieved_profile.successful_missions, 10); - assert_eq!(retrieved_profile.rejected_submissions, 3); - assert_eq!(retrieved_profile.reviewer_score, 150); - assert_eq!(retrieved_profile.founder_score, 75); - assert_eq!(retrieved_profile.total_earnings, 2000); + client.revoke_attestation(&issuer, &attestation_id); + client.revoke_attestation(&issuer, &attestation_id); } #[test] -fn test_profile_exists() { - let (env, _contract_id, _admin) = setup_test_env(); - let client = QuidReputationContractClient::new(&env, &_contract_id); +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!(!client.profile_exists(&subject)); + assert_eq!(client.get_attestation_count(), 0); - let profile = Profile { - subject: subject.clone(), - successful_missions: 0, - rejected_submissions: 0, - reviewer_score: 0, - founder_score: 0, - total_earnings: 0, - updated_at: env.ledger().timestamp(), - }; + let attestation_type = String::from_str(&env, "skill"); + let data_cid = String::from_str(&env, "QmTest123"); - client.set_profile(&profile); + client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); + assert_eq!(client.get_attestation_count(), 1); - assert!(client.profile_exists(&subject)); + client.issue_attestation(&issuer, &subject, &attestation_type, &data_cid); + assert_eq!(client.get_attestation_count(), 2); } #[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); +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); - client.get_profile(&subject); -========= -// ------------------------------------------------------------------------- -// Profile helper tests -// ------------------------------------------------------------------------- + + 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_get_profile_not_found() { +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 result = client.try_get_profile(&subject); - assert_eq!(result, Err(Ok(ReputationError::ProfileNotFound))); + + 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); } #[test] @@ -355,7 +271,6 @@ fn test_update_profile() { client.set_profile(&profile); - // Update the profile let updated_profile = Profile { subject: subject.clone(), score: 225, @@ -401,31 +316,3 @@ fn test_get_profile_not_found() { let subject = Address::generate(&env); client.get_profile(&subject); } - -#[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); - - // Revoke the attestation - client.revoke_attestation(&issuer, &attestation_id); - - // Verify the event was published - let events = env.events().all(); - let event = events.last().unwrap(); - - // Check that the event contains the attestation_id and revoked_by - assert_eq!(event.0, contract_id); - - // The event topics should contain "attestation" and "revoked" - let topics = event.1.clone(); - assert_eq!(topics.len(), 2); -} diff --git a/quid-contract/contracts/quid-reputation/src/types.rs b/quid-contract/contracts/quid-reputation/src/types.rs index 2e2a8dd..7f2f3a3 100644 --- a/quid-contract/contracts/quid-reputation/src/types.rs +++ b/quid-contract/contracts/quid-reputation/src/types.rs @@ -1,26 +1,3 @@ -use soroban_sdk::{contracttype, Address}; - -/// On-chain reputation profile for a single user address. -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ReputationProfile { - /// The wallet this profile belongs to. - pub owner: Address, - /// Number of successfully completed missions / approved submissions. - pub success_count: u32, - /// Number of rejected submissions. - pub rejection_count: u32, - /// Ledger timestamp of the last mutation. - pub last_updated: u64, -} - -/// Storage keys used by the reputation contract. -#[contracttype] -pub enum DataKey { - /// Admin address that is allowed to mutate profiles. - Admin, - /// Per-user reputation profile, keyed by wallet address. - Profile(Address), use soroban_sdk::{contracttype, Address, String}; #[contracttype] @@ -29,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, } @@ -58,7 +33,6 @@ pub enum DataKey { Profile(Address), Attestation(u64), AttestationCount, - Admin, } #[contracttype] From c74105e9db8070ccd78f7376ee901894921c5324 Mon Sep 17 00:00:00 2001 From: Birdmannn Date: Sat, 6 Jun 2026 09:32:41 +0100 Subject: [PATCH 5/5] ci --- .../quid-milestone-escrow/src/lib.rs | 100 +++---- .../contracts/quid-reputation/src/error.rs | 13 +- .../contracts/quid-reputation/src/lib.rs | 86 +----- .../contracts/quid-reputation/src/test.rs | 256 ++++++++---------- .../contracts/quid-reputation/src/types.rs | 20 +- 5 files changed, 165 insertions(+), 310 deletions(-) diff --git a/quid-contract/contracts/quid-milestone-escrow/src/lib.rs b/quid-contract/contracts/quid-milestone-escrow/src/lib.rs index 8ea8fb1..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}; @@ -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, @@ -62,14 +98,12 @@ impl QuidMilestoneEscrowContract { if total_amount <= 0 { return Err(MilestoneEscrowError::InvalidAmount); } - - // Transfer funds from sponsor into the contract + token::Client::new(&env, &token).transfer( &sponsor, env.current_contract_address(), &total_amount, ); - // let mut count: u64 = env .storage() @@ -82,14 +116,6 @@ impl QuidMilestoneEscrowContract { let program_id = count; let created_at = env.ledger().timestamp(); - token::Client::new(&env, &token).transfer( - &sponsor, - env.current_contract_address(), - &total_amount, - ); - - let program_id = Self::get_next_program_id(&env); - let status = ProgramStatus::Active; let program = Program { id: program_id, sponsor: sponsor.clone(), @@ -101,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() @@ -316,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 3db3a3e..fe504e3 100644 --- a/quid-contract/contracts/quid-reputation/src/error.rs +++ b/quid-contract/contracts/quid-reputation/src/error.rs @@ -4,13 +4,8 @@ use soroban_sdk::contracterror; #[derive(Copy, Clone, Debug, Eq, PartialEq)] 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 f2c650e..76a7b5e 100644 --- a/quid-contract/contracts/quid-reputation/src/lib.rs +++ b/quid-contract/contracts/quid-reputation/src/lib.rs @@ -24,7 +24,6 @@ impl QuidReputationContract { // Admin bootstrap // ------------------------------------------------------------------------- - /// Initialize the contract with an admin address. May only be called once. pub fn initialize(env: Env, admin: Address) -> Result<(), ReputationError> { admin.require_auth(); @@ -36,7 +35,6 @@ impl QuidReputationContract { Ok(()) } - /// Get the admin address. pub fn get_admin(env: Env) -> Result { env.storage() .instance() @@ -48,29 +46,15 @@ impl QuidReputationContract { // Attestations // ------------------------------------------------------------------------- - /// Issue a new attestation. 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(); @@ -94,17 +78,9 @@ impl QuidReputationContract { 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 { env.storage() .persistent() @@ -112,7 +88,6 @@ impl QuidReputationContract { .ok_or(ReputationError::AttestationNotFound) } - /// Revoke an attestation (issuer or admin only). pub fn revoke_attestation( env: Env, caller: Address, @@ -122,8 +97,6 @@ impl QuidReputationContract { let mut attestation = Self::get_attestation(env.clone(), attestation_id)?; - attestation.issuer.require_auth(); - if attestation.revoked { return Err(ReputationError::AlreadyRevoked); } @@ -134,7 +107,6 @@ impl QuidReputationContract { } attestation.revoked = true; - env.storage() .persistent() .set(&DataKey::Attestation(attestation_id), &attestation); @@ -154,7 +126,6 @@ impl QuidReputationContract { Ok(()) } - /// Get the total number of attestations. pub fn get_attestation_count(env: Env) -> u64 { env.storage() .instance() @@ -162,14 +133,16 @@ impl QuidReputationContract { .unwrap_or(0) } - /// Check if an attestation exists. pub fn attestation_exists(env: Env, attestation_id: u64) -> bool { env.storage() .persistent() .has(&DataKey::Attestation(attestation_id)) } - /// Get a profile by subject address. + // ------------------------------------------------------------------------- + // Profiles + // ------------------------------------------------------------------------- + pub fn get_profile(env: Env, subject: Address) -> Result { env.storage() .persistent() @@ -177,7 +150,6 @@ impl QuidReputationContract { .ok_or(ReputationError::ProfileNotFound) } - /// Create or update a profile. pub fn set_profile(env: Env, profile: Profile) -> Result<(), ReputationError> { profile.subject.require_auth(); @@ -194,7 +166,6 @@ impl QuidReputationContract { Ok(()) } - /// Check if a profile exists. pub fn profile_exists(env: Env, subject: Address) -> bool { env.storage().persistent().has(&DataKey::Profile(subject)) } @@ -214,7 +185,7 @@ impl QuidReputationContract { } // ------------------------------------------------------------------------- -// Internal helpers +// Internal helpers used by tests // ------------------------------------------------------------------------- #[allow(dead_code)] @@ -256,45 +227,4 @@ impl QuidReputationContract { } } -#[allow(dead_code)] -impl QuidReputationContract { - pub(crate) fn require_admin(env: &Env, caller: &Address) -> Result<(), QuidError> { - let admin: Address = env - .storage() - .persistent() - .get(&DataKey::Admin) - .ok_or(QuidError::AdminNotSet)?; - - admin.require_auth(); - - if *caller != admin { - return Err(QuidError::NotAuthorized); - } - - Ok(()) - } - - pub(crate) fn store_profile(env: &Env, profile: &Profile) { - let key = DataKey::Profile(profile.subject.clone()); - env.storage().persistent().set(&key, profile); - env.storage() - .persistent() - .extend_ttl(&key, PROFILE_TTL_LEDGERS, PROFILE_TTL_LEDGERS); - } - - pub(crate) fn load_or_default(env: &Env, subject: Address) -> Profile { - env.storage() - .persistent() - .get(&DataKey::Profile(subject.clone())) - .unwrap_or(Profile { - subject, - score: 0, - successful_missions: 0, - missions_created: 0, - total_earnings: 0, - updated_at: env.ledger().timestamp(), - }) - } -} - mod test; diff --git a/quid-contract/contracts/quid-reputation/src/test.rs b/quid-contract/contracts/quid-reputation/src/test.rs index ba9dbd3..d780681 100644 --- a/quid-contract/contracts/quid-reputation/src/test.rs +++ b/quid-contract/contracts/quid-reputation/src/test.rs @@ -1,64 +1,59 @@ #![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) -} - -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); + 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_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); 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); @@ -204,160 +199,131 @@ fn test_revoke_attestation_publishes_event() { let topics = event.1.clone(); assert_eq!(topics.len(), 2); - assert!(!attestation.revoked); } -#[test] -fn test_issue_attestation_with_expiry() { - let (env, _admin, client) = setup_test_env(); - 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_id = client.issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "expert"), - &String::from_str(&env, "Expert reviewer"), - &None, - &Some(expiry), - ); - - let attestation = client.get_attestation(&attestation_id); - assert_eq!(attestation.expires_at, Some(expiry)); -} +// ------------------------------------------------------------------------- +// Profile tests +// ------------------------------------------------------------------------- #[test] -fn test_revoke_attestation() { - let (env, _admin, client) = setup_test_env(); - let issuer = Address::generate(&env); - let subject = Address::generate(&env); +fn test_store_and_get_profile() { + let (env, contract_id, _admin) = setup_test_env(); + let client = QuidReputationContractClient::new(&env, &contract_id); - let attestation_id = client.issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "contributor"), - &String::from_str(&env, "Active contributor"), - &None, - &None, - ); + let subject = Address::generate(&env); - client.revoke_attestation(&attestation_id); + 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 attestation = client.get_attestation(&attestation_id); - assert!(attestation.revoked); + let fetched = client.get_profile(&subject); + assert_eq!(fetched.score, 42); + assert_eq!(fetched.missions_completed, 3); + assert_eq!(fetched.missions_created, 1); } #[test] -fn test_empty_label_fails() { - let (env, _admin, client) = setup_test_env(); - let issuer = Address::generate(&env); - let subject = Address::generate(&env); +fn test_load_or_default_returns_zeroed_profile() { + let (env, contract_id, _admin) = setup_test_env(); - let result = client.try_issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "contributor"), - &String::from_str(&env, ""), - &None, - &None, - ); + let subject = Address::generate(&env); - assert!(result.is_err()); + 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); + }); } #[test] -fn test_invalid_expiry_time_fails() { - let (env, _admin, client) = setup_test_env(); - let issuer = Address::generate(&env); +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 now = env.ledger().timestamp(); - let past_expiry = now; // Expiry <= now should fail + let profile = Profile { + subject: subject.clone(), + score: 150, + missions_completed: 5, + missions_created: 2, + }; - let result = client.try_issue_attestation( - &issuer, - &subject, - &String::from_str(&env, "contributor"), - &String::from_str(&env, "Active contributor"), - &None, - &Some(past_expiry), - ); + client.set_profile(&profile); - assert!(result.is_err()); + 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); } -// ------------------------------------------------------------------------- -// Profile helper tests -// ------------------------------------------------------------------------- - #[test] -fn test_get_profile_not_found() { - 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); - let result = client.try_get_profile(&subject); - assert_eq!(result, Err(Ok(QuidError::ProfileNotFound))); -} -#[test] -fn test_store_and_get_profile() { - let (env, _admin, client) = setup_test_env(); - let subject = Address::generate(&env); - - client.increment_success(&subject, &0); // ensure profile exists + let profile = Profile { + subject: subject.clone(), + score: 100, + missions_completed: 5, + missions_created: 2, + }; - 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); -} + client.set_profile(&profile); -#[test] -fn test_load_or_default_returns_zeroed_profile() { - let (env, _admin, client) = setup_test_env(); + let updated_profile = Profile { + subject: subject.clone(), + score: 225, + missions_completed: 10, + missions_created: 3, + }; - let subject = Address::generate(&env); + client.set_profile(&updated_profile); - // If profile doesn't exist, it should return error. - let result = client.try_get_profile(&subject); - assert_eq!(result, Err(Ok(QuidError::ProfileNotFound))); + 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); } -// ------------------------------------------------------------------------- -// increment_success tests -// ------------------------------------------------------------------------- - #[test] -fn test_increment_success() { - 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); - // First increment - client.increment_success(&subject, &100); + assert!(!client.profile_exists(&subject)); - let fetched = client.get_profile(&subject); - assert_eq!(fetched.successful_missions, 1); - assert_eq!(fetched.total_earnings, 100); + let profile = Profile { + subject: subject.clone(), + score: 0, + missions_completed: 0, + missions_created: 0, + }; - // Second increment - client.increment_success(&subject, &50); + client.set_profile(&profile); - let fetched2 = client.get_profile(&subject); - assert_eq!(fetched2.successful_missions, 2); - assert_eq!(fetched2.total_earnings, 150); + assert!(client.profile_exists(&subject)); } #[test] -fn test_increment_success_invalid_reward() { - let (env, _admin, client) = setup_test_env(); +#[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); - - let res = client.try_increment_success(&subject, &-10); - assert_eq!(res, Err(Ok(QuidError::InvalidRewardAmount))); + 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 b88f082..d7deea2 100644 --- a/quid-contract/contracts/quid-reputation/src/types.rs +++ b/quid-contract/contracts/quid-reputation/src/types.rs @@ -16,18 +16,10 @@ 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] @@ -37,14 +29,4 @@ pub enum DataKey { Profile(Address), Attestation(u64), AttestationCount, - Admin, - /// Per-subject reputation profile. - Profile(Address), -} - -#[contracttype] -pub enum AttestationKind { - Contributor, - Expert, - Reviewer, }