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,
}