Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 79 additions & 2 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,9 +503,28 @@ impl Escrow {
true
}

/// Release a milestone to the freelancer. Blocked when paused.
pub fn release_milestone(env: Env, contract_id: u32, milestone_index: u32) -> bool {
/// Release a funded milestone payment to the freelancer.
///
/// # Parameters
/// - `contract_id`: The ID of the escrow contract.
/// - `caller`: The address authorizing the release. Must be the recorded client.
/// - `milestone_index`: Zero-based index of the milestone to release.
///
/// # Errors / Panics
/// - `ContractPaused` — contract is paused or in emergency.
/// - `ContractNotFound` — no contract exists for `contract_id`.
/// - `UnauthorizedRole` — `caller` is not the recorded client.
/// - `InvalidMilestone` — `milestone_index` is out of range.
/// - `AlreadyReleased` — milestone was already released.
/// - `InsufficientFunds` — available balance is less than the milestone amount.
pub fn release_milestone(
env: Env,
contract_id: u32,
caller: Address,
milestone_index: u32,
) -> bool {
Self::require_not_paused(&env);
caller.require_auth();

let key = DataKey::Contract(contract_id);
let mut contract = env
Expand All @@ -514,6 +533,10 @@ impl Escrow {
.get::<_, EscrowContractData>(&key)
.unwrap_or_else(|| env.panic_with_error(EscrowError::ContractNotFound));

if caller != contract.client {
env.panic_with_error(EscrowError::UnauthorizedRole);
}

if milestone_index >= contract.milestones.len() {
env.panic_with_error(EscrowError::InvalidMilestone);
}
Expand Down Expand Up @@ -698,6 +721,60 @@ impl Escrow {

// ─── Read-only queries (not blocked by pause) ─────────────────────────────

/// Returns a versioned, denormalized snapshot of the escrow contract for
/// off-chain indexers. Intentionally unauthenticated and never blocked by
/// pause or emergency guards so that data availability is always maintained.
///
/// Panics with [`EscrowError::ContractNotFound`] if `contract_id` does not exist.
pub fn get_contract_summary(env: Env, contract_id: u32) -> ContractSummary {
let contract = env
.storage()
.persistent()
.get::<_, EscrowContractData>(&DataKey::Contract(contract_id))
.unwrap_or_else(|| env.panic_with_error(EscrowError::ContractNotFound));

let mut total_amount: i128 = 0;
let mut released_milestone_count: u32 = 0;
let mut milestones = Vec::new(&env);

for i in 0..contract.milestones.len() {
let amount = contract.milestones.get(i).unwrap();
total_amount += amount;
let released = env
.storage()
.persistent()
.get::<_, bool>(&DataKey::MilestoneReleased(contract_id, i))
.unwrap_or(false);
if released {
released_milestone_count += 1;
}
milestones.push_back(MilestoneSummary {
index: i,
amount,
released,
refunded: false,
});
}

let refundable_balance =
contract.total_deposited - contract.released_amount - contract.refunded_amount;

ContractSummary {
schema_version: CONTRACT_SUMMARY_SCHEMA_VERSION,
client: contract.client,
freelancer: contract.freelancer,
arbiter: contract.arbiter,
status: contract.status,
reputation_issued: contract.reputation_issued,
total_amount,
funded_amount: contract.total_deposited,
released_amount: contract.released_amount,
refundable_balance,
released_milestone_count,
milestones,
}
}

pub fn get_contract(env: Env, contract_id: u32) -> EscrowContractData {
env.storage()
.persistent()
Expand Down
8 changes: 4 additions & 4 deletions contracts/escrow/src/test/emergency_controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ fn setup_funded_contract(env: &Env, client: &EscrowClient) -> (Address, Address,

fn setup_completed_contract(env: &Env, client: &EscrowClient) -> (Address, Address, u32) {
let (client_addr, freelancer_addr, id) = setup_funded_contract(env, client);
client.release_milestone(&id, &0);
client.release_milestone(&id, &1);
client.release_milestone(&id, &client_addr, &0);
client.release_milestone(&id, &client_addr, &1);
(client_addr, freelancer_addr, id)
}

Expand Down Expand Up @@ -106,11 +106,11 @@ fn emergency_blocks_deposit_funds() {
fn emergency_blocks_release_milestone() {
let (env, contract_id, _admin) = setup_initialized();
let client = EscrowClient::new(&env, &contract_id);
let (_, _, id) = setup_funded_contract(&env, &client);
let (client_addr, _, id) = setup_funded_contract(&env, &client);
client.activate_emergency_pause();

super::assert_contract_error(
client.try_release_milestone(&id, &0),
client.try_release_milestone(&id, &client_addr, &0),
EscrowError::ContractPaused,
);
}
Expand Down
6 changes: 3 additions & 3 deletions contracts/escrow/src/test/lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ fn successful_contract_lifecycle() {
);

// Release milestones
assert!(client.release_milestone(&contract_id, &0));
assert!(client.release_milestone(&contract_id, &1));
assert!(client.release_milestone(&contract_id, &2));
assert!(client.release_milestone(&contract_id, &client_addr, &0));
assert!(client.release_milestone(&contract_id, &client_addr, &1));
assert!(client.release_milestone(&contract_id, &client_addr, &2));

let finalized = client.get_contract(&contract_id);
assert_eq!(finalized.status, ContractStatus::Completed);
Expand Down
7 changes: 4 additions & 3 deletions contracts/escrow/src/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{Escrow, EscrowClient, EscrowError};

mod emergency_controls;
mod pause_controls;
mod release_authorization;

// ─── Shared constants ─────────────────────────────────────────────────────────

Expand Down Expand Up @@ -56,9 +57,9 @@ pub fn create_contract(env: &Env, client: &EscrowClient) -> (Address, Address, u
pub fn complete_contract(env: &Env, client: &EscrowClient) -> (Address, Address, u32) {
let (client_addr, freelancer_addr, id) = create_contract(env, client);
assert!(client.deposit_funds(&id, &total_milestone_amount()));
assert!(client.release_milestone(&id, &0));
assert!(client.release_milestone(&id, &1));
assert!(client.release_milestone(&id, &2));
assert!(client.release_milestone(&id, &client_addr, &0));
assert!(client.release_milestone(&id, &client_addr, &1));
assert!(client.release_milestone(&id, &client_addr, &2));
(client_addr, freelancer_addr, id)
}

Expand Down
8 changes: 4 additions & 4 deletions contracts/escrow/src/test/pause_controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ fn setup_funded_contract(env: &Env, client: &EscrowClient) -> (Address, Address,
/// Create a completed contract ready for reputation issuance.
fn setup_completed_contract(env: &Env, client: &EscrowClient) -> (Address, Address, u32) {
let (client_addr, freelancer_addr, id) = setup_funded_contract(env, client);
client.release_milestone(&id, &0);
client.release_milestone(&id, &1);
client.release_milestone(&id, &client_addr, &0);
client.release_milestone(&id, &client_addr, &1);
(client_addr, freelancer_addr, id)
}

Expand Down Expand Up @@ -111,11 +111,11 @@ fn pause_blocks_deposit_funds() {
fn pause_blocks_release_milestone() {
let (env, contract_id, _admin) = setup_initialized();
let client = EscrowClient::new(&env, &contract_id);
let (_, _, id) = setup_funded_contract(&env, &client);
let (client_addr, _, id) = setup_funded_contract(&env, &client);
client.pause();

super::assert_contract_error(
client.try_release_milestone(&id, &0),
client.try_release_milestone(&id, &client_addr, &0),
EscrowError::ContractPaused,
);
}
Expand Down
2 changes: 1 addition & 1 deletion contracts/escrow/src/test/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ fn contract_state_round_trips_across_lifecycle_mutations() {
&contract_id,
&(total_milestone_amount() - 10_000_000_000_i128),
));
assert!(client.release_milestone(&contract_id, &0));
assert!(client.release_milestone(&contract_id, &client_addr, &0));

let after_release = client.get_contract(&contract_id);
assert_eq!(after_release.released_amount, super::MILESTONE_ONE);
Expand Down
Loading
Loading