diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 83e3637..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/Cargo.lock b/Cargo.lock index 7c25a92..9b7587b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,14 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "Contract-Events-for-Indexer" +version = "0.0.0" +dependencies = [ + "soroban-sdk", + "soroban-token-sdk", +] + [[package]] name = "ahash" version = "0.8.12" diff --git a/contracts/Contract-Events-for-Indexer/Cargo.toml b/contracts/Contract-Events-for-Indexer/Cargo.toml new file mode 100644 index 0000000..c93de69 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "Contract-Events-for-Indexer" +version = "0.0.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib"] + +[dependencies] +soroban-sdk = { workspace = true } +soroban-token-sdk = { workspace = true } + +[dev-dependencies] +soroban-sdk = { workspace = true, features = ["testutils"] } +soroban-token-sdk = { workspace = true } \ No newline at end of file diff --git a/contracts/Contract-Events-for-Indexer/README.md b/contracts/Contract-Events-for-Indexer/README.md new file mode 100644 index 0000000..d712344 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/README.md @@ -0,0 +1,26 @@ +# Spike 1 Findings: Adding Soroban Events + +## Overview + +This spike focused on implementing comprehensive Soroban events for the Trustless Work Smart Escrow contract to enable better indexing and monitoring capabilities. The implementation adds structured events for all major contract operations including escrow creation, funding, milestone management, dispute handling, and fund releases. + +## Findings + +- For each function that published an event, I needed to call a separate helper function to retrieve the current escrow state. +- Testing event publishing requires generating a contract ID. + +## Assumptions + +- **Soroban SDK Compatibility**: Events use standard Soroban SDK event publishing methods +- **Data Type Compatibility**: All event data types (Address, i128, String) are compatible with Soroban events +- **Event Ordering**: Events are emitted after successful state changes to ensure consistency +- **Gas Efficiency**: Event data is optimized to minimize gas costs while providing necessary context +- **Indexer Compatibility**: Event structure follows Soroban indexing best practices + +## Recommendations + +- **Add Error Event Handling**: Implement events for failed operations to help with debugging and monitoring +- **Event Validation**: Add tests specifically for event data structure and content validation +- **Event Documentation**: Create comprehensive documentation for indexers on event structure and data types +- **Event Versioning**: Consider adding event versioning for future contract upgrades +- **Event Filtering**: Implement event filtering capabilities for different use cases \ No newline at end of file diff --git a/contracts/Contract-Events-for-Indexer/src/contract.rs b/contracts/Contract-Events-for-Indexer/src/contract.rs new file mode 100644 index 0000000..26e538e --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/contract.rs @@ -0,0 +1,195 @@ +use soroban_sdk::{ + contract, contractimpl, symbol_short, Address, BytesN, Env, String, Symbol, Val, Vec, +}; + +use crate::core::{DisputeManager, EscrowManager, MilestoneManager}; +use crate::error::ContractError; +use crate::storage::types::{AddressBalance, DataKey, Escrow}; + +#[contract] +pub struct EscrowContract; + +#[contractimpl] +impl EscrowContract { + pub fn __constructor(env: Env, admin: Address) { + env.storage().instance().set(&DataKey::Admin, &admin); + } + + pub fn deploy( + env: Env, + deployer: Address, + wasm_hash: BytesN<32>, + salt: BytesN<32>, + init_fn: Symbol, + init_args: Vec, + constructor_args: Vec, + ) -> (Address, Val) { + if deployer != env.current_contract_address() { + deployer.require_auth(); + } + + let deployed_address = env + .deployer() + .with_address(deployer, salt) + .deploy_v2(wasm_hash, constructor_args); + + let res: Val = env.invoke_contract(&deployed_address, &init_fn, init_args); + (deployed_address, res) + } + + //////////////////////// + // Escrow ///// + //////////////////////// + + pub fn initialize_escrow(e: Env, escrow_properties: Escrow) -> Result { + let initialized_escrow = + EscrowManager::initialize_escrow(e.clone(), escrow_properties)?; + let escrow = initialized_escrow.clone(); + e.events().publish( + (symbol_short!("escrow"), symbol_short!("created")), + (escrow.engagement_id, escrow.amount, escrow.roles.receiver) + ); + Ok(initialized_escrow) + } + + pub fn fund_escrow( + e: Env, + signer: Address, + amount_to_deposit: i128, + ) -> Result<(), ContractError> { + EscrowManager::fund_escrow(e.clone(), signer.clone(), amount_to_deposit)?; + let escrow = EscrowManager::get_escrow(e.clone())?; + e.events().publish( + (symbol_short!("escrow"), symbol_short!("funded")), + (e.current_contract_address(), escrow.engagement_id, signer, amount_to_deposit) + ); + Ok(()) + } + + pub fn release_funds( + e: Env, + release_signer: Address, + trustless_work_address: Address, + ) -> Result<(), ContractError> { + EscrowManager::release_funds( + e.clone(), + release_signer.clone(), + trustless_work_address.clone(), + )?; + let escrow = EscrowManager::get_escrow(e.clone())?; + e.events().publish( + (symbol_short!("escrow"), symbol_short!("released")), + (escrow.engagement_id, trustless_work_address) + ); + Ok(()) + } + + pub fn update_escrow( + e: Env, + plataform_address: Address, + escrow_properties: Escrow, + ) -> Result { + let updated_escrow = EscrowManager::change_escrow_properties( + e.clone(), + plataform_address.clone(), + escrow_properties.clone(), + )?; + + Ok(updated_escrow) + } + + pub fn get_escrow(e: Env) -> Result { + EscrowManager::get_escrow(e) + } + + pub fn get_escrow_by_contract_id( + e: Env, + contract_id: Address, + ) -> Result { + EscrowManager::get_escrow_by_contract_id(e, &contract_id) + } + + pub fn get_multiple_escrow_balances( + e: Env, + signer: Address, + addresses: Vec
, + ) -> Result, ContractError> { + EscrowManager::get_multiple_escrow_balances(e, signer, addresses) + } + + //////////////////////// + // Milestones ///// + //////////////////////// + + pub fn change_milestone_status( + e: Env, + milestone_index: i128, + new_status: String, + new_evidence: Option, + service_provider: Address, + ) -> Result<(), ContractError> { + MilestoneManager::change_milestone_status( + e.clone(), + milestone_index, + new_status.clone(), + new_evidence, + service_provider, + )?; + let escrow = EscrowManager::get_escrow(e.clone())?; + e.events().publish( + (symbol_short!("escrow"), symbol_short!("marked")), + (milestone_index, new_status, escrow.engagement_id) + ); + Ok(()) + } + + pub fn approve_milestone( + e: Env, + milestone_index: i128, + new_flag: bool, + approver: Address, + ) -> Result<(), ContractError> { + MilestoneManager::change_milestone_approved_flag(e.clone(), milestone_index, new_flag, approver)?; + let escrow = EscrowManager::get_escrow(e.clone())?; + e.events().publish( + (symbol_short!("escrow"), symbol_short!("approved")), + (milestone_index, new_flag, escrow.engagement_id)); + Ok(()) + } + + //////////////////////// + // Disputes ///// + //////////////////////// + + pub fn resolve_dispute( + e: Env, + dispute_resolver: Address, + approver_funds: i128, + receiver_funds: i128, + trustless_work_address: Address, + ) -> Result<(), ContractError> { + DisputeManager::resolve_dispute( + e.clone(), + dispute_resolver, + approver_funds, + receiver_funds, + trustless_work_address, + )?; + let escrow = EscrowManager::get_escrow(e.clone())?; + e.events().publish( + (symbol_short!("escrow"), symbol_short!("resolved")), + (approver_funds, receiver_funds, escrow.engagement_id) + ); + Ok(()) + } + + pub fn dispute_escrow(e: Env, signer: Address) -> Result<(), ContractError> { + DisputeManager::dispute_escrow(e.clone(), signer.clone())?; + let escrow = EscrowManager::get_escrow(e.clone())?; + e.events().publish( + (symbol_short!("escrow"), symbol_short!("raised")), + (signer, escrow.engagement_id) + ); + Ok(()) + } +} diff --git a/contracts/Contract-Events-for-Indexer/src/core/dispute.rs b/contracts/Contract-Events-for-Indexer/src/core/dispute.rs new file mode 100644 index 0000000..9ac17f5 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/core/dispute.rs @@ -0,0 +1,90 @@ +use soroban_sdk::{Address, Env}; +use soroban_sdk::token::Client as TokenClient; + +use crate::core::escrow::EscrowManager; +use crate::error::ContractError; +use crate::events::escrows_by_contract_id; +use crate::modules::{ + fee::{FeeCalculator, FeeCalculatorTrait}, + math::{BasicArithmetic, BasicMath}, +}; +use crate::storage::types::DataKey; + +use super::validators::dispute::{ + validate_dispute_flag_change_conditions, validate_dispute_resolution_conditions, +}; + +pub struct DisputeManager; + +impl DisputeManager { + pub fn resolve_dispute( + e: Env, + dispute_resolver: Address, + approver_funds: i128, + receiver_funds: i128, + trustless_work_address: Address, + ) -> Result<(), ContractError> { + dispute_resolver.require_auth(); + let mut escrow = EscrowManager::get_escrow(e.clone())?; + let contract_address = e.current_contract_address(); + + let token_client = TokenClient::new(&e, &escrow.trustline.address); + + let total_funds = BasicMath::safe_add(approver_funds, receiver_funds)?; + + if token_client.balance(&contract_address) < total_funds { + return Err(ContractError::InsufficientFundsForResolution); + } + + let fee_result = FeeCalculator::calculate_dispute_fees( + approver_funds, + receiver_funds, + escrow.platform_fee as i128, + total_funds, + )?; + + let current_balance = token_client.balance(&contract_address); + validate_dispute_resolution_conditions( + &escrow, + &dispute_resolver, + approver_funds, + receiver_funds, + total_funds, + &fee_result, + current_balance, + )?; + + token_client.transfer(&contract_address, &trustless_work_address, &fee_result.trustless_work_fee); + token_client.transfer(&contract_address, &escrow.roles.platform_address, &fee_result.platform_fee); + + if fee_result.net_approver_funds > 0 { + token_client.transfer(&contract_address, &escrow.roles.approver, &fee_result.net_approver_funds); + } + + if fee_result.net_receiver_funds > 0 { + let receiver = EscrowManager::get_receiver(&escrow); + token_client.transfer(&contract_address, &receiver, &fee_result.net_receiver_funds); + } + + escrow.flags.resolved = true; + escrow.flags.disputed = false; + e.storage().instance().set(&DataKey::Escrow, &escrow); + + escrows_by_contract_id(&e, escrow.engagement_id.clone(), escrow); + + Ok(()) + } + + pub fn dispute_escrow(e: Env, signer: Address) -> Result<(), ContractError> { + signer.require_auth(); + let mut escrow = EscrowManager::get_escrow(e.clone())?; + validate_dispute_flag_change_conditions(&escrow, &signer)?; + + escrow.flags.disputed = true; + e.storage().instance().set(&DataKey::Escrow, &escrow); + + escrows_by_contract_id(&e, escrow.engagement_id.clone(), escrow); + + Ok(()) + } +} diff --git a/contracts/Contract-Events-for-Indexer/src/core/escrow.rs b/contracts/Contract-Events-for-Indexer/src/core/escrow.rs new file mode 100644 index 0000000..59c97c9 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/core/escrow.rs @@ -0,0 +1,135 @@ +use soroban_sdk::{Address, Env, Symbol, Vec}; +use soroban_sdk::token::Client as TokenClient; + +use crate::core::validators::escrow::{ + validate_escrow_property_change_conditions, + validate_initialize_escrow_conditions, validate_release_conditions, +}; +use crate::error::ContractError; +use crate::modules::fee::{FeeCalculator, FeeCalculatorTrait}; +use crate::storage::types::{AddressBalance, DataKey, Escrow}; + +pub struct EscrowManager; + +impl EscrowManager { + #[inline] + pub fn get_receiver(escrow: &Escrow) -> Address { + if escrow.roles.receiver == escrow.roles.service_provider { + escrow.roles.service_provider.clone() + } else { + escrow.roles.receiver.clone() + } + } + + pub fn initialize_escrow(e: Env, escrow_properties: Escrow) -> Result { + validate_initialize_escrow_conditions(e.clone(), escrow_properties.clone())?; + e.storage() + .instance() + .set(&DataKey::Escrow, &escrow_properties); + Ok(escrow_properties) + } + + pub fn fund_escrow( + e: Env, + signer: Address, + amount_to_deposit: i128, + ) -> Result<(), ContractError> { + signer.require_auth(); + let escrow = Self::get_escrow(e.clone())?; + let token_client = TokenClient::new(&e, &escrow.trustline.address); + token_client.transfer(&signer, &e.current_contract_address(), &amount_to_deposit); + Ok(()) + } + + pub fn release_funds( + e: Env, + release_signer: Address, + trustless_work_address: Address, + ) -> Result<(), ContractError> { + release_signer.require_auth(); + let mut escrow = Self::get_escrow(e.clone())?; + validate_release_conditions(&escrow, &release_signer)?; + + escrow.flags.released = true; + e.storage().instance().set(&DataKey::Escrow, &escrow); + + let contract_address = e.current_contract_address(); + let token_client = TokenClient::new(&e, &escrow.trustline.address); + + if token_client.balance(&contract_address) < escrow.amount { + return Err(ContractError::EscrowBalanceNotEnoughToSendEarnings); + } + + let fee_result = + FeeCalculator::calculate_standard_fees(escrow.amount as i128, escrow.platform_fee as i128)?; + + token_client.transfer(&contract_address, &trustless_work_address, &fee_result.trustless_work_fee); + token_client.transfer(&contract_address, &escrow.roles.platform_address, &fee_result.platform_fee); + + let receiver = Self::get_receiver(&escrow); + token_client.transfer(&contract_address, &receiver, &fee_result.receiver_amount); + + Ok(()) + } + + pub fn change_escrow_properties( + e: Env, + platform_address: Address, + escrow_properties: Escrow, + ) -> Result { + platform_address.require_auth(); + let existing_escrow = Self::get_escrow(e.clone())?; + let token_client = TokenClient::new(&e, &existing_escrow.trustline.address); + let contract_balance = token_client.balance(&e.current_contract_address()); + + validate_escrow_property_change_conditions( + &existing_escrow, + &platform_address, + contract_balance, + )?; + + e.storage() + .instance() + .set(&DataKey::Escrow, &escrow_properties); + Ok(escrow_properties) + } + + pub fn get_multiple_escrow_balances( + e: Env, + signer: Address, + addresses: Vec
, + ) -> Result, ContractError> { + signer.require_auth(); + const MAX_ESCROWS: u32 = 20; + if addresses.len() > MAX_ESCROWS { + return Err(ContractError::TooManyEscrowsRequested); + } + + let mut balances: Vec = Vec::new(&e); + for address in addresses.iter() { + let escrow = Self::get_escrow_by_contract_id(e.clone(), &address)?; + let token_client = TokenClient::new(&e, &escrow.trustline.address); + let balance = token_client.balance(&address); + balances.push_back(AddressBalance { + address: address.clone(), + balance, + trustline_decimals: escrow.trustline.decimals, + }); + } + Ok(balances) + } + + pub fn get_escrow_by_contract_id( + e: Env, + contract_id: &Address, + ) -> Result { + Ok(e.invoke_contract::(contract_id, &Symbol::new(&e, "get_escrow"), Vec::new(&e))) + } + + pub fn get_escrow(e: Env) -> Result { + e.storage() + .instance() + .get(&DataKey::Escrow) + .ok_or(ContractError::EscrowNotFound)? + } +} diff --git a/contracts/Contract-Events-for-Indexer/src/core/milestone.rs b/contracts/Contract-Events-for-Indexer/src/core/milestone.rs new file mode 100644 index 0000000..210d40e --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/core/milestone.rs @@ -0,0 +1,78 @@ +use crate::core::escrow::EscrowManager; +use crate::error::ContractError; +use crate::events::escrows_by_contract_id; +use crate::storage::types::DataKey; +use soroban_sdk::{Address, Env, String}; + +use super::validators::milestone::{ + validate_milestone_flag_change_conditions, validate_milestone_status_change_conditions, +}; + +pub struct MilestoneManager; + +impl MilestoneManager { + pub fn change_milestone_status( + e: Env, + milestone_index: i128, + new_status: String, + new_evidence: Option, + service_provider: Address, + ) -> Result<(), ContractError> { + service_provider.require_auth(); + let mut existing_escrow = EscrowManager::get_escrow(e.clone())?; + + validate_milestone_status_change_conditions( + &existing_escrow, + milestone_index, + &service_provider, + )?; + + let mut milestone_to_update = existing_escrow + .milestones + .get(milestone_index as u32) + .ok_or(ContractError::InvalidMileStoneIndex)?; + + if let Some(evidence) = new_evidence { + milestone_to_update.evidence = evidence; + } + + milestone_to_update.status = new_status; + + existing_escrow + .milestones + .set(milestone_index as u32, milestone_to_update); + e.storage() + .instance() + .set(&DataKey::Escrow, &existing_escrow); + escrows_by_contract_id(&e, existing_escrow.engagement_id.clone(), existing_escrow); + Ok(()) + } + + pub fn change_milestone_approved_flag( + e: Env, + milestone_index: i128, + new_flag: bool, + approver: Address, + ) -> Result<(), ContractError> { + approver.require_auth(); + let mut existing_escrow = EscrowManager::get_escrow(e.clone())?; + + validate_milestone_flag_change_conditions(&existing_escrow, milestone_index, &approver)?; + + let mut milestone_to_update = existing_escrow + .milestones + .get(milestone_index as u32) + .ok_or(ContractError::InvalidMileStoneIndex)?; + + milestone_to_update.approved = new_flag; + + existing_escrow + .milestones + .set(milestone_index as u32, milestone_to_update); + e.storage() + .instance() + .set(&DataKey::Escrow, &existing_escrow); + escrows_by_contract_id(&e, existing_escrow.engagement_id.clone(), existing_escrow); + Ok(()) + } +} diff --git a/contracts/Contract-Events-for-Indexer/src/core/validators/dispute.rs b/contracts/Contract-Events-for-Indexer/src/core/validators/dispute.rs new file mode 100644 index 0000000..e0c317f --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/core/validators/dispute.rs @@ -0,0 +1,72 @@ +use soroban_sdk::Address; + +use crate::{ + error::ContractError, + modules::fee::DisputeFeeResult, + storage::types::{Escrow, Roles}, +}; + +#[inline] +pub fn validate_dispute_resolution_conditions( + escrow: &Escrow, + dispute_resolver: &Address, + approver_funds: i128, + receiver_funds: i128, + total_funds: i128, + fee_result: &DisputeFeeResult, + current_balance: i128, +) -> Result<(), ContractError> { + if dispute_resolver != &escrow.roles.dispute_resolver { + return Err(ContractError::OnlyDisputeResolverCanExecuteThisFunction); + } + + if !escrow.flags.disputed { + return Err(ContractError::EscrowNotInDispute); + } + + if total_funds != current_balance { + return Err(ContractError::ReceiverAndApproverFundsNotEqual); + } + + if approver_funds < fee_result.net_approver_funds { + return Err(ContractError::InsufficientApproverFundsForCommissions); + } + + if receiver_funds < fee_result.net_receiver_funds { + return Err(ContractError::InsufficientServiceProviderFundsForCommissions); + } + + Ok(()) +} + +#[inline] +pub fn validate_dispute_flag_change_conditions( + escrow: &Escrow, + signer: &Address, +) -> Result<(), ContractError> { + if escrow.flags.disputed { + return Err(ContractError::EscrowAlreadyInDispute); + } + + let Roles { + approver, + service_provider, + platform_address, + release_signer, + dispute_resolver, + receiver, + } = &escrow.roles; + + let is_authorized = signer == approver + || signer == service_provider + || signer == platform_address + || signer == release_signer + || signer == dispute_resolver + || signer == receiver; + + if !is_authorized { + return Err(ContractError::UnauthorizedToChangeDisputeFlag); + } + + Ok(()) +} diff --git a/contracts/Contract-Events-for-Indexer/src/core/validators/escrow.rs b/contracts/Contract-Events-for-Indexer/src/core/validators/escrow.rs new file mode 100644 index 0000000..306da9b --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/core/validators/escrow.rs @@ -0,0 +1,86 @@ +use soroban_sdk::{Address, Env}; + +use crate::{ + error::ContractError, + storage::types::{DataKey, Escrow}, +}; + +#[inline] +pub fn validate_release_conditions( + escrow: &Escrow, + release_signer: &Address, +) -> Result<(), ContractError> { + if escrow.flags.released { + return Err(ContractError::EscrowAlreadyResolved); + } + + if release_signer != &escrow.roles.release_signer { + return Err(ContractError::OnlyReleaseSignerCanReleaseEarnings); + } + + if escrow.milestones.is_empty() { + return Err(ContractError::NoMileStoneDefined); + } + + if !escrow + .milestones + .iter() + .all(|milestone| milestone.approved) + { + return Err(ContractError::EscrowNotCompleted); + } + + if escrow.flags.disputed { + return Err(ContractError::EscrowOpenedForDisputeResolution); + } + + Ok(()) +} + +#[inline] +pub fn validate_escrow_property_change_conditions( + existing_escrow: &Escrow, + platform_address: &Address, + contract_balance: i128, +) -> Result<(), ContractError> { + + if platform_address != &existing_escrow.roles.platform_address { + return Err(ContractError::OnlyPlatformAddressExecuteThisFunction); + } + + for milestone in existing_escrow.milestones.iter() { + if milestone.approved { + return Err(ContractError::MilestoneApprovedCantChangeEscrowProperties); + } + } + + if contract_balance > 0 { + return Err(ContractError::EscrowHasFunds); + } + + if existing_escrow.flags.disputed { + return Err(ContractError::EscrowOpenedForDisputeResolution); + } + + Ok(()) +} + +#[inline] +pub fn validate_initialize_escrow_conditions( + e: Env, + escrow_properties: Escrow, +) -> Result<(), ContractError> { + if e.storage().instance().has(&DataKey::Escrow) { + return Err(ContractError::EscrowAlreadyInitialized); + } + + if escrow_properties.amount == 0 { + return Err(ContractError::AmountCannotBeZero); + } + + if escrow_properties.milestones.len() > 10 { + return Err(ContractError::TooManyMilestones); + } + + Ok(()) +} diff --git a/contracts/Contract-Events-for-Indexer/src/core/validators/milestone.rs b/contracts/Contract-Events-for-Indexer/src/core/validators/milestone.rs new file mode 100644 index 0000000..17f4b52 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/core/validators/milestone.rs @@ -0,0 +1,45 @@ +use soroban_sdk::Address; + +use crate::{error::ContractError, storage::types::Escrow}; + +#[inline] +pub fn validate_milestone_status_change_conditions( + escrow: &Escrow, + milestone_index: i128, + service_provider: &Address, +) -> Result<(), ContractError> { + if service_provider != &escrow.roles.service_provider { + return Err(ContractError::OnlyServiceProviderChangeMilstoneStatus); + } + + if escrow.milestones.is_empty() { + return Err(ContractError::NoMileStoneDefined); + } + + if milestone_index < 0 || milestone_index >= escrow.milestones.len() as i128 { + return Err(ContractError::InvalidMileStoneIndex); + } + + Ok(()) +} + +#[inline] +pub fn validate_milestone_flag_change_conditions( + escrow: &Escrow, + milestone_index: i128, + approver: &Address, +) -> Result<(), ContractError> { + if approver != &escrow.roles.approver { + return Err(ContractError::OnlyApproverChangeMilstoneFlag); + } + + if escrow.milestones.is_empty() { + return Err(ContractError::NoMileStoneDefined); + } + + if milestone_index < 0 || milestone_index >= escrow.milestones.len() as i128 { + return Err(ContractError::InvalidMileStoneIndex); + } + + Ok(()) +} diff --git a/contracts/Contract-Events-for-Indexer/src/error.rs b/contracts/Contract-Events-for-Indexer/src/error.rs new file mode 100644 index 0000000..40175d4 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/error.rs @@ -0,0 +1,161 @@ +use core::fmt; +use soroban_sdk::contracterror; + +#[derive(Debug, Copy, Clone, PartialEq)] +#[contracterror] +pub enum ContractError { + EscrowNotFunded = 1, + AmountCannotBeZero = 2, + EscrowAlreadyInitialized = 3, + OnlySignerCanFundEscrow = 4, + EscrowAlreadyFunded = 5, + EscrowFullyFunded = 6, + SignerInsufficientFunds = 7, + NotEnoughAllowance = 8, + EscrowAlreadyCompleted = 9, + SignerInsufficientFundsToComplete = 10, + OnlySignerCanRequestRefund = 11, + NoFundsToRefund = 12, + ContractHasInsufficientBalance = 13, + EscrowNotFound = 14, + OnlyReleaseSignerCanReleaseEarnings = 15, + EscrowNotCompleted = 16, + EscrowBalanceNotEnoughToSendEarnings = 17, + ContractInsufficientFunds = 18, + OnlyPlatformAddressExecuteThisFunction = 19, + EscrowNotInitialized = 20, + OnlyServiceProviderChangeMilstoneStatus = 21, + NoMileStoneDefined = 22, + InvalidMileStoneIndex = 23, + OnlyApproverChangeMilstoneFlag = 24, + OnlyDisputeResolverCanExecuteThisFunction = 25, + EscrowAlreadyInDispute = 26, + EscrowNotInDispute = 27, + InsufficientFundsForResolution = 28, + InvalidState = 29, + EscrowOpenedForDisputeResolution = 30, + AmountToDepositGreatherThanEscrowAmount = 31, + Overflow = 32, + Underflow = 33, + DivisionError = 34, + AdminNotFound = 35, + InsufficientApproverFundsForCommissions = 36, + InsufficientServiceProviderFundsForCommissions = 37, + MilestoneApprovedCantChangeEscrowProperties = 38, + EscrowHasFunds = 39, + EscrowAlreadyResolved = 40, + TooManyEscrowsRequested = 41, + UnauthorizedToChangeDisputeFlag = 42, + ArgumentConversionFailed = 43, + TooManyMilestones = 44, + ReceiverAndApproverFundsNotEqual = 45, +} + +impl fmt::Display for ContractError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ContractError::EscrowNotFunded => write!(f, "Escrow not funded"), + ContractError::AmountCannotBeZero => write!(f, "Amount cannot be zero"), + ContractError::EscrowAlreadyInitialized => write!(f, "Escrow already initialized"), + ContractError::OnlySignerCanFundEscrow => { + write!(f, "Only the signer can fund the escrow") + } + ContractError::EscrowAlreadyFunded => write!(f, "Escrow already funded"), + ContractError::EscrowFullyFunded => write!(f, "This escrow is already fully funded"), + ContractError::SignerInsufficientFunds => { + write!(f, "The signer does not have sufficient funds") + } + ContractError::NotEnoughAllowance => { + write!(f, "Not enough allowance to fund this escrow") + } + ContractError::EscrowAlreadyCompleted => write!(f, "Escrow already completed"), + ContractError::SignerInsufficientFundsToComplete => write!( + f, + "The signer does not have sufficient funds to complete this escrow" + ), + ContractError::OnlySignerCanRequestRefund => { + write!(f, "Only the signer can request a refund") + } + ContractError::NoFundsToRefund => write!(f, "No funds available to refund"), + ContractError::ContractHasInsufficientBalance => { + write!(f, "The contract has no balance to repay") + } + ContractError::EscrowNotFound => write!(f, "Escrow not found"), + ContractError::OnlyReleaseSignerCanReleaseEarnings => write!( + f, + "Only the release signer can release the escrow earnings" + ), + ContractError::EscrowNotCompleted => { + write!(f, "The escrow must be completed to release earnings") + } + ContractError::EscrowBalanceNotEnoughToSendEarnings => write!( + f, + "The escrow balance must be equal to the amount of earnings defined for the escrow" + ), + ContractError::ContractInsufficientFunds => { + write!(f, "The contract does not have sufficient funds") + } + ContractError::OnlyPlatformAddressExecuteThisFunction => write!( + f, + "Only the platform address should be able to execute this function" + ), + ContractError::EscrowNotInitialized => write!(f, "Escrow not initialized"), + ContractError::OnlyServiceProviderChangeMilstoneStatus => { + write!(f, "Only the service provider can change milestone status") + } + ContractError::NoMileStoneDefined => write!(f, "Escrow initialized without milestone"), + ContractError::InvalidMileStoneIndex => write!(f, "Invalid milestone index"), + ContractError::OnlyApproverChangeMilstoneFlag => { + write!(f, "Only the approver can change milestone flag") + } + ContractError::OnlyDisputeResolverCanExecuteThisFunction => { + write!(f, "Only the dispute resolver can execute this function") + } + ContractError::EscrowAlreadyInDispute => write!(f, "Escrow already in dispute"), + ContractError::EscrowNotInDispute => write!(f, "Escrow not in dispute"), + ContractError::InsufficientFundsForResolution => { + write!(f, "Insufficient funds for resolution") + } + ContractError::InvalidState => write!(f, "Invalid state"), + ContractError::EscrowOpenedForDisputeResolution => { + write!(f, "Escrow has been opened for dispute resolution") + } + ContractError::AmountToDepositGreatherThanEscrowAmount => { + write!(f, "Amount to deposit is greater than the escrow amount") + } + ContractError::InsufficientApproverFundsForCommissions => { + write!(f, "Insufficient approver funds for commissions") + } + ContractError::InsufficientServiceProviderFundsForCommissions => { + write!(f, "Insufficient Service Provider funds for commissions") + } + ContractError::MilestoneApprovedCantChangeEscrowProperties => { + write!( + f, + "You can't change the escrow properties after the milestone is approved" + ) + } + ContractError::EscrowHasFunds => write!(f, "Escrow has funds"), + ContractError::Overflow => write!(f, "This operation can cause an Overflow"), + ContractError::Underflow => write!(f, "This operation can cause an Underflow"), + ContractError::DivisionError => write!(f, "This operation can cause Division error"), + ContractError::AdminNotFound => write!(f, "Admin not found!"), + ContractError::EscrowAlreadyResolved => write!(f, "This escrow is already resolved"), + ContractError::TooManyEscrowsRequested => { + write!(f, "You have requested too many escrows") + } + ContractError::UnauthorizedToChangeDisputeFlag => { + write!(f, "You are not authorized to change the dispute flag") + } + ContractError::ArgumentConversionFailed => { + write!(f, "Argument conversion failed") + } + ContractError::TooManyMilestones => { + write!(f, "Cannot define more than 10 milestones in an escrow") + } + ContractError::ReceiverAndApproverFundsNotEqual => { + write!(f, "The approver's and receiver's funds must equal the current escrow balance.") + } + } + } +} diff --git a/contracts/Contract-Events-for-Indexer/src/events/handler.rs b/contracts/Contract-Events-for-Indexer/src/events/handler.rs new file mode 100644 index 0000000..94417fa --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/events/handler.rs @@ -0,0 +1,13 @@ +use crate::storage::types::Escrow; +use soroban_sdk::{symbol_short, vec, Env, IntoVal, String, Val}; + +// ------ Escrows +pub fn escrows_by_contract_id(e: &Env, escrow_id: String, escrow: Escrow) { + let topics = (symbol_short!("p_by_spdr"),); + + let escrow_id_val: Val = escrow_id.into_val(e); + let escrow_val: Val = escrow.into_val(e); + + let event_payload = vec![e, escrow_id_val, escrow_val]; + e.events().publish(topics, event_payload); +} diff --git a/contracts/Contract-Events-for-Indexer/src/lib.rs b/contracts/Contract-Events-for-Indexer/src/lib.rs new file mode 100644 index 0000000..b5e3f67 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/lib.rs @@ -0,0 +1,48 @@ +#![no_std] + +mod contract; +mod core { + pub mod dispute; + pub mod escrow; + pub mod milestone; + pub use dispute::*; + pub use escrow::*; + pub use milestone::*; + pub mod validators { + pub mod dispute; + pub mod escrow; + pub mod milestone; + } +} +mod error; +mod events { + pub mod handler; + pub(crate) use handler::escrows_by_contract_id; +} +mod modules { + pub mod math { + pub mod basic; + pub mod safe; + + pub use basic::*; + pub use safe::*; + } + + pub mod fee { + pub mod calculator; + + pub use calculator::*; + } + +} + +/// This module is currently Work In Progress. +mod storage { + pub mod types; +} +mod tests { + #[cfg(test)] + mod test; +} + +pub use crate::contract::EscrowContract; diff --git a/contracts/Contract-Events-for-Indexer/src/modules/fee/calculator.rs b/contracts/Contract-Events-for-Indexer/src/modules/fee/calculator.rs new file mode 100644 index 0000000..dbdecb1 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/modules/fee/calculator.rs @@ -0,0 +1,108 @@ +use crate::{ + error::ContractError, + modules::{ + math::{BasicArithmetic, BasicMath}, + math::{SafeArithmetic, SafeMath}, + }, +}; + +const TRUSTLESS_WORK_FEE_BPS: i128 = 30; +const BASIS_POINTS_DENOMINATOR: i128 = 10000; + +#[derive(Debug, Clone)] +pub struct StandardFeeResult { + pub trustless_work_fee: i128, + pub platform_fee: i128, + pub receiver_amount: i128, +} + +#[derive(Debug, Clone)] +pub struct DisputeFeeResult { + pub trustless_work_fee: i128, + pub platform_fee: i128, + pub net_approver_funds: i128, + pub net_receiver_funds: i128, +} + +pub trait FeeCalculatorTrait { + fn calculate_standard_fees( + total_amount: i128, + platform_fee_bps: i128, + ) -> Result; + + fn calculate_dispute_fees( + approver_funds: i128, + receiver_funds: i128, + platform_fee_bps: i128, + total_resolved_funds: i128, + ) -> Result; +} + +#[derive(Clone)] +pub struct FeeCalculator; + +impl FeeCalculatorTrait for FeeCalculator { + fn calculate_standard_fees( + total_amount: i128, + platform_fee_bps: i128, + ) -> Result { + let trustless_work_fee = SafeMath::safe_mul_div( + total_amount, + TRUSTLESS_WORK_FEE_BPS, + BASIS_POINTS_DENOMINATOR, + )?; + let platform_fee = + SafeMath::safe_mul_div(total_amount, platform_fee_bps, BASIS_POINTS_DENOMINATOR)?; + + let after_tw = BasicMath::safe_sub(total_amount, trustless_work_fee)?; + let receiver_amount = BasicMath::safe_sub(after_tw, platform_fee)?; + + Ok(StandardFeeResult { + trustless_work_fee, + platform_fee, + receiver_amount, + }) + } + + fn calculate_dispute_fees( + approver_funds: i128, + receiver_funds: i128, + platform_fee_bps: i128, + total_resolved_funds: i128, + ) -> Result { + let trustless_work_fee = SafeMath::safe_mul_div( + total_resolved_funds, + TRUSTLESS_WORK_FEE_BPS, + BASIS_POINTS_DENOMINATOR, + )?; + let platform_fee = SafeMath::safe_mul_div( + total_resolved_funds, + platform_fee_bps, + BASIS_POINTS_DENOMINATOR, + )?; + let total_fees = BasicMath::safe_add(trustless_work_fee, platform_fee)?; + + let net_approver_funds = if total_resolved_funds > 0 { + let approver_fee_share = + SafeMath::safe_mul_div(approver_funds, total_fees, total_resolved_funds)?; + BasicMath::safe_sub(approver_funds, approver_fee_share)? + } else { + 0 + }; + + let net_receiver_funds = if total_resolved_funds > 0 { + let receiver_fee_share = + SafeMath::safe_mul_div(receiver_funds, total_fees, total_resolved_funds)?; + BasicMath::safe_sub(receiver_funds, receiver_fee_share)? + } else { + 0 + }; + + Ok(DisputeFeeResult { + trustless_work_fee, + platform_fee, + net_approver_funds, + net_receiver_funds, + }) + } +} diff --git a/contracts/Contract-Events-for-Indexer/src/modules/math/basic.rs b/contracts/Contract-Events-for-Indexer/src/modules/math/basic.rs new file mode 100644 index 0000000..9f0133f --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/modules/math/basic.rs @@ -0,0 +1,18 @@ +use crate::error::ContractError; + +pub struct BasicMath; + +pub trait BasicArithmetic { + fn safe_add(a: i128, b: i128) -> Result; + fn safe_sub(a: i128, b: i128) -> Result; +} + +impl BasicArithmetic for BasicMath { + fn safe_add(a: i128, b: i128) -> Result { + a.checked_add(b).ok_or(ContractError::Overflow) + } + + fn safe_sub(a: i128, b: i128) -> Result { + a.checked_sub(b).ok_or(ContractError::Underflow) + } +} diff --git a/contracts/Contract-Events-for-Indexer/src/modules/math/safe.rs b/contracts/Contract-Events-for-Indexer/src/modules/math/safe.rs new file mode 100644 index 0000000..3c7a424 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/modules/math/safe.rs @@ -0,0 +1,17 @@ +use crate::error::ContractError; + +pub struct SafeMath; + +pub trait SafeArithmetic { + fn safe_mul_div(amount: i128, multiplier: i128, divisor: i128) -> Result; +} + +impl SafeArithmetic for SafeMath { + fn safe_mul_div(amount: i128, multiplier: i128, divisor: i128) -> Result { + amount + .checked_mul(multiplier) + .ok_or(ContractError::Overflow)? + .checked_div(divisor) + .ok_or(ContractError::DivisionError) + } +} diff --git a/contracts/Contract-Events-for-Indexer/src/storage/types.rs b/contracts/Contract-Events-for-Indexer/src/storage/types.rs new file mode 100644 index 0000000..e387603 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/storage/types.rs @@ -0,0 +1,66 @@ +use soroban_sdk::{contracttype, Address, String, Vec}; + +#[contracttype] +#[derive(Clone, PartialEq, Eq)] +pub struct Escrow { + pub engagement_id: String, + pub title: String, + pub roles: Roles, + pub description: String, + pub amount: i128, + pub platform_fee: i128, + pub milestones: Vec, + pub flags: Flags, + pub trustline: Trustline, + pub receiver_memo: i128, +} + +#[contracttype] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Milestone { + pub description: String, + pub status: String, + pub evidence: String, + pub approved: bool, +} + +#[contracttype] +#[derive(Clone, PartialEq, Eq)] +pub struct Roles { + pub approver: Address, + pub service_provider: Address, + pub platform_address: Address, + pub release_signer: Address, + pub dispute_resolver: Address, + pub receiver: Address, +} + +#[contracttype] +#[derive(Clone, PartialEq, Eq)] +pub struct Flags { + pub disputed: bool, + pub released: bool, + pub resolved: bool, +} + +#[contracttype] +#[derive(Clone, PartialEq, Eq)] +pub struct Trustline { + pub address: Address, + pub decimals: u32, +} + +#[contracttype] +#[derive(Clone)] +pub struct AddressBalance { + pub address: Address, + pub balance: i128, + pub trustline_decimals: u32, +} + +#[derive(Clone)] +#[contracttype] +pub enum DataKey { + Admin, + Escrow, +} diff --git a/contracts/Contract-Events-for-Indexer/src/tests/test.rs b/contracts/Contract-Events-for-Indexer/src/tests/test.rs new file mode 100644 index 0000000..081e023 --- /dev/null +++ b/contracts/Contract-Events-for-Indexer/src/tests/test.rs @@ -0,0 +1,1407 @@ +#![cfg(test)] + +extern crate std; + +use crate::storage::types::{ Escrow, Flags, Milestone, Roles, Trustline }; +use crate::contract::EscrowContract; +use crate::contract::EscrowContractClient; + +use soroban_sdk::Vec; +use soroban_sdk::{ testutils::Address as _, vec, Address, Env, String, token }; +use token::Client as TokenClient; +use token::StellarAssetClient as TokenAdminClient; +// use test_token::token::{Token, TokenClient}; + +fn create_usdc_token<'a>(e: &Env, admin: &Address) -> (TokenClient<'a>, TokenAdminClient<'a>) { + let sac = e.register_stellar_asset_contract_v2(admin.clone()); + ( + TokenClient::new(e, &sac.address()), + TokenAdminClient::new(e, &sac.address()), + ) +} + +struct TestData<'a> { + client: EscrowContractClient<'a>, +} + +fn create_escrow_contract<'a>(env: &Env) -> TestData { + env.mock_all_auths(); + let admin = Address::generate(env); + let client = EscrowContractClient::new( + env, + &env.register( + EscrowContract {}, + ( + admin.clone(), + ) + ), + ); + + TestData { + client, + } +} + +#[test] +fn test_initialize_excrow() { + let env = Env::default(); + env.mock_all_auths(); + + let approver_address = Address::generate(&env); + let admin = Address::generate(&env); + let platform_address = Address::generate(&env); + let amount: i128 = 100_000_000; + let service_provider_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let _receiver_address = Address::generate(&env); + let platform_fee = 3; + let milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + Milestone { + description: String::from_str(&env, "Second milestone"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + ]; + + let usdc_token = create_usdc_token(&env, &admin); + + let engagement_id = String::from_str(&env, "41431"); + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: service_provider_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: milestones, + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + let initialized_escrow = escrow_approver.initialize_escrow(&escrow_properties); + + let escrow = escrow_approver.get_escrow(); + assert_eq!(escrow.engagement_id, initialized_escrow.engagement_id); + assert_eq!(escrow.roles.approver, escrow_properties.roles.approver); + assert_eq!( + escrow.roles.service_provider, + escrow_properties.roles.service_provider + ); + assert_eq!( + escrow.roles.platform_address, + escrow_properties.roles.platform_address + ); + assert_eq!(escrow.amount, amount); + assert_eq!(escrow.platform_fee, platform_fee); + assert_eq!(escrow.milestones, escrow_properties.milestones); + assert_eq!( + escrow.roles.release_signer, + escrow_properties.roles.release_signer + ); + assert_eq!( + escrow.roles.dispute_resolver, + escrow_properties.roles.dispute_resolver + ); + assert_eq!(escrow.roles.receiver, escrow_properties.roles.receiver); + assert_eq!(escrow.receiver_memo, escrow_properties.receiver_memo); + + let result = escrow_approver.try_initialize_escrow(&escrow_properties); + assert!(result.is_err()); +} + +#[test] +fn test_update_escrow() { + let env = Env::default(); + env.mock_all_auths(); + + let approver_address = Address::generate(&env); + let admin = Address::generate(&env); + let platform_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let _receiver_address = Address::generate(&env); + + let amount: i128 = 100_000_000; + let platform_fee = (0.3 * 10i128.pow(18) as f64) as i128; + + let initial_milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + Milestone { + description: String::from_str(&env, "Second milestone"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + ]; + + let usdc_token = create_usdc_token(&env, &admin); + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: service_provider_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let engagement_id = String::from_str(&env, "test_escrow_2"); + let initial_escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles: roles.clone(), + amount: amount, + platform_fee: platform_fee, + milestones: initial_milestones.clone(), + flags: flags.clone(), + trustline: trustline.clone(), + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&initial_escrow_properties); + + // Create a new updated escrow properties + let new_milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone updated"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + Milestone { + description: String::from_str(&env, "Second milestone updated"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + Milestone { + description: String::from_str(&env, "Third milestone new"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + ]; + + let updated_escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow Updated"), + description: String::from_str(&env, "Test Escrow Description Updated"), + roles, + amount: amount * 2, + platform_fee: platform_fee * 2, + milestones: new_milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + // Update escrow properties + let _updated_escrow = + escrow_approver.update_escrow(&platform_address, &updated_escrow_properties); + + // Verify updated escrow properties + let escrow = escrow_approver.get_escrow(); + assert_eq!(escrow.title, updated_escrow_properties.title); + assert_eq!(escrow.description, updated_escrow_properties.description); + assert_eq!(escrow.amount, updated_escrow_properties.amount); + assert_eq!(escrow.platform_fee, updated_escrow_properties.platform_fee); + assert_eq!(escrow.milestones, updated_escrow_properties.milestones); + assert_eq!( + escrow.roles.release_signer, + updated_escrow_properties.roles.release_signer + ); + assert_eq!( + escrow.roles.dispute_resolver, + updated_escrow_properties.roles.dispute_resolver + ); + assert_eq!( + escrow.roles.receiver, + updated_escrow_properties.roles.receiver + ); + assert_eq!( + escrow.receiver_memo, + updated_escrow_properties.receiver_memo + ); + + // Try to update escrow properties without platform address (should fail) + let non_platform_address = Address::generate(&env); + let result = escrow_approver + .try_update_escrow(&non_platform_address, &updated_escrow_properties); + assert!(result.is_err()); +} + +#[test] +fn test_change_milestone_status_and_approved() { + let env = Env::default(); + env.mock_all_auths(); + + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let admin = Address::generate(&env); + let platform_address = Address::generate(&env); + let usdc_token = create_usdc_token(&env, &admin); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let amount: i128 = 100_000_000; + let platform_fee = (0.3 * 10i128.pow(18) as f64) as i128; + + let initial_milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "Milestone 1"), + status: String::from_str(&env, "in-progress"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + Milestone { + description: String::from_str(&env, "Milestone 2"), + status: String::from_str(&env, "in-progress"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: service_provider_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let engagement_id = String::from_str(&env, "test_escrow"); + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles: roles.clone(), + amount: amount, + platform_fee: platform_fee, + milestones: initial_milestones.clone(), + flags: flags.clone(), + trustline: trustline.clone(), + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + // Change milestone status (valid case) + let new_status = String::from_str(&env, "completed"); + let new_evidence = Some(String::from_str(&env, "New evidence")); + escrow_approver.change_milestone_status( + &(0 as i128), + &new_status, + &new_evidence, + &service_provider_address, + ); + + let updated_escrow = escrow_approver.get_escrow(); + assert_eq!(updated_escrow.milestones.get(0).unwrap().status, new_status); + assert_eq!( + updated_escrow.milestones.get(0).unwrap().evidence, + String::from_str(&env, "New evidence") + ); + + // Change milestone approved (valid case) + escrow_approver.approve_milestone(&(0 as i128), &true, &approver_address); + + let final_escrow = escrow_approver.get_escrow(); + assert!(final_escrow.milestones.get(0).unwrap().approved); + + let invalid_index = 10 as i128; + let new_status = String::from_str(&env, "completed"); + let new_evidence = Some(String::from_str(&env, "New evidence")); + + let result = escrow_approver.try_change_milestone_status( + &invalid_index, + &new_status, + &new_evidence, + &service_provider_address, + ); + assert!(result.is_err()); + + let result = + escrow_approver.try_approve_milestone(&invalid_index, &true, &approver_address); + assert!(result.is_err()); + + let unauthorized_address = Address::generate(&env); + + // Test for `change_status` by invalid service provider + let result = escrow_approver.try_change_milestone_status( + &(0 as i128), + &new_status, + &new_evidence, + &unauthorized_address, + ); + assert!(result.is_err()); + + // Test for `change_approved` by invalid approver + let result = + escrow_approver.try_approve_milestone(&(0 as i128), &true, &unauthorized_address); + assert!(result.is_err()); + + let test_data = create_escrow_contract(&env); + let new_escrow_approver = test_data.client; + + //Escrow Test with no milestone + let escrow_properties_v2: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Updated Escrow"), + description: String::from_str(&env, "Updated Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: vec![&env], + flags, + trustline, + receiver_memo: 0, + }; + + new_escrow_approver.initialize_escrow(&escrow_properties_v2); + + let result = new_escrow_approver.try_change_milestone_status( + &(0 as i128), + &new_status, + &new_evidence, + &service_provider_address, + ); + assert!(result.is_err()); + + let result = + new_escrow_approver.try_approve_milestone(&(0 as i128), &true, &approver_address); + assert!(result.is_err()); +} + +#[test] +fn test_release_funds_successful_flow() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let trustless_work_address = Address::generate(&env); + let _receiver_address = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + let amount: i128 = 100_000_000; + usdc_token.1.mint(&approver_address, &(amount as i128)); + + let platform_fee = 500; + + let milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Completed"), + evidence: String::from_str(&env, "Initial evidence"), + approved: true, + }, + Milestone { + description: String::from_str(&env, "Second milestone"), + status: String::from_str(&env, "Completed"), + evidence: String::from_str(&env, "Initial evidence"), + approved: true, + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: _receiver_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let engagement_id = String::from_str(&env, "test_escrow_1"); + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + usdc_token.1.mint(&escrow_approver.address, &(amount as i128)); + + escrow_approver.release_funds(&release_signer_address, &trustless_work_address); + + let total_amount = amount as i128; + let trustless_work_commission = ((total_amount * 30) / 10000) as i128; + let platform_commission = (total_amount * platform_fee as i128) / 10000 as i128; + let receiver_amount = + (total_amount - (trustless_work_commission + platform_commission)) as i128; + + assert_eq!( + usdc_token.0.balance(&trustless_work_address), + trustless_work_commission, + "Trustless Work commission amount is incorrect" + ); + + assert_eq!( + usdc_token.0.balance(&platform_address), + platform_commission, + "Platform commission amount is incorrect" + ); + + assert_eq!( + usdc_token.0.balance(&_receiver_address), + receiver_amount, + "Receiver received incorrect amount" + ); + + assert_eq!( + usdc_token.0.balance(&service_provider_address), + 0, + "Service Provider should have zero balance when using separate receiver" + ); + + assert_eq!( + usdc_token.0.balance(&escrow_approver.address), + 0, + "Contract should have zero balance after claiming earnings" + ); +} + +//test claim escrow earnings in failure scenarios +// Scenario 1: Escrow with no milestones: +#[test] +fn test_release_funds_no_milestones() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let _receiver_address = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + let engagement_id_no_milestones = String::from_str(&env, "test_no_milestones"); + let amount: i128 = 100_000_000; + let platform_fee = 30; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: service_provider_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id_no_milestones.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: vec![&env], + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + usdc_token.1.mint(&escrow_approver.address, &(amount as i128)); + + // Try to claim earnings with no milestones (should fail) + let result = escrow_approver.try_release_funds(&release_signer_address, &platform_address); + assert!(result.is_err()); +} + +// Scenario 2: Milestones incomplete +#[test] +fn test_release_funds_milestones_incomplete() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let _receiver_address = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + let engagement_id_incomplete_milestones = String::from_str(&env, "test_incomplete_milestones"); + let amount: i128 = 100_000_000; + let platform_fee = 30; + + // Define milestones with one not approved + let incomplete_milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Completed"), + evidence: String::from_str(&env, "Initial evidence"), + approved: true, + }, + Milestone { + description: String::from_str(&env, "Second milestone"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, // Not approved yet + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: service_provider_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id_incomplete_milestones.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: incomplete_milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + usdc_token.1.mint(&escrow_approver.address, &(amount as i128)); + + // Try to distribute earnings with incomplete milestones (should fail) + let result = escrow_approver.try_release_funds(&release_signer_address, &platform_address); + assert!(result.is_err()); +} + +#[test] +fn test_release_funds_same_receiver_as_provider() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let trustless_work_address = Address::generate(&env); + // Use service_provider_address as receiver to test same-address case + let _receiver_address = service_provider_address.clone(); + + let usdc_token = create_usdc_token(&env, &admin); + + let amount: i128 = 100_000_000; + usdc_token.1.mint(&approver_address, &(amount as i128)); + + let platform_fee = 500; + + let milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Completed"), + evidence: String::from_str(&env, "Initial evidence"), + approved: true, + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: _receiver_address.clone(), // Set to service_provider to test same-address case + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let engagement_id = String::from_str(&env, "test_escrow_same_receiver"); + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + usdc_token.1.mint(&escrow_approver.address, &(amount as i128)); + + escrow_approver.release_funds(&release_signer_address, &trustless_work_address); + + let total_amount = amount as i128; + let trustless_work_commission = ((total_amount * 30) / 10000) as i128; + let platform_commission = (total_amount * platform_fee as i128) / 10000 as i128; + let service_provider_amount = + (total_amount - (trustless_work_commission + platform_commission)) as i128; + + assert_eq!( + usdc_token.0.balance(&trustless_work_address), + trustless_work_commission, + "Trustless Work commission amount is incorrect" + ); + + assert_eq!( + usdc_token.0.balance(&platform_address), + platform_commission, + "Platform commission amount is incorrect" + ); + + assert_eq!( + usdc_token.0.balance(&service_provider_address), + service_provider_amount, + "Service Provider should receive funds when receiver is set to same address" + ); + + assert_eq!( + usdc_token.0.balance(&escrow_approver.address), + 0, + "Contract should have zero balance after claiming earnings" + ); +} + +#[test] +fn test_release_funds_invalid_receiver_fallback() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let trustless_work_address = Address::generate(&env); + + // Create a valid but separate receiver address + let _receiver_address = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + let amount: i128 = 100_000_000; + usdc_token.1.mint(&approver_address, &(amount as i128)); + + let platform_fee = 500; + + let milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Completed"), + evidence: String::from_str(&env, "Initial evidence"), + approved: true, + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: _receiver_address.clone(), // Different receiver address than service provider + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let engagement_id = String::from_str(&env, "test_escrow_receiver"); + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + usdc_token.1.mint(&escrow_approver.address, &(amount as i128)); + + escrow_approver.release_funds(&release_signer_address, &trustless_work_address); + + let total_amount = amount as i128; + let trustless_work_commission = ((total_amount * 30) / 10000) as i128; + let platform_commission = (total_amount * platform_fee as i128) / 10000 as i128; + let receiver_amount = + (total_amount - (trustless_work_commission + platform_commission)) as i128; + + assert_eq!( + usdc_token.0.balance(&trustless_work_address), + trustless_work_commission, + "Trustless Work commission amount is incorrect" + ); + + assert_eq!( + usdc_token.0.balance(&platform_address), + platform_commission, + "Platform commission amount is incorrect" + ); + + // Funds should go to the receiver (not service provider) + assert_eq!( + usdc_token.0.balance(&_receiver_address), + receiver_amount, + "Receiver should receive funds when set to a different address than service provider" + ); + + // The service provider should not receive funds when a different receiver is set + assert_eq!( + usdc_token.0.balance(&service_provider_address), + 0, + "Service provider should not receive funds when a different receiver is set" + ); + + assert_eq!( + usdc_token.0.balance(&escrow_approver.address), + 0, + "Contract should have zero balance after claiming earnings" + ); +} + +#[test] +fn test_dispute_management() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + + let engagement_id = String::from_str(&env, "test_dispute"); + let amount: i128 = 100_000_000; + let platform_fee = 30; + + let milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: service_provider_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + let escrow = escrow_approver.get_escrow(); + assert!(!escrow.flags.disputed); + + escrow_approver.dispute_escrow(&dispute_resolver_address); + + let escrow_after_change = escrow_approver.get_escrow(); + assert!(escrow_after_change.flags.disputed); + + usdc_token.1.mint(&approver_address, &(amount as i128)); + // Test block on distributing earnings during dispute + let result = escrow_approver.try_release_funds(&release_signer_address, &platform_address); + assert!(result.is_err()); + + let _ = escrow_approver.try_dispute_escrow(&dispute_resolver_address); + + let escrow_after_second_change = escrow_approver.get_escrow(); + assert!(escrow_after_second_change.flags.disputed); +} + +#[test] +fn test_dispute_resolution_process() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let trustless_work_address = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + let amount: i128 = 100_000_000; + usdc_token.1.mint(&approver_address, &(amount as i128)); + + let platform_fee = 500; + + let milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Completed"), + evidence: String::from_str(&env, "Initial evidence"), + approved: true, + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: service_provider_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let engagement_id = String::from_str(&env, "test_dispute_resolution"); + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + usdc_token.0.transfer(&approver_address, &escrow_approver.address, &amount); + + escrow_approver.dispute_escrow(&approver_address); + + let escrow_with_dispute = escrow_approver.get_escrow(); + assert!(escrow_with_dispute.flags.disputed); + + // Try to resolve dispute with incorrect dispute resolver (should fail) + let result = escrow_approver.try_resolve_dispute( + &approver_address, + &(50_000_000 as i128), + &(50_000_000 as i128), + &trustless_work_address, + ); + assert!(result.is_err()); + + let approver_funds: i128 = 50_000_000; + let insufficient_receiver_funds: i128 = 40_000_000; + + let incorrect_dispute_resolution_result = escrow_approver.try_resolve_dispute( + &dispute_resolver_address, + &approver_funds, + &insufficient_receiver_funds, + &trustless_work_address, + ); + + assert!(incorrect_dispute_resolution_result.is_err()); + + // Resolve dispute with correct dispute resolver (50/50 split) + let receiver_funds: i128 = 50_000_000; + + escrow_approver.resolve_dispute( + &dispute_resolver_address, + &approver_funds, + &receiver_funds, + &trustless_work_address, + ); + + // Verify dispute was resolved + let escrow_after_resolution = escrow_approver.get_escrow(); + assert!(!escrow_after_resolution.flags.disputed); + assert!(escrow_after_resolution.flags.resolved); + + let total_amount = amount as i128; + let trustless_work_commission = ((total_amount * 30) / 10000) as i128; + let platform_commission = (total_amount * platform_fee as i128) / 10000 as i128; + let remaining_amount = total_amount - (trustless_work_commission + platform_commission); + + let platform_amount = platform_commission; + let trustless_amount = trustless_work_commission; + let service_provider_amount = (remaining_amount * receiver_funds) / total_amount; + let approver_amount = (remaining_amount * approver_funds) / total_amount; + + // Check balances + assert_eq!( + usdc_token.0.balance(&trustless_work_address), + trustless_amount, + "Trustless Work commission amount is incorrect" + ); + + assert_eq!( + usdc_token.0.balance(&platform_address), + platform_amount, + "Platform commission amount is incorrect" + ); + + assert_eq!( + usdc_token.0.balance(&service_provider_address), + service_provider_amount, + "Service provider amount is incorrect" + ); + + assert_eq!( + usdc_token.0.balance(&approver_address), + approver_amount, + "Approver amount is incorrect" + ); +} + +#[test] +fn test_fund_escrow_successful_deposit() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let _receiver_address = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + let amount: i128 = 100_000_000; + usdc_token.1.mint(&approver_address, &amount); + + let platform_fee = 500; + + let milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: _receiver_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let engagement_id = String::from_str(&env, "test_escrow_fund"); + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + // Check initial balances + assert_eq!(usdc_token.0.balance(&approver_address), amount); + assert_eq!(usdc_token.0.balance(&escrow_approver.address ), 0); + + let deposit_amount = amount / 2; + escrow_approver.fund_escrow(&approver_address, &deposit_amount); + + // Check balances after deposit + assert_eq!( + usdc_token.0.balance(&approver_address), + amount - deposit_amount + ); + assert_eq!(usdc_token.0.balance(&escrow_approver.address), deposit_amount); + + // Deposit remaining amount + escrow_approver.fund_escrow(&approver_address, &deposit_amount); + + assert_eq!(usdc_token.0.balance(&approver_address), 0); + assert_eq!(usdc_token.0.balance(&escrow_approver.address), amount); +} + +#[test] +fn test_fund_escrow_signer_insufficient_funds_error() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver_address = Address::generate(&env); + let service_provider_address = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer_address = Address::generate(&env); + let dispute_resolver_address = Address::generate(&env); + let _receiver_address = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + let amount: i128 = 100_000_000; + // Only mint a small amount to the approver + let small_amount: i128 = 1_000_000; + usdc_token.1.mint(&approver_address, &small_amount); + + let platform_fee = 500; + + let milestones = vec![ + &env, + Milestone { + description: String::from_str(&env, "First milestone"), + status: String::from_str(&env, "Pending"), + evidence: String::from_str(&env, "Initial evidence"), + approved: false, + }, + ]; + + let roles: Roles = Roles { + approver: approver_address.clone(), + service_provider: service_provider_address.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer_address.clone(), + dispute_resolver: dispute_resolver_address.clone(), + receiver: _receiver_address.clone(), + }; + + let flags: Flags = Flags { + disputed: false, + released: false, + resolved: false, + }; + + let trustline: Trustline = Trustline { + address: usdc_token.0.address.clone(), + decimals: 10_000_000, + }; + + let engagement_id = String::from_str(&env, "test_escrow_insufficient_funds"); + let escrow_properties: Escrow = Escrow { + engagement_id: engagement_id.clone(), + title: String::from_str(&env, "Test Escrow"), + description: String::from_str(&env, "Test Escrow Description"), + roles, + amount: amount, + platform_fee: platform_fee, + milestones: milestones.clone(), + flags, + trustline, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_approver = test_data.client; + + escrow_approver.initialize_escrow(&escrow_properties); + + // Check initial balance + assert_eq!(usdc_token.0.balance(&approver_address), small_amount); + + // Try to deposit more than the approver has (should fail) + let result = escrow_approver.try_fund_escrow(&approver_address, &amount); + assert!(result.is_err()); + + // Verify balances didn't change + assert_eq!(usdc_token.0.balance(&approver_address), small_amount); + assert_eq!(usdc_token.0.balance(&escrow_approver.address), 0); +} + +#[test] +fn test_dispute_escrow_authorized_and_unauthorized() { + let env = Env::default(); + env.mock_all_auths(); + + let admin = Address::generate(&env); + let approver = Address::generate(&env); + let service_provider = Address::generate(&env); + let platform_address = Address::generate(&env); + let release_signer = Address::generate(&env); + let dispute_resolver = Address::generate(&env); + let receiver = Address::generate(&env); + let unauthorized = Address::generate(&env); + + let usdc_token = create_usdc_token(&env, &admin); + + let roles = Roles { + approver: approver.clone(), + service_provider: service_provider.clone(), + platform_address: platform_address.clone(), + release_signer: release_signer.clone(), + dispute_resolver: dispute_resolver.clone(), + receiver: receiver.clone(), + }; + + let escrow_base = Escrow { + engagement_id: String::from_str(&env, "engagement_001"), + title: String::from_str(&env, "Escrow for test"), + description: String::from_str(&env, "Test for dispute flag"), + roles, + amount: 10_000_000, + platform_fee: 0, + milestones: Vec::new(&env), + flags: Flags { + disputed: false, + released: false, + resolved: false, + }, + trustline: Trustline { + address: usdc_token.0.address.clone(), + decimals: 7, + }, + receiver_memo: 0, + }; + + let test_data = create_escrow_contract(&env); + let escrow_client_1 = test_data.client; + + escrow_client_1.initialize_escrow(&escrow_base); + escrow_client_1.dispute_escrow(&approver); + + let updated_escrow = escrow_client_1.get_escrow(); + assert!( + updated_escrow.flags.disputed, + "Dispute flag should be set to true for authorized address" + ); + + let test_data = create_escrow_contract(&env); + let escrow_client_2 = test_data.client; + + escrow_client_2.initialize_escrow(&escrow_base); + let result = escrow_client_2.try_dispute_escrow(&unauthorized); + + assert!( + result.is_err(), + "Unauthorized user should not be able to change dispute flag" + ); +}