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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions contracts/invoice-escrow/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,12 @@ pub enum Error {
InvalidMilestoneAmount = 23,
/// Cannot cancel because escrow is not in the correct state.
CancelNotAllowed = 24,
/// Penalty configuration is invalid (e.g. rate exceeds maximum).
InvalidPenaltyConfig = 25,
/// Payment token contract is invalid or does not implement the token interface.
InvalidPaymentToken = 26,
/// Invoice token contract is invalid or does not implement the required interface.
InvalidInvoiceToken = 27,
/// Payment token and invoice token must be different contracts.
IdenticalTokens = 28,
}
9 changes: 4 additions & 5 deletions contracts/invoice-escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ impl InvoiceEscrow {
if data.status == EscrowStatus::Cancelled {
return Err(Error::EscrowCancelled);
}
if data.status == EscrowStatus::Funded {
return Err(Error::EscrowFunded);
}
if data.status != EscrowStatus::Created {
return Err(Error::CancelNotAllowed);
}
Expand Down Expand Up @@ -254,9 +257,6 @@ impl InvoiceEscrow {
);
events::escrow_refunded(&env, invoice_id.clone(), amount_to_refund);
}
if data.funded_amt > 0 {
return Err(Error::EscrowPartiallyFunded);
}
data.status = EscrowStatus::Cancelled;
storage::set_escrow(&env, invoice_id.clone(), &data);
events::escrow_cancelled(&env, invoice_id.clone(), &seller);
Expand Down Expand Up @@ -534,8 +534,7 @@ impl InvoiceEscrow {
}
}
}

// 4. Release the purchase_price collateral back to the seller
// Seller receives the full payment amount
token.transfer(&contract, &data.seller, &amount);
}

Expand Down
61 changes: 45 additions & 16 deletions contracts/invoice-escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,8 @@ fn test_two_token_escrow_different_tokens() {
escrow_client.fund_escrow(&invoice_a, &buyer, &1000);
escrow_client.record_payment(&invoice_a, &payer, &1000);


assert_eq!(
escrow_client.get_escrow_status(&invoice_a),
EscrowStatus::Settled
);
// Token A balances
Expand Down Expand Up @@ -2373,14 +2374,13 @@ fn test_cancel_escrow_already_funded_rejected() {

// Cannot cancel once fully funded (status is Funded)
let res = client.try_cancel_escrow(&invoice_id, &seller);
assert_eq!(res, Err(Ok(Error::CancelNotAllowed)));
assert_eq!(res, Err(Ok(Error::EscrowFunded)));

let _ = pt_client;
}

#[test]
fn test_cancel_escrow_partially_funded_refunds() {
fn test_cancel_escrow_partially_funded_rejected() {
let env = Env::default();
env.mock_all_auths();

Expand All @@ -2399,7 +2399,6 @@ fn test_cancel_escrow_partially_funded_rejected() {
let seller = Address::generate(&env);
let buyer = Address::generate(&env);
let invoice_id = Symbol::new(&env, "INV_PART");
let invoice_id = Symbol::new(&env, "INV_PFUND");

pt_asset.mint(&buyer, &1000);

Expand All @@ -2420,25 +2419,55 @@ fn test_cancel_escrow_partially_funded_rejected() {
assert_eq!(pt_client.balance(&buyer), 500);
assert_eq!(pt_client.balance(&escrow_id), 500);

// Cancel while partially funded
// Cancel while partially funded should refund the buyer
client.cancel_escrow(&invoice_id, &seller);

assert_eq!(client.get_escrow_status(&invoice_id), EscrowStatus::Cancelled);

// Funds should be returned to buyer
assert_eq!(pt_client.balance(&escrow_id), 0);
assert_eq!(pt_client.balance(&buyer), 1000);
);
}

// Partial funding: status stays Created, but funds have already moved into escrow.
client.fund_escrow(&invoice_id, &buyer, &400);
assert_eq!(client.get_escrow_status(&invoice_id), EscrowStatus::Created);
#[test]
fn test_cancel_escrow_partially_funded_cancels_and_refunds() {
let env = Env::default();
env.mock_all_auths();

let res = client.try_cancel_escrow(&invoice_id, &seller);
assert_eq!(res, Err(Ok(Error::EscrowPartiallyFunded)));
let escrow_id = env.register_contract(None, InvoiceEscrow);
let client = InvoiceEscrowClient::new(&env, &escrow_id);
let admin = Address::generate(&env);
let inv_token_id = env.register_contract(None, MockInvoiceToken);

// Status must remain unchanged after the rejected cancellation attempt.
assert_eq!(client.get_escrow_status(&invoice_id), EscrowStatus::Created);
let pt_admin = Address::generate(&env);
let pt_id = env.register_stellar_asset_contract_v2(pt_admin.clone());
let pt_asset = AssetClient::new(&env, &pt_id.address());
let pt_client = TokenClient::new(&env, &pt_id.address());

client.initialize(&admin, &0);

let seller = Address::generate(&env);
let buyer = Address::generate(&env);
let invoice_id = Symbol::new(&env, "INV_PFUND");

pt_asset.mint(&buyer, &1000);

client.create_escrow(
&invoice_id,
&seller,
&seller,
&1000i128,
&1000i128,
&9_999_999u64,
&pt_id.address(),
&inv_token_id,
&test_commitment(&env, "test_invoice_data"),
&Some(500),
);
client.fund_escrow(&invoice_id, &buyer, &500);

// Partial funding cancellation should refund and succeed
client.cancel_escrow(&invoice_id, &seller);
assert_eq!(client.get_escrow_status(&invoice_id), EscrowStatus::Cancelled);
assert_eq!(pt_client.balance(&buyer), 1000);
}

#[test]
Expand Down Expand Up @@ -5180,7 +5209,7 @@ fn test_settlement_at_exact_due_date_state_persistence() {
let payer = Address::generate(&env);
let invoice_id = Symbol::new(&env, "INV_STATE_DT");
let amount = 2000i128;
let purchase_price = 1800i128;
let purchase_price = 2000i128;
let due_date = 60000u64;

env.ledger().with_mut(|li| li.timestamp = 10000);
Expand Down
Loading
Loading