From 3229a38ab4f579cf5c6211147a1c30c5d034bab7 Mon Sep 17 00:00:00 2001 From: Gozirimdev Date: Thu, 23 Apr 2026 11:49:28 +0100 Subject: [PATCH 1/3] feat(escrow): emit release event --- contracts/escrow/src/lib.rs | 74 +++++++++++++++++++++++++++++++++++-- docs/contracts/escrow.md | 37 +++++++++++++++++++ 2 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 docs/contracts/escrow.md diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 9413b0de..eb8688eb 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -95,6 +95,8 @@ pub struct DepositEvent { pub amount: i128, pub deposited_at: u64, } + +#[contracttype] #[derive(Clone)] pub struct ReleaseMilestoneEvent { pub job_id: u64, @@ -103,6 +105,18 @@ pub struct ReleaseMilestoneEvent { pub released_at: u64, } +#[contracttype] +#[derive(Clone)] +pub struct ReleaseEvent { + pub job_id: u64, + pub released_by: Address, + pub released_to: Address, + pub milestone_index: u32, + pub amount: i128, + pub total_released: i128, + pub released_at: u64, +} + #[contracttype] #[derive(Clone)] pub struct OpenDisputeEvent { @@ -362,7 +376,7 @@ impl EscrowContract { milestone.status = MilestoneStatus::Released; job.milestones.set(milestone_index, milestone.clone()); - job.released_amount += milestone.amount; + job.released_amount = job.released_amount.saturating_add(milestone.amount); job.status = EscrowStatus::WorkInProgress; let token_client = token::Client::new(&env, &job.token); @@ -377,6 +391,17 @@ impl EscrowContract { } env.storage().persistent().set(&key, &job); + + let evt = ReleaseEvent { + job_id, + released_by: caller.clone(), + released_to: job.freelancer.clone(), + milestone_index, + amount: milestone.amount, + total_released: job.released_amount, + released_at: env.ledger().timestamp(), + }; + env.events().publish(("escrow", "ReleaseEvent"), evt); } /// Either party opens a dispute, locking remaining funds. @@ -556,8 +581,8 @@ impl EscrowContract { #[cfg(test)] mod test { use super::*; - use soroban_sdk::testutils::Address as _; - use soroban_sdk::{token, Address, Env}; + use soroban_sdk::testutils::{Address as _, Events as _}; + use soroban_sdk::{token, Address, Env, IntoVal}; fn setup_token(env: &Env, admin: &Address) -> Address { let contract = env.register_stellar_asset_contract_v2(admin.clone()); @@ -894,6 +919,49 @@ mod test { assert_eq!(tc.balance(&contract_id), 0); } + #[test] + fn test_release_funds_emits_release_event() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let agent_judge = Address::generate(&env); + let client = Address::generate(&env); + let freelancer = Address::generate(&env); + + let token_addr = setup_token(&env, &admin); + mint(&env, &token_addr, &client); + + let contract_id = env.register_contract(None, EscrowContract); + let cc = EscrowContractClient::new(&env, &contract_id); + + cc.initialize(&admin, &agent_judge); + cc.create_job(&88u64, &client, &freelancer, &token_addr); + cc.add_milestone(&88u64, &1500i128); + cc.add_milestone(&88u64, &2500i128); + cc.deposit(&88u64, &4000i128); + + cc.release_funds(&88u64, &client, &1u32); + + let events = env.events().all(); + let release_event = events.get(events.len() - 1).unwrap(); + assert_eq!(release_event.0, contract_id); + assert_eq!(release_event.1, ("escrow", "ReleaseEvent").into_val(&env)); + assert_eq!( + release_event.2, + ReleaseEvent { + job_id: 88, + released_by: client, + released_to: freelancer, + milestone_index: 1, + amount: 2500, + total_released: 2500, + released_at: env.ledger().timestamp(), + } + .into_val(&env) + ); + } + #[test] fn test_raise_dispute_by_client_locks_funds() { let env = Env::default(); diff --git a/docs/contracts/escrow.md b/docs/contracts/escrow.md new file mode 100644 index 00000000..d6371c3a --- /dev/null +++ b/docs/contracts/escrow.md @@ -0,0 +1,37 @@ +# Escrow Smart Contract + +## Overview + +The `Escrow` contract manages funded milestone payments, releases, refunds, and disputes for Lance jobs. + +## `ReleaseEvent` + +### Purpose + +`ReleaseEvent` is emitted by `release_funds` after a client releases an explicit milestone index to the freelancer. It gives backend indexers and clients an auditable signal for state-changing escrow releases. + +### Topic + +- `("escrow", "ReleaseEvent")` + +### Payload + +- `job_id`: Unique job identifier. +- `released_by`: Client address that authorized the release. +- `released_to`: Freelancer address that received the payment. +- `milestone_index`: Zero-based milestone index released by the call. +- `amount`: Amount transferred for the released milestone. +- `total_released`: Cumulative amount released for the job after this release. +- `released_at`: Ledger timestamp when the event was emitted. + +### Validation + +`release_funds` preserves the existing escrow checks before emitting the event: + +- Caller authentication via `require_auth()`. +- Job must be funded or already in progress. +- Caller must be the job client. +- Milestone index must exist. +- Milestone must still be pending. + +The event is emitted only after the token transfer and persistent job update succeed. From 40bd6857011c53ee0b2df242d880fab4454badc7 Mon Sep 17 00:00:00 2001 From: EduVault Developer Date: Tue, 28 Apr 2026 14:50:49 +0100 Subject: [PATCH 2/3] fix(contract): resolve CI error E0369 by deriving PartialEq for events and fixing test comparison --- contracts/escrow/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index ff1f3240..7f8c986b 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -133,7 +133,7 @@ pub struct DisputeRaisedEvent { } #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub struct DepositEvent { pub job_id: u64, pub amount: i128, @@ -141,7 +141,7 @@ pub struct DepositEvent { } #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub struct ReleaseMilestoneEvent { pub job_id: u64, pub milestone_index: u32, @@ -150,7 +150,7 @@ pub struct ReleaseMilestoneEvent { } #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub struct ReleaseEvent { pub job_id: u64, pub released_by: Address, @@ -162,7 +162,7 @@ pub struct ReleaseEvent { } #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub struct OpenDisputeEvent { pub job_id: u64, pub initiator: Address, @@ -170,7 +170,7 @@ pub struct OpenDisputeEvent { } #[contracttype] -#[derive(Clone)] +#[derive(Clone, Debug, PartialEq)] pub struct JobRegistryConfiguredEvent { pub configured_by: Address, pub registry_contract: Address, @@ -1244,8 +1244,9 @@ mod test { let release_event = events.get(events.len() - 1).unwrap(); assert_eq!(release_event.0, contract_id); assert_eq!(release_event.1, ("escrow", "ReleaseEvent").into_val(&env)); + let actual_event: ReleaseEvent = release_event.2.into_val(&env); assert_eq!( - release_event.2, + actual_event, ReleaseEvent { job_id: 88, released_by: client, @@ -1255,7 +1256,6 @@ mod test { total_released: 2500, released_at: env.ledger().timestamp(), } - .into_val(&env) ); } From 49cb35f03bab09c351e6a5bf14e4123ffa042dba Mon Sep 17 00:00:00 2001 From: EduVault Developer Date: Tue, 28 Apr 2026 15:09:54 +0100 Subject: [PATCH 3/3] fix(contract): transition job status to Completed in release_funds when all funds are released --- contracts/escrow/src/lib.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 7f8c986b..b5bee963 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -615,7 +615,13 @@ impl EscrowContract { job.milestones.set(milestone_index, milestone.clone()); job.released_amount = job.released_amount.saturating_add(milestone.amount); - job.status = EscrowStatus::WorkInProgress; + + let next_status = if job.released_amount == job.total_amount { + EscrowStatus::Completed + } else { + EscrowStatus::WorkInProgress + }; + job.status = next_status; let token_client = token::Client::new(&env, &job.token); token_client.transfer(