From b60886546d710081be8374f0ac439ec85d767277 Mon Sep 17 00:00:00 2001 From: Fuad ALPHATIC Date: Mon, 27 Apr 2026 16:42:13 +0100 Subject: [PATCH] feat(escrow_contract): implement escrow lifecycle for issues 7-10 --- contracts/escrow_contract/lib.rs | 137 +++++-- contracts/escrow_contract/test.rs | 599 ++++++------------------------ contracts/shared_types/lib.rs | 8 +- 3 files changed, 227 insertions(+), 517 deletions(-) diff --git a/contracts/escrow_contract/lib.rs b/contracts/escrow_contract/lib.rs index e6c9170..acb1fdd 100644 --- a/contracts/escrow_contract/lib.rs +++ b/contracts/escrow_contract/lib.rs @@ -22,6 +22,19 @@ fn require_admin(env: &Env, caller: &Address) { } } +fn is_admin(env: &Env, caller: &Address) -> bool { + let stored_admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("Not initialized"); + *caller == stored_admin +} + +fn calculate_fee(amount: i128, platform_fee_bps: u32) -> i128 { + amount.saturating_mul(platform_fee_bps as i128) / 10_000 +} + fn save_escrow(env: &Env, delivery_id: u64, record: &EscrowRecord) { let key = DataKey::Escrow(delivery_id); env.storage().persistent().set(&key, record); @@ -64,6 +77,7 @@ pub enum EscrowError { InvalidState = 1, DeliveryNotFound = 2, InsufficientFunds = 3, + DuplicateDelivery = 4, } #[contracttype] @@ -192,6 +206,7 @@ impl EscrowContract { pub fn create_escrow( env: Env, sender: Address, + recipient: Address, driver: Address, delivery_id: u64, token: Address, @@ -203,7 +218,7 @@ impl EscrowContract { .persistent() .has(&DataKey::Escrow(delivery_id)) { - panic!("escrow already exists for this delivery_id"); + panic_with_error!(&env, EscrowError::DuplicateDelivery); } token::Client::new(&env, &token).transfer( &sender, @@ -215,22 +230,32 @@ impl EscrowContract { delivery_id, &EscrowRecord { sender: sender.clone(), + recipient: recipient.clone(), driver, token, amount, - status: EscrowStatus::Pending, + status: EscrowStatus::Locked, + created_at: env.ledger().timestamp(), + disputed_by: None, + disputed_at: None, }, ); - env.events() - .publish((events::escrow_funded(&env), delivery_id), (sender, amount)); + env.events().publish( + (events::escrow_funded(&env), delivery_id), + (sender, recipient, amount), + ); } pub fn release_escrow(env: Env, caller: Address, delivery_id: u64) { caller.require_auth(); - require_admin(&env, &caller); let mut record = load_escrow(&env, delivery_id); - if record.status != EscrowStatus::Pending { - panic!("escrow is not in pending state"); + let admin_authorized = is_admin(&env, &caller); + let recipient_authorized = caller == record.recipient; + if !admin_authorized && !recipient_authorized { + panic!("Unauthorized"); + } + if record.status != EscrowStatus::Locked { + panic_with_error!(&env, EscrowError::InvalidState); } // Balance verification guard: confirm contract holds sufficient funds before transfer let contract_balance = @@ -238,25 +263,53 @@ impl EscrowContract { if contract_balance < record.amount { panic_with_error!(&env, EscrowError::InsufficientFunds); } - token::Client::new(&env, &record.token).transfer( - &env.current_contract_address(), - &record.driver, - &record.amount, - ); + let platform_fee_bps: u32 = env + .storage() + .instance() + .get(&DataKey::PlatformFeeBps) + .unwrap_or(0); + let platform_fee = calculate_fee(record.amount, platform_fee_bps); + let driver_amount = record.amount.saturating_sub(platform_fee); + + if driver_amount > 0 { + token::Client::new(&env, &record.token).transfer( + &env.current_contract_address(), + &record.driver, + &driver_amount, + ); + } + + if platform_fee > 0 { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("Not initialized"); + token::Client::new(&env, &record.token).transfer( + &env.current_contract_address(), + &admin, + &platform_fee, + ); + } + record.status = EscrowStatus::Released; save_escrow(&env, delivery_id, &record); env.events().publish( (events::escrow_released(&env), delivery_id), - (record.driver, record.amount, 0i128), + (record.driver, driver_amount, platform_fee), ); } pub fn refund_escrow(env: Env, caller: Address, delivery_id: u64) { caller.require_auth(); - require_admin(&env, &caller); let mut record = load_escrow(&env, delivery_id); - if record.status != EscrowStatus::Pending { - panic!("escrow is not in pending state"); + let admin_authorized = is_admin(&env, &caller); + let sender_authorized = caller == record.sender; + if !admin_authorized && !sender_authorized { + panic!("Unauthorized"); + } + if record.status != EscrowStatus::Locked && record.status != EscrowStatus::Paused { + panic_with_error!(&env, EscrowError::InvalidState); } // Balance verification guard: confirm contract holds sufficient funds before transfer let contract_balance = @@ -280,15 +333,17 @@ impl EscrowContract { pub fn raise_dispute(env: Env, caller: Address, delivery_id: u64) { caller.require_auth(); let mut record = load_escrow(&env, delivery_id); - if caller != record.sender { - panic!("only the escrow sender can raise a dispute"); + if caller != record.sender && caller != record.recipient { + panic!("Unauthorized"); } - if record.status != EscrowStatus::Pending { - panic!("escrow is not in pending state"); + if record.status != EscrowStatus::Locked { + panic_with_error!(&env, EscrowError::InvalidState); } - record.status = EscrowStatus::Disputed; - save_escrow(&env, delivery_id, &record); let timestamp = env.ledger().timestamp(); + record.status = EscrowStatus::Paused; + record.disputed_by = Some(caller.clone()); + record.disputed_at = Some(timestamp); + save_escrow(&env, delivery_id, &record); env.events().publish( (events::delivery_disputed(&env), delivery_id), (caller, timestamp), @@ -299,15 +354,39 @@ impl EscrowContract { caller.require_auth(); require_admin(&env, &caller); let mut record = load_escrow(&env, delivery_id); - if record.status != EscrowStatus::Disputed { - panic!("escrow is not in disputed state"); + if record.status != EscrowStatus::Paused { + panic_with_error!(&env, EscrowError::InvalidState); } if release_to_driver { - token::Client::new(&env, &record.token).transfer( - &env.current_contract_address(), - &record.driver, - &record.amount, - ); + let platform_fee_bps: u32 = env + .storage() + .instance() + .get(&DataKey::PlatformFeeBps) + .unwrap_or(0); + let platform_fee = calculate_fee(record.amount, platform_fee_bps); + let driver_amount = record.amount.saturating_sub(platform_fee); + + if driver_amount > 0 { + token::Client::new(&env, &record.token).transfer( + &env.current_contract_address(), + &record.driver, + &driver_amount, + ); + } + + if platform_fee > 0 { + let admin: Address = env + .storage() + .instance() + .get(&DataKey::Admin) + .expect("Not initialized"); + token::Client::new(&env, &record.token).transfer( + &env.current_contract_address(), + &admin, + &platform_fee, + ); + } + record.status = EscrowStatus::Released; } else { token::Client::new(&env, &record.token).transfer( diff --git a/contracts/escrow_contract/test.rs b/contracts/escrow_contract/test.rs index f886685..32bd28c 100644 --- a/contracts/escrow_contract/test.rs +++ b/contracts/escrow_contract/test.rs @@ -1,8 +1,8 @@ use super::*; use soroban_sdk::{ - testutils::{Address as _, Events}, + testutils::Address as _, token::{Client as TokenClient, StellarAssetClient}, - Address, Env, IntoVal, Symbol, + Address, Env, }; fn setup_env() -> (Env, Address) { @@ -26,676 +26,303 @@ fn balance(env: &Env, token: &Address, of: &Address) -> i128 { } #[test] -fn test_init_and_get_status() { +fn test_init_and_platform_fee_default() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - client.init(&admin, &1000); - - let status = client.get_status(); - assert_eq!(status, DeliveryStatus::Created); + client.init(&admin, &0); assert_eq!(client.get_platform_fee(), 0); + assert_eq!(client.get_admin(), admin); } #[test] fn test_update_platform_fee_success() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - - client.init(&admin, &1000); - - client.update_platform_fee(&admin, &500); - - let events = env.events().all(); - let last_event = events.last().unwrap(); - - assert_eq!(last_event.0, contract_id); - - let topics = last_event.1.clone(); - assert_eq!(topics.len(), 1); - let topic_sym: Symbol = topics.get(0).unwrap().into_val(&env); - assert_eq!(topic_sym, Symbol::new(&env, "FeeUpdated")); - - let event_value: FeeUpdated = last_event.2.into_val(&env); - assert_eq!( - event_value, - FeeUpdated { - old_fee: 0, - new_fee: 500 - } - ); - - assert_eq!(client.get_platform_fee(), 500); -} - -#[test] -#[should_panic(expected = "Unauthorized")] -fn test_update_platform_fee_unauthorized() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let malicious_user = Address::generate(&env); - client.init(&admin, &1000); + client.init(&admin, &0); + client.update_platform_fee(&admin, &250); - client.update_platform_fee(&malicious_user, &500); + assert_eq!(client.get_platform_fee(), 250); } #[test] fn test_update_platform_fee_invalid_value() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - client.init(&admin, &1000); - + client.init(&admin, &0); let result = client.try_update_platform_fee(&admin, &1100); match result { Err(Ok(err)) => assert_eq!(err, EscrowError::InvalidState.into()), - _ => panic!("Expected EscrowError::InvalidState, got {:?}", result), + _ => panic!("Expected EscrowError::InvalidState"), } } #[test] -fn test_propose_and_accept_admin() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let new_admin = Address::generate(&env); - - client.init(&admin, &1000); - assert_eq!(client.get_admin(), admin); - - client.propose_admin(&admin, &new_admin); - client.accept_admin(&new_admin); - - assert_eq!(client.get_admin(), new_admin); -} - -#[test] -#[should_panic] -fn test_accept_admin_rejected_for_non_pending() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let proposed = Address::generate(&env); - let other = Address::generate(&env); - - client.init(&admin, &1000); - client.propose_admin(&admin, &proposed); - client.accept_admin(&other); -} - -#[test] -fn test_admin_cleared_after_transfer() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let new_admin = Address::generate(&env); - - client.init(&admin, &1000); - client.propose_admin(&admin, &new_admin); - client.accept_admin(&new_admin); - - assert_ne!(client.get_admin(), admin); - assert_eq!(client.get_admin(), new_admin); -} - -#[test] -fn test_admin_transfer_emits_event() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let new_admin = Address::generate(&env); - - client.init(&admin, &1000); - client.propose_admin(&admin, &new_admin); - client.accept_admin(&new_admin); - - assert!(!env.events().all().is_empty()); -} - -#[test] -fn test_init_persists_escrow_amount_with_ttl() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - let sender = Address::generate(&env); - - client.init(&sender, &5000); - - assert_eq!(client.get_amount(), 5000); -} - -#[test] -fn test_propose_admin_extends_instance_ttl() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let new_admin = Address::generate(&env); - - client.init(&admin, &1000); - client.propose_admin(&admin, &new_admin); - assert_eq!(client.get_admin(), admin); -} - -#[test] -fn test_accept_admin_extends_instance_ttl() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - let admin = Address::generate(&env); - let new_admin = Address::generate(&env); - - client.init(&admin, &1000); - client.propose_admin(&admin, &new_admin); - client.accept_admin(&new_admin); - assert_eq!(client.get_admin(), new_admin); -} - -// ── Lifecycle integration tests ─────────────────────────────────────────────── - -#[test] -fn test_happy_path_create_and_release() { +fn test_create_escrow_locks_funds_and_persists_record() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 1000); + mint(&env, &token, &sender, 1000); - client.create_escrow(&sender, &driver, &1u64, &token_addr, &1000); + client.create_escrow(&sender, &recipient, &driver, &1u64, &token, &1000); - assert_eq!(balance(&env, &token_addr, &sender), 0); - assert_eq!(balance(&env, &token_addr, &contract_id), 1000); + assert_eq!(balance(&env, &token, &sender), 0); + assert_eq!(balance(&env, &token, &contract_id), 1000); let record = client.get_escrow(&1u64); - assert_eq!(record.status, EscrowStatus::Pending); - - client.release_escrow(&admin, &1u64); + assert_eq!(record.sender, sender); + assert_eq!(record.recipient, recipient); + assert_eq!(record.driver, driver); + assert_eq!(record.amount, 1000); + assert_eq!(record.status, EscrowStatus::Locked); + assert_eq!(record.disputed_by, None); + assert_eq!(record.disputed_at, None); + assert_eq!(record.created_at, env.ledger().timestamp()); - assert_eq!(balance(&env, &token_addr, &driver), 1000); - assert_eq!(balance(&env, &token_addr, &contract_id), 0); - assert_eq!(client.get_escrow(&1u64).status, EscrowStatus::Released); } #[test] -fn test_refund_path_restores_sender_balance() { +fn test_create_escrow_duplicate_delivery_rejected() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 500); - - client.create_escrow(&sender, &driver, &2u64, &token_addr, &500); - - assert_eq!(balance(&env, &token_addr, &sender), 0); + mint(&env, &token, &sender, 2000); - client.refund_escrow(&admin, &2u64); + client.create_escrow(&sender, &recipient, &driver, &2u64, &token, &1000); - assert_eq!(balance(&env, &token_addr, &sender), 500); - assert_eq!(balance(&env, &token_addr, &contract_id), 0); - assert_eq!(client.get_escrow(&2u64).status, EscrowStatus::Refunded); + let result = client.try_create_escrow(&sender, &recipient, &driver, &2u64, &token, &500); + match result { + Err(Ok(err)) => assert_eq!(err, EscrowError::DuplicateDelivery.into()), + _ => panic!("Expected EscrowError::DuplicateDelivery"), + } } #[test] -fn test_dispute_resolved_to_driver() { +fn test_release_escrow_by_recipient_with_platform_fee_split() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 750); - - client.create_escrow(&sender, &driver, &3u64, &token_addr, &750); - client.raise_dispute(&sender, &3u64); - - assert_eq!(client.get_escrow(&3u64).status, EscrowStatus::Disputed); + client.update_platform_fee(&admin, &500); // 5% + mint(&env, &token, &sender, 1000); - client.resolve_dispute(&admin, &3u64, &true); + client.create_escrow(&sender, &recipient, &driver, &3u64, &token, &1000); + client.release_escrow(&recipient, &3u64); - assert_eq!(balance(&env, &token_addr, &driver), 750); - assert_eq!(balance(&env, &token_addr, &sender), 0); + assert_eq!(balance(&env, &token, &driver), 950); + assert_eq!(balance(&env, &token, &admin), 50); + assert_eq!(balance(&env, &token, &contract_id), 0); assert_eq!(client.get_escrow(&3u64).status, EscrowStatus::Released); } #[test] -fn test_dispute_resolved_to_sender() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); - - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 300); - - client.create_escrow(&sender, &driver, &4u64, &token_addr, &300); - client.raise_dispute(&sender, &4u64); - client.resolve_dispute(&admin, &4u64, &false); - - assert_eq!(balance(&env, &token_addr, &sender), 300); - assert_eq!(balance(&env, &token_addr, &driver), 0); - assert_eq!(client.get_escrow(&4u64).status, EscrowStatus::Refunded); -} - -#[test] -#[should_panic] -fn test_release_by_non_admin_rejected() { +fn test_release_escrow_unauthorized_rejected() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); let attacker = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 200); - client.create_escrow(&sender, &driver, &5u64, &token_addr, &200); + mint(&env, &token, &sender, 500); + client.create_escrow(&sender, &recipient, &driver, &4u64, &token, &500); - client.release_escrow(&attacker, &5u64); + let result = client.try_release_escrow(&attacker, &4u64); + assert!(result.is_err()); } #[test] -#[should_panic] -fn test_refund_by_non_admin_rejected() { +fn test_refund_escrow_by_sender_full_amount_no_fee() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); - let attacker = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 200); - client.create_escrow(&sender, &driver, &6u64, &token_addr, &200); - - client.refund_escrow(&attacker, &6u64); -} - -#[test] -#[should_panic] -fn test_raise_dispute_by_non_sender_rejected() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let attacker = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + client.update_platform_fee(&admin, &500); + mint(&env, &token, &sender, 600); - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 200); - client.create_escrow(&sender, &driver, &7u64, &token_addr, &200); + client.create_escrow(&sender, &recipient, &driver, &5u64, &token, &600); + client.refund_escrow(&sender, &5u64); - client.raise_dispute(&attacker, &7u64); + assert_eq!(balance(&env, &token, &sender), 600); + assert_eq!(balance(&env, &token, &admin), 0); + assert_eq!(balance(&env, &token, &contract_id), 0); + assert_eq!(client.get_escrow(&5u64).status, EscrowStatus::Refunded); } #[test] -#[should_panic] -fn test_resolve_dispute_by_non_admin_rejected() { +fn test_raise_dispute_pauses_escrow_and_records_metadata() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); - let attacker = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 200); - client.create_escrow(&sender, &driver, &8u64, &token_addr, &200); - client.raise_dispute(&sender, &8u64); - - client.resolve_dispute(&attacker, &8u64, &true); -} - -#[test] -#[should_panic] -fn test_duplicate_delivery_id_rejected() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + mint(&env, &token, &sender, 700); + client.create_escrow(&sender, &recipient, &driver, &6u64, &token, &700); - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 2000); - client.create_escrow(&sender, &driver, &9u64, &token_addr, &1000); + client.raise_dispute(&recipient, &6u64); - client.create_escrow(&sender, &driver, &9u64, &token_addr, &1000); + let record = client.get_escrow(&6u64); + assert_eq!(record.status, EscrowStatus::Paused); + assert_eq!(record.disputed_by, Some(recipient)); + assert_eq!(record.disputed_at, Some(env.ledger().timestamp())); } #[test] -#[should_panic] -fn test_release_on_already_released_rejected() { +fn test_refund_from_paused_state_by_admin_allowed() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 400); - client.create_escrow(&sender, &driver, &10u64, &token_addr, &400); - client.release_escrow(&admin, &10u64); - - client.release_escrow(&admin, &10u64); -} - -#[test] -#[should_panic] -fn test_refund_on_released_escrow_rejected() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); + mint(&env, &token, &sender, 300); - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + client.create_escrow(&sender, &recipient, &driver, &7u64, &token, &300); + client.raise_dispute(&sender, &7u64); + client.refund_escrow(&admin, &7u64); - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 400); - client.create_escrow(&sender, &driver, &11u64, &token_addr, &400); - client.release_escrow(&admin, &11u64); - - client.refund_escrow(&admin, &11u64); + assert_eq!(balance(&env, &token, &sender), 300); + assert_eq!(client.get_escrow(&7u64).status, EscrowStatus::Refunded); } -// ── Balance verification guard tests (Issue #17) ───────────────────────────── - #[test] -fn test_release_escrow_passes_when_balance_sufficient() { +fn test_release_from_paused_state_rejected_with_invalid_state() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 1000); - client.create_escrow(&sender, &driver, &50u64, &token_addr, &1000); + mint(&env, &token, &sender, 300); - // Contract holds exactly 1000, escrow amount is 1000 — guard should pass - client.release_escrow(&admin, &50u64); + client.create_escrow(&sender, &recipient, &driver, &8u64, &token, &300); + client.raise_dispute(&recipient, &8u64); - assert_eq!(balance(&env, &token_addr, &driver), 1000); - assert_eq!(client.get_escrow(&50u64).status, EscrowStatus::Released); + let result = client.try_release_escrow(&admin, &8u64); + match result { + Err(Ok(err)) => assert_eq!(err, EscrowError::InvalidState.into()), + _ => panic!("Expected EscrowError::InvalidState"), + } } #[test] -fn test_release_escrow_insufficient_funds_rejected() { +fn test_refund_on_released_escrow_rejected_with_invalid_state() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 1000); - client.create_escrow(&sender, &driver, &51u64, &token_addr, &1000); + mint(&env, &token, &sender, 300); - // Artificially inflate the stored escrow amount so it exceeds the actual contract balance - env.as_contract(&contract_id, || { - let mut record: EscrowRecord = env - .storage() - .persistent() - .get(&DataKey::Escrow(51u64)) - .unwrap(); - record.amount = 2000; // contract only holds 1000 tokens - env.storage() - .persistent() - .set(&DataKey::Escrow(51u64), &record); - }); + client.create_escrow(&sender, &recipient, &driver, &9u64, &token, &300); + client.release_escrow(&admin, &9u64); - let result = client.try_release_escrow(&admin, &51u64); + let result = client.try_refund_escrow(&admin, &9u64); match result { - Err(Ok(err)) => assert_eq!(err, EscrowError::InsufficientFunds.into()), - _ => panic!("Expected EscrowError::InsufficientFunds, got {:?}", result), + Err(Ok(err)) => assert_eq!(err, EscrowError::InvalidState.into()), + _ => panic!("Expected EscrowError::InvalidState"), } } #[test] -fn test_refund_escrow_insufficient_funds_rejected() { +fn test_insufficient_funds_guard_on_release() { let (env, contract_id) = setup_env(); let client = EscrowContractClient::new(&env, &contract_id); let admin = Address::generate(&env); let sender = Address::generate(&env); + let recipient = Address::generate(&env); let driver = Address::generate(&env); let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); + let token = setup_token(&env, &token_admin); client.init(&admin, &0); - mint(&env, &token_addr, &sender, 1000); - client.create_escrow(&sender, &driver, &52u64, &token_addr, &1000); + mint(&env, &token, &sender, 200); + client.create_escrow(&sender, &recipient, &driver, &10u64, &token, &200); - // Artificially inflate the stored escrow amount to simulate underfunded contract env.as_contract(&contract_id, || { let mut record: EscrowRecord = env .storage() .persistent() - .get(&DataKey::Escrow(52u64)) + .get(&DataKey::Escrow(10u64)) .unwrap(); - record.amount = 2000; // contract only holds 1000 tokens + record.amount = 500; env.storage() .persistent() - .set(&DataKey::Escrow(52u64), &record); + .set(&DataKey::Escrow(10u64), &record); }); - let result = client.try_refund_escrow(&admin, &52u64); + let result = client.try_release_escrow(&admin, &10u64); match result { Err(Ok(err)) => assert_eq!(err, EscrowError::InsufficientFunds.into()), - _ => panic!("Expected EscrowError::InsufficientFunds, got {:?}", result), + _ => panic!("Expected EscrowError::InsufficientFunds"), } } -// ── Event emission tests ────────────────────────────────────────────────────── - -#[test] -fn test_create_escrow_emits_escrow_funded_event() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); - - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 1000); - - client.create_escrow(&sender, &driver, &100u64, &token_addr, &1000); - - let events = env.events().all(); - let event = events.last().unwrap(); - - assert_eq!(event.1.len(), 2); - assert!(!events.is_empty()); -} - -#[test] -fn test_release_escrow_emits_escrow_released_event() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); - - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 1000); - client.create_escrow(&sender, &driver, &101u64, &token_addr, &1000); - - client.release_escrow(&admin, &101u64); - - let events = env.events().all(); - let release_event = events.last().unwrap(); - assert_eq!(release_event.1.len(), 2); -} - -#[test] -fn test_refund_escrow_emits_escrow_refunded_event() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); - - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 500); - client.create_escrow(&sender, &driver, &102u64, &token_addr, &500); - - client.refund_escrow(&admin, &102u64); - - let events = env.events().all(); - let refund_event = events.last().unwrap(); - assert_eq!(refund_event.1.len(), 2); -} - -#[test] -fn test_raise_dispute_emits_delivery_disputed_event() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); - - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 750); - client.create_escrow(&sender, &driver, &103u64, &token_addr, &750); - - client.raise_dispute(&sender, &103u64); - - let events = env.events().all(); - let dispute_event = events.last().unwrap(); - assert_eq!(dispute_event.1.len(), 2); -} - -#[test] -fn test_resolve_dispute_to_driver_emits_dispute_resolved_event() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); - - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 750); - client.create_escrow(&sender, &driver, &104u64, &token_addr, &750); - client.raise_dispute(&sender, &104u64); - - client.resolve_dispute(&admin, &104u64, &true); - - let events = env.events().all(); - let resolve_event = events.last().unwrap(); - assert_eq!(resolve_event.1.len(), 2); -} - -#[test] -fn test_resolve_dispute_to_sender_emits_dispute_resolved_event() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); - - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 300); - client.create_escrow(&sender, &driver, &105u64, &token_addr, &300); - client.raise_dispute(&sender, &105u64); - - client.resolve_dispute(&admin, &105u64, &false); - - let events = env.events().all(); - let resolve_event = events.last().unwrap(); - assert_eq!(resolve_event.1.len(), 2); -} - -#[test] -fn test_lifecycle_events_emitted() { - let (env, contract_id) = setup_env(); - let client = EscrowContractClient::new(&env, &contract_id); - - let admin = Address::generate(&env); - let sender = Address::generate(&env); - let driver = Address::generate(&env); - let token_admin = Address::generate(&env); - let token_addr = setup_token(&env, &token_admin); - - client.init(&admin, &0); - mint(&env, &token_addr, &sender, 600); - - client.create_escrow(&sender, &driver, &12u64, &token_addr, &600); - client.raise_dispute(&sender, &12u64); - client.resolve_dispute(&admin, &12u64, &true); - - assert!(!env.events().all().is_empty()); -} - #[test] fn test_get_escrow_not_found() { let (env, contract_id) = setup_env(); @@ -704,6 +331,6 @@ fn test_get_escrow_not_found() { let result = client.try_get_escrow(&999u64); match result { Err(Ok(err)) => assert_eq!(err, EscrowError::DeliveryNotFound.into()), - _ => panic!("Expected DeliveryNotFound error"), + _ => panic!("Expected DeliveryNotFound"), } } diff --git a/contracts/shared_types/lib.rs b/contracts/shared_types/lib.rs index eb9c0e4..712a844 100644 --- a/contracts/shared_types/lib.rs +++ b/contracts/shared_types/lib.rs @@ -47,18 +47,22 @@ pub struct DeliveryDetails { #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub enum EscrowStatus { - Pending, + Locked, + Paused, Released, Refunded, - Disputed, } #[contracttype] #[derive(Clone, Debug, Eq, PartialEq)] pub struct EscrowRecord { pub sender: Address, + pub recipient: Address, pub driver: Address, pub token: Address, pub amount: i128, pub status: EscrowStatus, + pub created_at: u64, + pub disputed_by: Option
, + pub disputed_at: Option, }