From 3b879c9103aa305c065f681ed8caaf1cf4c77268 Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:12:29 +0100 Subject: [PATCH 1/9] Create Cargo.toml --- contracts/package/reputation-decay/Cargo.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 contracts/package/reputation-decay/Cargo.toml diff --git a/contracts/package/reputation-decay/Cargo.toml b/contracts/package/reputation-decay/Cargo.toml new file mode 100644 index 00000000..2d2c19ef --- /dev/null +++ b/contracts/package/reputation-decay/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "reputation-decay" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = { version = "22.0.0", features = ["alloc"] } + +[dev-dependencies] +soroban-sdk = { version = "22.0.0", features = ["testutils"] } From 9ec6da4e215d932971f6624bad1db6be074573d0 Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:13:29 +0100 Subject: [PATCH 2/9] Create lib.rs --- contracts/package/reputation-decay/src/lib.rs | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 contracts/package/reputation-decay/src/lib.rs diff --git a/contracts/package/reputation-decay/src/lib.rs b/contracts/package/reputation-decay/src/lib.rs new file mode 100644 index 00000000..e0877670 --- /dev/null +++ b/contracts/package/reputation-decay/src/lib.rs @@ -0,0 +1,217 @@ +#![no_std] +use soroban_sdk::{contract, contractimpl, contracttype, Address, BytesN, Env, Vec}; + +#[contracttype] +enum DataKey { + Config, + Ratings(BytesN<32>), // keyed by user_id +} + +#[contracttype] +#[derive(Clone)] +pub struct DecayConfig { + /// Seconds before a rating is considered "recent" (default: 365 days) + pub recent_threshold_secs: u64, + /// Seconds before a rating is considered "old" (default: 730 days) + pub old_threshold_secs: u64, + /// Weight in basis points for recent ratings (default: 10000 = 100%) + pub recent_weight_bps: u32, + /// Weight in basis points for mid-age ratings (default: 7000 = 70%) + pub mid_weight_bps: u32, + /// Weight in basis points for old ratings (default: 4000 = 40%) + pub old_weight_bps: u32, +} + +impl Default for DecayConfig { + fn default() -> Self { + Self { + recent_threshold_secs: 365 * 24 * 3600, + old_threshold_secs: 730 * 24 * 3600, + recent_weight_bps: 10_000, + mid_weight_bps: 7_000, + old_weight_bps: 4_000, + } + } +} + +/// A single rating entry stored on-chain. +#[contracttype] +#[derive(Clone)] +pub struct RatingEntry { + /// Raw score 0–1000 + pub score: u32, + /// Ledger timestamp when the rating was submitted + pub created_at: u64, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum ContractError { + AlreadyInitialized = 1, + NotInitialized = 2, + InvalidScore = 3, + NoRatings = 4, +} + +#[contract] +pub struct ReputationDecayContract; + +#[contractimpl] +impl ReputationDecayContract { + /// One-time initialisation. Pass `None` to use default thresholds. + pub fn initialize(env: Env, config: Option) -> Result<(), ContractError> { + if env.storage().instance().has(&DataKey::Config) { + return Err(ContractError::AlreadyInitialized); + } + let cfg = config.unwrap_or_default(); + env.storage().instance().set(&DataKey::Config, &cfg); + Ok(()) + } + + /// Submit a rating (0–1000) for a user at the current ledger time. + pub fn submit_rating( + env: Env, + user_id: BytesN<32>, + score: u32, + ) -> Result<(), ContractError> { + if score > 1000 { + return Err(ContractError::InvalidScore); + } + let now = env.ledger().timestamp(); + let entry = RatingEntry { score, created_at: now }; + + let mut ratings: Vec = env + .storage() + .persistent() + .get(&DataKey::Ratings(user_id.clone())) + .unwrap_or_else(|| Vec::new(&env)); + + ratings.push_back(entry); + env.storage() + .persistent() + .set(&DataKey::Ratings(user_id), &ratings); + Ok(()) + } + + /// Returns the decay-weighted composite score (0–1000) for a user. + pub fn get_decayed_score( + env: Env, + user_id: BytesN<32>, + current_ledger_time: u64, + ) -> Result { + let cfg: DecayConfig = env + .storage() + .instance() + .get(&DataKey::Config) + .ok_or(ContractError::NotInitialized)?; + + let ratings: Vec = env + .storage() + .persistent() + .get(&DataKey::Ratings(user_id)) + .unwrap_or_else(|| Vec::new(&env)); + + if ratings.is_empty() { + return Err(ContractError::NoRatings); + } + + let mut weighted_sum: u64 = 0; + let mut weight_total: u64 = 0; + + for entry in ratings.iter() { + let age_secs = current_ledger_time.saturating_sub(entry.created_at); + + let weight_bps = if age_secs < cfg.recent_threshold_secs { + cfg.recent_weight_bps + } else if age_secs < cfg.old_threshold_secs { + cfg.mid_weight_bps + } else { + cfg.old_weight_bps + } as u64; + + weighted_sum += entry.score as u64 * weight_bps; + weight_total += weight_bps; + } + + if weight_total == 0 { + return Ok(0); + } + + Ok((weighted_sum / weight_total) as u32) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{testutils::Ledger, BytesN, Env}; + + const DAY: u64 = 24 * 3600; + + fn setup() -> (Env, ReputationDecayContractClient<'static>) { + let env = Env::default(); + let id = env.register(ReputationDecayContract, ()); + let client = ReputationDecayContractClient::new(&env, &id); + env.as_contract(&id, || { client.initialize(&None).unwrap(); }); + (env, client) + } + + fn user(env: &Env) -> BytesN<32> { BytesN::from_array(env, &[1u8; 32]) } + + #[test] + fn test_recent_rating_full_weight() { + let (env, client) = setup(); + let u = user(&env); + // Submit rating at t=0, query at t=0 (0 days old → 100% weight) + env.ledger().set_timestamp(0); + env.as_contract(&client.address, || { + ReputationDecayContract::submit_rating(env.clone(), u.clone(), 800).unwrap(); + }); + let score = env.as_contract(&client.address, || { + ReputationDecayContract::get_decayed_score(env.clone(), u.clone(), 0).unwrap() + }); + assert_eq!(score, 800); + } + + #[test] + fn test_mid_age_rating_70_percent() { + let (env, client) = setup(); + let u = user(&env); + // Rating submitted at t=0, queried at t=366 days (mid-age → 70% weight) + env.ledger().set_timestamp(0); + env.as_contract(&client.address, || { + ReputationDecayContract::submit_rating(env.clone(), u.clone(), 1000).unwrap(); + }); + let query_time = 366 * DAY; + let score = env.as_contract(&client.address, || { + ReputationDecayContract::get_decayed_score(env.clone(), u.clone(), query_time).unwrap() + }); + // 1000 * 7000 / 7000 = 1000 (only one rating, weight cancels out) + assert_eq!(score, 1000); + } + + #[test] + fn test_old_rating_40_percent_mixed() { + let (env, client) = setup(); + let u = user(&env); + + // Two ratings: one recent (score=1000), one old at 731 days (score=500) + // Recent: 1000 * 10000 = 10_000_000 + // Old: 500 * 4000 = 2_000_000 + // Total weight: 14000 → weighted avg = 12_000_000 / 14000 = 857 + env.ledger().set_timestamp(0); + env.as_contract(&client.address, || { + ReputationDecayContract::submit_rating(env.clone(), u.clone(), 500).unwrap(); + }); + let recent_time = 731 * DAY + 1; + env.ledger().set_timestamp(recent_time); + env.as_contract(&client.address, || { + ReputationDecayContract::submit_rating(env.clone(), u.clone(), 1000).unwrap(); + }); + + let score = env.as_contract(&client.address, || { + ReputationDecayContract::get_decayed_score(env.clone(), u.clone(), recent_time).unwrap() + }); + assert_eq!(score, 857); + } +} From 0c06044e3644d7e0c59f0ff9f8299c3281be19fb Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:13:53 +0100 Subject: [PATCH 3/9] Create Cargo.toml --- contracts/package/escrow-pausable/Cargo.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 contracts/package/escrow-pausable/Cargo.toml diff --git a/contracts/package/escrow-pausable/Cargo.toml b/contracts/package/escrow-pausable/Cargo.toml new file mode 100644 index 00000000..4176ee98 --- /dev/null +++ b/contracts/package/escrow-pausable/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "escrow-pausable" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = { version = "22.0.0", features = ["alloc"] } + +[dev-dependencies] +soroban-sdk = { version = "22.0.0", features = ["testutils"] } From 39da720f704a8aa89f95d0cd4f1d4c368bda06d0 Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:15:17 +0100 Subject: [PATCH 4/9] Create lib.rs --- contracts/package/escrow-pausable/src/lib.rs | 250 +++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 contracts/package/escrow-pausable/src/lib.rs diff --git a/contracts/package/escrow-pausable/src/lib.rs b/contracts/package/escrow-pausable/src/lib.rs new file mode 100644 index 00000000..5f66160c --- /dev/null +++ b/contracts/package/escrow-pausable/src/lib.rs @@ -0,0 +1,250 @@ +#![no_std] +use soroban_sdk::{ + contract, contractimpl, contracttype, + symbol_short, Address, BytesN, Env, Symbol, +}; + +#[contracttype] +enum DataKey { + Admin, + Paused, + Escrow(BytesN<32>), +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum EscrowStatus { + Funded, + Released, + Refunded, + Disputed, +} + +#[contracttype] +#[derive(Clone)] +pub struct EscrowRecord { + pub depositor: Address, + pub amount: i128, + pub status: EscrowStatus, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum ContractError { + Unauthorized = 1, + ContractPaused = 2, + EscrowNotFound = 3, + AlreadyFunded = 4, + InvalidState = 5, + AlreadyInitialized = 6, +} + +#[contract] +pub struct EscrowPausableContract; + +#[contractimpl] +impl EscrowPausableContract { + pub fn initialize(env: Env, admin: Address) -> Result<(), ContractError> { + if env.storage().instance().has(&DataKey::Admin) { + return Err(ContractError::AlreadyInitialized); + } + env.storage().instance().set(&DataKey::Admin, &admin); + env.storage().instance().set(&DataKey::Paused, &false); + Ok(()) + } + + pub fn pause(env: Env, caller: Address) -> Result<(), ContractError> { + Self::require_admin(&env, &caller)?; + env.storage().instance().set(&DataKey::Paused, &true); + let now = env.ledger().timestamp(); + env.events().publish( + (Symbol::new(&env, "ContractPaused"), caller.clone()), + (caller, now), + ); + Ok(()) + } + + pub fn unpause(env: Env, caller: Address) -> Result<(), ContractError> { + Self::require_admin(&env, &caller)?; + env.storage().instance().set(&DataKey::Paused, &false); + Ok(()) + } + + pub fn is_paused(env: Env) -> bool { + env.storage() + .instance() + .get(&DataKey::Paused) + .unwrap_or(false) + } + + /// Deposit funds — blocked when paused. + pub fn fund_escrow( + env: Env, + escrow_id: BytesN<32>, + depositor: Address, + amount: i128, + ) -> Result<(), ContractError> { + Self::require_not_paused(&env)?; + + if env.storage().persistent().has(&DataKey::Escrow(escrow_id.clone())) { + return Err(ContractError::AlreadyFunded); + } + + let record = EscrowRecord { depositor, amount, status: EscrowStatus::Funded }; + env.storage().persistent().set(&DataKey::Escrow(escrow_id), &record); + Ok(()) + } + + /// Open a dispute — blocked when paused. + pub fn open_dispute( + env: Env, + escrow_id: BytesN<32>, + ) -> Result<(), ContractError> { + Self::require_not_paused(&env)?; + let mut record = Self::get_escrow_record(&env, &escrow_id)?; + if record.status != EscrowStatus::Funded { + return Err(ContractError::InvalidState); + } + record.status = EscrowStatus::Disputed; + env.storage().persistent().set(&DataKey::Escrow(escrow_id), &record); + Ok(()) + } + + /// Release funds to recipient — works even when paused. + pub fn release_funds( + env: Env, + caller: Address, + escrow_id: BytesN<32>, + _recipient: Address, + ) -> Result { + Self::require_admin(&env, &caller)?; + let mut record = Self::get_escrow_record(&env, &escrow_id)?; + if record.status != EscrowStatus::Funded && record.status != EscrowStatus::Disputed { + return Err(ContractError::InvalidState); + } + let amount = record.amount; + record.status = EscrowStatus::Released; + env.storage().persistent().set(&DataKey::Escrow(escrow_id), &record); + Ok(amount) + } + + /// Refund to depositor — works even when paused. + pub fn refund( + env: Env, + caller: Address, + escrow_id: BytesN<32>, + ) -> Result { + Self::require_admin(&env, &caller)?; + let mut record = Self::get_escrow_record(&env, &escrow_id)?; + if record.status != EscrowStatus::Funded && record.status != EscrowStatus::Disputed { + return Err(ContractError::InvalidState); + } + let amount = record.amount; + record.status = EscrowStatus::Refunded; + env.storage().persistent().set(&DataKey::Escrow(escrow_id), &record); + Ok(amount) + } + + fn require_admin(env: &Env, caller: &Address) -> Result<(), ContractError> { + caller.require_auth(); + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .ok_or(ContractError::Unauthorized)?; + if *caller != admin { + return Err(ContractError::Unauthorized); + } + Ok(()) + } + + fn require_not_paused(env: &Env) -> Result<(), ContractError> { + if env.storage().instance().get(&DataKey::Paused).unwrap_or(false) { + return Err(ContractError::ContractPaused); + } + Ok(()) + } + + fn get_escrow_record(env: &Env, escrow_id: &BytesN<32>) -> Result { + env.storage() + .persistent() + .get(&DataKey::Escrow(escrow_id.clone())) + .ok_or(ContractError::EscrowNotFound) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{testutils::Address as _, Address, BytesN, Env}; + + fn setup() -> (Env, EscrowPausableContractClient<'static>, Address) { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register(EscrowPausableContract, ()); + let client = EscrowPausableContractClient::new(&env, &id); + let admin = Address::generate(&env); + client.initialize(&admin).unwrap(); + (env, client, admin) + } + + fn escrow_id(env: &Env) -> BytesN<32> { BytesN::from_array(env, &[2u8; 32]) } + + #[test] + fn test_fund_fails_when_paused() { + let (env, client, admin) = setup(); + client.pause(&admin).unwrap(); + let depositor = Address::generate(&env); + let err = client.try_fund_escrow(&escrow_id(&env), &depositor, &1000).unwrap_err(); + assert_eq!(err.unwrap(), ContractError::ContractPaused); + } + + #[test] + fn test_dispute_fails_when_paused() { + let (env, client, admin) = setup(); + let depositor = Address::generate(&env); + client.fund_escrow(&escrow_id(&env), &depositor, &1000).unwrap(); + client.pause(&admin).unwrap(); + let err = client.try_open_dispute(&escrow_id(&env)).unwrap_err(); + assert_eq!(err.unwrap(), ContractError::ContractPaused); + } + + #[test] + fn test_release_succeeds_when_paused() { + let (env, client, admin) = setup(); + let depositor = Address::generate(&env); + let recipient = Address::generate(&env); + client.fund_escrow(&escrow_id(&env), &depositor, &1000).unwrap(); + client.pause(&admin).unwrap(); + let amount = client.release_funds(&admin, &escrow_id(&env), &recipient).unwrap(); + assert_eq!(amount, 1000); + } + + #[test] + fn test_refund_succeeds_when_paused() { + let (env, client, admin) = setup(); + let depositor = Address::generate(&env); + client.fund_escrow(&escrow_id(&env), &depositor, &500).unwrap(); + client.pause(&admin).unwrap(); + let amount = client.refund(&admin, &escrow_id(&env)).unwrap(); + assert_eq!(amount, 500); + } + + #[test] + fn test_non_admin_pause_unauthorized() { + let (env, client, _admin) = setup(); + let rogue = Address::generate(&env); + let err = client.try_pause(&rogue).unwrap_err(); + assert_eq!(err.unwrap(), ContractError::Unauthorized); + } + + #[test] + fn test_is_paused_reflects_state() { + let (env, client, admin) = setup(); + assert!(!client.is_paused()); + client.pause(&admin).unwrap(); + assert!(client.is_paused()); + client.unpause(&admin).unwrap(); + assert!(!client.is_paused()); + } +} From bf366a1823f1b1cf2d0efbecc125faa93075af71 Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:15:35 +0100 Subject: [PATCH 5/9] Create Cargo.toml --- .../package/cross-contract-validator/Cargo.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 contracts/package/cross-contract-validator/Cargo.toml diff --git a/contracts/package/cross-contract-validator/Cargo.toml b/contracts/package/cross-contract-validator/Cargo.toml new file mode 100644 index 00000000..06862427 --- /dev/null +++ b/contracts/package/cross-contract-validator/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cross-contract-validator" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = { version = "22.0.0", features = ["alloc"] } + +[dev-dependencies] +soroban-sdk = { version = "22.0.0", features = ["testutils"] } From 703214075317d424d752277030278f679516e57e Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:16:43 +0100 Subject: [PATCH 6/9] Create lib.rs --- .../cross-contract-validator/src/lib.rs | 206 ++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 contracts/package/cross-contract-validator/src/lib.rs diff --git a/contracts/package/cross-contract-validator/src/lib.rs b/contracts/package/cross-contract-validator/src/lib.rs new file mode 100644 index 00000000..355a9e7c --- /dev/null +++ b/contracts/package/cross-contract-validator/src/lib.rs @@ -0,0 +1,206 @@ +#![no_std] +use soroban_sdk::{ + contract, contractimpl, contracttype, + Address, BytesN, Env, Symbol, +}; + +mod shipment_interface { + use soroban_sdk::{contractclient, BytesN, Env}; + #[contractclient(name = "ShipmentClient")] + pub trait ShipmentTrait { + fn get_status(env: Env, shipment_id: BytesN<32>) -> Symbol; + } +} + +mod escrow_interface { + use soroban_sdk::{contractclient, BytesN, Env}; + #[contractclient(name = "EscrowClient")] + pub trait EscrowTrait { + fn get_status(env: Env, escrow_id: BytesN<32>) -> Symbol; + fn release(env: Env, escrow_id: BytesN<32>); + } +} + +use escrow_interface::EscrowClient; +use shipment_interface::ShipmentClient; + +#[contracttype] +enum DataKey { + ShipmentContract, + EscrowContract, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum ValidationError { + ShipmentNotDelivered = 1, + EscrowNotFunded = 2, + NotInitialized = 3, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum ReleaseResult { + Released, + Blocked(ValidationError), +} + + +#[contract] +pub struct CrossContractValidator; + +#[contractimpl] +impl CrossContractValidator { + pub fn initialize( + env: Env, + shipment_contract: Address, + escrow_contract: Address, + ) { + env.storage().instance().set(&DataKey::ShipmentContract, &shipment_contract); + env.storage().instance().set(&DataKey::EscrowContract, &escrow_contract); + } + + /// Atomically validates shipment delivery and escrow funding before releasing. + /// Returns `ReleaseResult::Blocked(reason)` if either check fails. + pub fn validate_and_release( + env: Env, + shipment_id: BytesN<32>, + escrow_id: BytesN<32>, + ) -> ReleaseResult { + let shipment_addr: Address = match env + .storage() + .instance() + .get(&DataKey::ShipmentContract) + { + Some(a) => a, + None => return ReleaseResult::Blocked(ValidationError::NotInitialized), + }; + + let escrow_addr: Address = match env + .storage() + .instance() + .get(&DataKey::EscrowContract) + { + Some(a) => a, + None => return ReleaseResult::Blocked(ValidationError::NotInitialized), + }; + + // 1. Check shipment status + let shipment_status = + ShipmentClient::new(&env, &shipment_addr).get_status(&shipment_id); + if shipment_status != Symbol::new(&env, "DELIVERED") { + return ReleaseResult::Blocked(ValidationError::ShipmentNotDelivered); + } + + // 2. Check escrow status + let escrow_status = + EscrowClient::new(&env, &escrow_addr).get_status(&escrow_id); + if escrow_status != Symbol::new(&env, "FUNDED") { + return ReleaseResult::Blocked(ValidationError::EscrowNotFunded); + } + + // 3. Both checks passed — release + EscrowClient::new(&env, &escrow_addr).release(&escrow_id); + + ReleaseResult::Released + } +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{contract, contractimpl, testutils::Address as _, BytesN, Env, Symbol}; + + #[contract] + struct MockShipment; + #[contractimpl] + impl MockShipment { + pub fn get_status(_env: Env, _id: BytesN<32>) -> Symbol { + Symbol::new(&_env, "DELIVERED") + } + } + + #[contract] + struct MockShipmentNotDelivered; + #[contractimpl] + impl MockShipmentNotDelivered { + pub fn get_status(_env: Env, _id: BytesN<32>) -> Symbol { + Symbol::new(&_env, "IN_TRANSIT") + } + } + + #[contract] + struct MockEscrow; + #[contractimpl] + impl MockEscrow { + pub fn get_status(_env: Env, _id: BytesN<32>) -> Symbol { + Symbol::new(&_env, "FUNDED") + } + pub fn release(_env: Env, _id: BytesN<32>) {} + } + + #[contract] + struct MockEscrowNotFunded; + #[contractimpl] + impl MockEscrowNotFunded { + pub fn get_status(_env: Env, _id: BytesN<32>) -> Symbol { + Symbol::new(&_env, "RELEASED") + } + pub fn release(_env: Env, _id: BytesN<32>) {} + } + + fn ids(env: &Env) -> (BytesN<32>, BytesN<32>) { + (BytesN::from_array(env, &[1u8; 32]), BytesN::from_array(env, &[2u8; 32])) + } + + #[test] + fn test_happy_path_releases() { + let env = Env::default(); + env.mock_all_auths(); + + let shipment_id = env.register(MockShipment, ()); + let escrow_id = env.register(MockEscrow, ()); + let validator_id = env.register(CrossContractValidator, ()); + let client = CrossContractValidatorClient::new(&env, &validator_id); + + client.initialize(&shipment_id, &escrow_id); + let (sid, eid) = ids(&env); + assert_eq!(client.validate_and_release(&sid, &eid), ReleaseResult::Released); + } + + #[test] + fn test_shipment_not_delivered_blocked() { + let env = Env::default(); + env.mock_all_auths(); + + let shipment_id = env.register(MockShipmentNotDelivered, ()); + let escrow_id = env.register(MockEscrow, ()); + let validator_id = env.register(CrossContractValidator, ()); + let client = CrossContractValidatorClient::new(&env, &validator_id); + + client.initialize(&shipment_id, &escrow_id); + let (sid, eid) = ids(&env); + assert_eq!( + client.validate_and_release(&sid, &eid), + ReleaseResult::Blocked(ValidationError::ShipmentNotDelivered) + ); + } + + #[test] + fn test_escrow_not_funded_blocked() { + let env = Env::default(); + env.mock_all_auths(); + + let shipment_id = env.register(MockShipment, ()); + let escrow_id = env.register(MockEscrowNotFunded, ()); + let validator_id = env.register(CrossContractValidator, ()); + let client = CrossContractValidatorClient::new(&env, &validator_id); + + client.initialize(&shipment_id, &escrow_id); + let (sid, eid) = ids(&env); + assert_eq!( + client.validate_and_release(&sid, &eid), + ReleaseResult::Blocked(ValidationError::EscrowNotFunded) + ); + } +} From c92b54ce4913f9e8a586f0b887c953bdd392b97a Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:17:09 +0100 Subject: [PATCH 7/9] Create Cargo.toml --- contracts/package/shipment-events/Cargo.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 contracts/package/shipment-events/Cargo.toml diff --git a/contracts/package/shipment-events/Cargo.toml b/contracts/package/shipment-events/Cargo.toml new file mode 100644 index 00000000..ab5c1581 --- /dev/null +++ b/contracts/package/shipment-events/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "shipment-events" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "rlib"] +doctest = false + +[dependencies] +soroban-sdk = { version = "22.0.0", features = ["alloc"] } + +[dev-dependencies] +soroban-sdk = { version = "22.0.0", features = ["testutils"] } From 8a33302eed000518822f20b2074bb4168348e632 Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:18:37 +0100 Subject: [PATCH 8/9] Create lib.rs --- contracts/package/shipment-events/src/lib.rs | 262 +++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 contracts/package/shipment-events/src/lib.rs diff --git a/contracts/package/shipment-events/src/lib.rs b/contracts/package/shipment-events/src/lib.rs new file mode 100644 index 00000000..45c175ce --- /dev/null +++ b/contracts/package/shipment-events/src/lib.rs @@ -0,0 +1,262 @@ +#![no_std] +use soroban_sdk::{ + contract, contractimpl, contracttype, + Address, BytesN, Env, Symbol, +}; + +#[contracttype] +enum DataKey { + Shipment(BytesN<32>), +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum ShipmentStatus { + Created, + Accepted, + InTransit, + Delivered, + Completed, + Disputed, + Cancelled, +} + +#[contracttype] +#[derive(Clone)] +pub struct ShipmentRecord { + pub shipper: Address, + pub carrier: Address, + pub status: ShipmentStatus, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq)] +pub enum ContractError { + ShipmentNotFound = 1, + InvalidTransition = 2, + AlreadyExists = 3, +} + +fn emit( + env: &Env, + event_name: &str, + shipment_id: &BytesN<32>, + new_status: ShipmentStatus, + actor: &Address, +) { + let topic = (Symbol::new(env, event_name), shipment_id.clone()); + let payload = (shipment_id.clone(), new_status, actor.clone(), env.ledger().timestamp()); + env.events().publish(topic, payload); +} + +#[contract] +pub struct ShipmentEventsContract; + +#[contractimpl] +impl ShipmentEventsContract { + pub fn create_shipment( + env: Env, + shipment_id: BytesN<32>, + shipper: Address, + carrier: Address, + ) -> Result<(), ContractError> { + if env.storage().persistent().has(&DataKey::Shipment(shipment_id.clone())) { + return Err(ContractError::AlreadyExists); + } + let record = ShipmentRecord { shipper: shipper.clone(), carrier, status: ShipmentStatus::Created }; + env.storage().persistent().set(&DataKey::Shipment(shipment_id.clone()), &record); + emit(&env, "ShipmentCreated", &shipment_id, ShipmentStatus::Created, &shipper); + Ok(()) + } + + pub fn accept_shipment( + env: Env, + shipment_id: BytesN<32>, + actor: Address, + ) -> Result<(), ContractError> { + let mut r = Self::load(&env, &shipment_id)?; + if r.status != ShipmentStatus::Created { return Err(ContractError::InvalidTransition); } + r.status = ShipmentStatus::Accepted; + env.storage().persistent().set(&DataKey::Shipment(shipment_id.clone()), &r); + emit(&env, "ShipmentAccepted", &shipment_id, ShipmentStatus::Accepted, &actor); + Ok(()) + } + + pub fn mark_in_transit( + env: Env, + shipment_id: BytesN<32>, + actor: Address, + ) -> Result<(), ContractError> { + let mut r = Self::load(&env, &shipment_id)?; + if r.status != ShipmentStatus::Accepted { return Err(ContractError::InvalidTransition); } + r.status = ShipmentStatus::InTransit; + env.storage().persistent().set(&DataKey::Shipment(shipment_id.clone()), &r); + emit(&env, "InTransit", &shipment_id, ShipmentStatus::InTransit, &actor); + Ok(()) + } + + pub fn mark_delivered( + env: Env, + shipment_id: BytesN<32>, + actor: Address, + ) -> Result<(), ContractError> { + let mut r = Self::load(&env, &shipment_id)?; + if r.status != ShipmentStatus::InTransit { return Err(ContractError::InvalidTransition); } + r.status = ShipmentStatus::Delivered; + env.storage().persistent().set(&DataKey::Shipment(shipment_id.clone()), &r); + emit(&env, "Delivered", &shipment_id, ShipmentStatus::Delivered, &actor); + Ok(()) + } + + pub fn complete_shipment( + env: Env, + shipment_id: BytesN<32>, + actor: Address, + ) -> Result<(), ContractError> { + let mut r = Self::load(&env, &shipment_id)?; + if r.status != ShipmentStatus::Delivered { return Err(ContractError::InvalidTransition); } + r.status = ShipmentStatus::Completed; + env.storage().persistent().set(&DataKey::Shipment(shipment_id.clone()), &r); + emit(&env, "Completed", &shipment_id, ShipmentStatus::Completed, &actor); + Ok(()) + } + + pub fn open_dispute( + env: Env, + shipment_id: BytesN<32>, + actor: Address, + ) -> Result<(), ContractError> { + let mut r = Self::load(&env, &shipment_id)?; + if r.status != ShipmentStatus::Delivered && r.status != ShipmentStatus::InTransit { + return Err(ContractError::InvalidTransition); + } + r.status = ShipmentStatus::Disputed; + env.storage().persistent().set(&DataKey::Shipment(shipment_id.clone()), &r); + emit(&env, "Disputed", &shipment_id, ShipmentStatus::Disputed, &actor); + Ok(()) + } + + pub fn cancel_shipment( + env: Env, + shipment_id: BytesN<32>, + actor: Address, + ) -> Result<(), ContractError> { + let mut r = Self::load(&env, &shipment_id)?; + if r.status == ShipmentStatus::Completed || r.status == ShipmentStatus::Cancelled { + return Err(ContractError::InvalidTransition); + } + r.status = ShipmentStatus::Cancelled; + env.storage().persistent().set(&DataKey::Shipment(shipment_id.clone()), &r); + emit(&env, "Cancelled", &shipment_id, ShipmentStatus::Cancelled, &actor); + Ok(()) + } + + fn load(env: &Env, shipment_id: &BytesN<32>) -> Result { + env.storage() + .persistent() + .get(&DataKey::Shipment(shipment_id.clone())) + .ok_or(ContractError::ShipmentNotFound) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_sdk::{ + testutils::{Address as _, Events}, + Address, BytesN, Env, IntoVal, Symbol, + }; + + fn setup() -> (Env, ShipmentEventsContractClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let id = env.register(ShipmentEventsContract, ()); + let client = ShipmentEventsContractClient::new(&env, &id); + (env, client) + } + + fn sid(env: &Env) -> BytesN<32> { BytesN::from_array(env, &[9u8; 32]) } + + fn assert_event(env: &Env, expected_name: &str, shipment_id: &BytesN<32>) { + let events = env.events().all(); + let found = events.iter().any(|(_, topics, _)| { + // topics is a Vec; first topic is the Symbol + topics.iter().any(|t| { + t == Symbol::new(env, expected_name).into_val(env) + }) + }); + assert!(found, "event '{}' not found in event log", expected_name); + } + + #[test] + fn test_create_emits_event() { + let (env, client) = setup(); + let shipper = Address::generate(&env); + let carrier = Address::generate(&env); + client.create_shipment(&sid(&env), &shipper, &carrier).unwrap(); + assert_event(&env, "ShipmentCreated", &sid(&env)); + } + + #[test] + fn test_accept_emits_event() { + let (env, client) = setup(); + let shipper = Address::generate(&env); + let carrier = Address::generate(&env); + client.create_shipment(&sid(&env), &shipper, &carrier).unwrap(); + client.accept_shipment(&sid(&env), &carrier).unwrap(); + assert_event(&env, "ShipmentAccepted", &sid(&env)); + } + + #[test] + fn test_full_happy_path_events() { + let (env, client) = setup(); + let shipper = Address::generate(&env); + let carrier = Address::generate(&env); + let s = sid(&env); + client.create_shipment(&s, &shipper, &carrier).unwrap(); + client.accept_shipment(&s, &carrier).unwrap(); + client.mark_in_transit(&s, &carrier).unwrap(); + client.mark_delivered(&s, &carrier).unwrap(); + client.complete_shipment(&s, &shipper).unwrap(); + + for name in ["ShipmentCreated", "ShipmentAccepted", "InTransit", "Delivered", "Completed"] { + assert_event(&env, name, &s); + } + } + + #[test] + fn test_dispute_emits_event() { + let (env, client) = setup(); + let shipper = Address::generate(&env); + let carrier = Address::generate(&env); + let s = sid(&env); + client.create_shipment(&s, &shipper, &carrier).unwrap(); + client.accept_shipment(&s, &carrier).unwrap(); + client.mark_in_transit(&s, &carrier).unwrap(); + client.open_dispute(&s, &shipper).unwrap(); + assert_event(&env, "Disputed", &s); + } + + #[test] + fn test_cancel_emits_event() { + let (env, client) = setup(); + let shipper = Address::generate(&env); + let carrier = Address::generate(&env); + let s = sid(&env); + client.create_shipment(&s, &shipper, &carrier).unwrap(); + client.cancel_shipment(&s, &shipper).unwrap(); + assert_event(&env, "Cancelled", &s); + } + + #[test] + fn test_invalid_transition_rejected() { + let (env, client) = setup(); + let shipper = Address::generate(&env); + let carrier = Address::generate(&env); + let s = sid(&env); + client.create_shipment(&s, &shipper, &carrier).unwrap(); + // Can't go from Created → Delivered directly + let err = client.try_mark_delivered(&s, &carrier).unwrap_err(); + assert_eq!(err.unwrap(), ContractError::InvalidTransition); + } +} From 400364a401e8af1a09fc106dbd6b49f493768ffd Mon Sep 17 00:00:00 2001 From: Halima Date: Sat, 30 May 2026 12:20:53 +0100 Subject: [PATCH 9/9] Update Cargo.toml --- contracts/Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 8a582c49..04afccd2 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -6,6 +6,10 @@ members = [ "escrow", "document", "reputation", + "reputation-decay", + "escrow-pausable", + "cross-contract-validator", + "shipment-events", ] [profile.release]