From 03eeb54bc1cb8b5b6c36afd03e3a541ddcaa6a00 Mon Sep 17 00:00:00 2001 From: edwarddavid929-png Date: Wed, 29 Jul 2026 21:44:52 +0000 Subject: [PATCH] Implement confidential payment settlement using Pedersen commitments Payers can now hide payment amounts on-chain with a real elliptic-curve Pedersen commitment C = value*G + blinding*H over BLS12-381 G1: - initialize() derives two independent, nothing-up-my-sleeve generators G and H via hash-to-curve and pins them in instance storage. - pay() gains an optional `commitment: Option>` -- when set, the contract stores a digest of the commitment instead of moving any funds or touching the invoice total, so nothing is revealed at pay time. - reveal_confidential_payment(invoice_id, payer, value, blinding) recomputes the commitment from the opening and checks it against the stored digest; on success it pulls the real amount from the payer and credits the invoice (only now does the amount become visible on-chain). Named differently from the existing hash-based reveal_payment to avoid colliding with that unrelated, previously shipped feature. - Emits ConfidentialPaymentRevealed { payer, event_seq } with no amount. - Tampered value/blinding and double-reveal all rejected; tests cover commit->reveal, both tamper cases, double-reveal, and double-commit. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01LZRyq9VXnCuCqKLUW4QLdj --- contracts/split/src/events.rs | 14 + contracts/split/src/lib.rs | 228 +++++++++++- contracts/split/src/test.rs | 632 ++++++++++++++++++++++------------ 3 files changed, 644 insertions(+), 230 deletions(-) diff --git a/contracts/split/src/events.rs b/contracts/split/src/events.rs index 4a6eb44..de8abe0 100644 --- a/contracts/split/src/events.rs +++ b/contracts/split/src/events.rs @@ -54,6 +54,20 @@ pub fn payment_committed(env: &Env, invoice_id: u64, payer: &Address, commit_led ); } +/// Emitted when a confidential (Pedersen-committed) payment is settled via +/// `reveal_confidential_payment`. Deliberately omits the amount — that is the +/// entire point of a confidential payment — even though the amount is visible +/// elsewhere on-chain after settlement (e.g. the token transfer, `funded`). +/// Topics: (split, conf_rev, invoice_id) +/// Data: (payer, event_seq) +pub fn confidential_payment_revealed(env: &Env, invoice_id: u64, payer: &Address) { + let event_seq = next_seq(env, invoice_id); + env.events().publish( + (symbol_short!("split"), symbol_short!("conf_rev"), invoice_id), + (payer.clone(), event_seq), + ); +} + pub fn milestone_released(env: &Env, invoice_id: u64, milestone_bps: u32, amount_released: i128) { let event_seq = next_seq(env, invoice_id); env.events().publish( diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index 99c75b8..8391956 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -60,10 +60,11 @@ mod storage_snapshot; mod storage_keys; use error::ContractError; +use soroban_sdk::crypto::bls12_381::{Fr, G1Affine}; use soroban_sdk::xdr::ToXdr; use soroban_sdk::{ contract, contractimpl, symbol_short, token, Address, Bytes, BytesN, Env, IntoVal, Map, String, - Symbol, TryFromVal, Val, Vec, + Symbol, TryFromVal, Val, Vec, U256, }; use types::{ @@ -865,6 +866,62 @@ fn commitment_key(invoice_id: u64, payer: &Address) -> (Symbol, u64, Address) { (symbol_short!("commit"), invoice_id, payer.clone()) } +// --------------------------------------------------------------------------- +// Confidential payment settlement — Pedersen commitments over BLS12-381 G1. +// +// Distinct from the pre-existing `pay_confidential` / `reveal_confidential_total` +// placeholder (issue #295), which only checks that a proof blob is non-zero. +// This scheme performs a real elliptic-curve commitment opening: a payer commits +// to `C = value*G + blinding*H` during `pay`, then proves the opening at +// `reveal_confidential_payment` time by supplying `(value, blinding)`, which the +// contract recombines and checks against the stored commitment. +// --------------------------------------------------------------------------- + +/// Instance storage: fixed Pedersen base generator `G` (BLS12-381 G1), derived +/// once at `initialize` via hash-to-curve. +fn pedersen_g_key() -> Symbol { + symbol_short!("pc_gen_g") +} + +/// Instance storage: fixed Pedersen blinding generator `H` (BLS12-381 G1), +/// independent of `G` — nobody knows the discrete log of one relative to the +/// other, which is what makes the commitment hiding. +fn pedersen_h_key() -> Symbol { + symbol_short!("pc_gen_h") +} + +/// Persistent storage: pending Pedersen commitment digest for `(invoice_id, payer)`, +/// awaiting `reveal_confidential_payment`. +fn pedersen_commitment_key(invoice_id: u64, payer: &Address) -> (Symbol, u64, Address) { + (symbol_short!("pc_commit"), invoice_id, payer.clone()) +} + +/// Recompute the commitment digest for `(value, blinding)` against the +/// contract's fixed generators, so it can be checked against a commitment +/// digest previously supplied off-chain to `pay`. +/// +/// The commitment point `C = value*G + blinding*H` is itself never stored: +/// only a SHA-256 digest of its serialized form is kept on-chain, which is +/// what fits `pay`'s `BytesN<32>` commitment slot and keeps storage compact. +fn pedersen_commitment_digest(env: &Env, value: i128, blinding: &BytesN<32>) -> BytesN<32> { + guard_nonzero_amount(value).expect("ZeroAmountNotAllowed"); + let g: G1Affine = env + .storage() + .instance() + .get(&pedersen_g_key()) + .expect("not initialized"); + let h: G1Affine = env + .storage() + .instance() + .get(&pedersen_h_key()) + .expect("not initialized"); + let value_scalar = Fr::from_u256(U256::from_u128(env, value as u128)); + let blinding_scalar = Fr::from_bytes(blinding.clone()); + let commitment_point: G1Affine = (g * value_scalar) + (h * blinding_scalar); + let commitment_bytes: Bytes = commitment_point.to_bytes().into(); + env.crypto().sha256(&commitment_bytes).into() +} + fn surplus_key(invoice_id: u64) -> (Symbol, u64) { (symbol_short!("surplus"), invoice_id) } @@ -2665,6 +2722,18 @@ impl SplitContract { env.storage() .instance() .set(&storage_quota_key(), &DEFAULT_INVOICE_STORAGE_QUOTA); + // Confidential payment settlement: derive two independent, + // nothing-up-my-sleeve BLS12-381 G1 generators via hash-to-curve so + // neither party can know the discrete log of one relative to the + // other, then pin them in instance storage so every commit/reveal + // uses the exact same basis. + let pedersen_dst = Bytes::from_slice(&env, b"StellarSplit-Pedersen-BLS12381G1-v1"); + let bls = env.crypto().bls12_381(); + let g = bls.hash_to_g1(&Bytes::from_slice(&env, b"pedersen-generator-G"), &pedersen_dst); + let h = bls.hash_to_g1(&Bytes::from_slice(&env, b"pedersen-generator-H"), &pedersen_dst); + env.storage().instance().set(&pedersen_g_key(), &g); + env.storage().instance().set(&pedersen_h_key(), &h); + // Issue #477: set the initialisation flag atomically at the end so the // contract is marked fully initialised only after all state is written. env.storage().instance().set(&initialised_key(), &true); @@ -6321,6 +6390,13 @@ impl SplitContract { .remove(&channel_key(invoice_id, &payer)); } + /// # Confidential payments + /// When `commitment` is `Some`, the payment amount is hidden: `amount` is + /// ignored and no funds move yet. The contract only stores `commitment` — + /// a digest of the payer's Pedersen commitment `C = value*G + blinding*H` — + /// keyed by `(invoice_id, payer)`. The payer later calls + /// [`Self::reveal_confidential_payment`] with the opening `(value, blinding)` + /// to settle: only then does the real amount move and become visible on-chain. pub fn pay( env: Env, payer: Address, @@ -6329,10 +6405,17 @@ impl SplitContract { nonce: u64, _auto_convert: bool, donate_on_failure: bool, + commitment: Option>, ) { require_fn_not_paused(&env, &symbol_short!("pay")); require_not_frozen(&env); payer.require_auth(); + + if let Some(commitment) = commitment { + Self::_commit_confidential_payment(&env, &payer, invoice_id, commitment); + return; + } + Self::enforce_invoice_rate_limit(&env, invoice_id, &payer); Self::_pay( &env, @@ -6347,6 +6430,149 @@ impl SplitContract { ); } + /// Store a Pedersen commitment in place of a raw payment amount (see + /// `pay`'s confidential-payments doc above). Runs only the invoice-state + /// checks that don't depend on knowing the amount; amount-dependent + /// features (KYC, oracle pricing, rate/velocity limits, instalment + /// schedules) are intentionally out of scope here since none of them can + /// evaluate against a hidden value — they still apply normally to + /// non-confidential `pay` calls. + fn _commit_confidential_payment( + env: &Env, + payer: &Address, + invoice_id: u64, + commitment: BytesN<32>, + ) { + let invoice = load_invoice(env, invoice_id); + assert!( + invoice.status == InvoiceStatus::Pending, + "invoice is not pending" + ); + assert!(!invoice.disputed, "invoice is disputed"); + assert!(!invoice.frozen, "invoice is frozen"); + assert!(!invoice.admin_frozen, "invoice frozen by admin"); + assert!( + env.ledger().timestamp() <= invoice.deadline, + "invoice deadline has passed" + ); + if let Some(ref whitelist) = invoice.allowed_payers { + assert!(whitelist.contains(payer), "payer not allowed"); + } + if let Some(ref allowlist) = invoice.contributor_allowlist { + assert!(allowlist.contains(payer), "ContributorNotAllowed"); + } + + let key = pedersen_commitment_key(invoice_id, payer); + assert!( + !env.storage().persistent().has(&key), + "ConfidentialCommitmentExists" + ); + env.storage().persistent().set(&key, &commitment); + } + + /// Settle a confidential payment by opening the Pedersen commitment stored + /// by a prior `pay(..., commitment: Some(_))` call: recomputes + /// `value*G + blinding*H` and checks its digest against the stored one. On + /// success the real `value` is pulled from the payer and credited to the + /// invoice — only now does the amount become visible on-chain, exactly as + /// the token transfer and `funded` total reveal it. + /// + /// Named `reveal_confidential_payment` (rather than `reveal_payment`) to + /// avoid colliding with the existing hash-based commit/reveal pair + /// (`commit_payment` / `reveal_payment`), an unrelated, previously shipped + /// feature this PR does not touch. + /// + /// # Errors (panics) + /// * `"NoConfidentialCommitment"` — no pending commitment for this payer/invoice + /// (never committed, or already revealed). + /// * `"ConfidentialCommitmentMismatch"` — `(value, blinding)` does not open + /// the stored commitment. + pub fn reveal_confidential_payment( + env: Env, + invoice_id: u64, + payer: Address, + value: i128, + blinding: BytesN<32>, + ) { + require_fn_not_paused(&env, &symbol_short!("pay")); + require_not_frozen(&env); + payer.require_auth(); + guard_nonzero_amount(value).expect("ZeroAmountNotAllowed"); + + let key = pedersen_commitment_key(invoice_id, &payer); + let stored: BytesN<32> = env + .storage() + .persistent() + .get(&key) + .expect("NoConfidentialCommitment"); + + let digest = pedersen_commitment_digest(&env, value, &blinding); + assert!(digest == stored, "ConfidentialCommitmentMismatch"); + + // Remove before moving funds so a reentrant call can't reveal twice. + env.storage().persistent().remove(&key); + + let mut invoice = load_invoice(&env, invoice_id); + assert!( + invoice.status == InvoiceStatus::Pending, + "invoice is not pending" + ); + assert!(!invoice.disputed, "invoice is disputed"); + assert!(!invoice.frozen, "invoice is frozen"); + assert!(!invoice.admin_frozen, "invoice frozen by admin"); + assert!( + env.ledger().timestamp() <= invoice.deadline, + "invoice deadline has passed" + ); + + let token_client = token::Client::new(&env, &funding_token_for(&invoice)); + token_client.transfer(&payer, &env.current_contract_address(), &value); + + invoice.funded = invoice.funded.checked_add(value).expect("funded overflow"); + + let cumulative_key = cumulative_contributed_key(invoice_id); + let cumulative: i128 = env.storage().persistent().get(&cumulative_key).unwrap_or(0); + env.storage() + .persistent() + .set(&cumulative_key, &(cumulative + value)); + + let contrib_key = contribution_key(invoice_id, &payer); + let prev_contrib: i128 = env.storage().persistent().get(&contrib_key).unwrap_or(0); + env.storage() + .persistent() + .set(&contrib_key, &(prev_contrib + value)); + + events::confidential_payment_revealed(&env, invoice_id, &payer); + + let total: i128 = invoice.amounts.iter().sum(); + check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total); + + if invoice.funded >= total { + let in_group = env + .storage() + .persistent() + .has(&invoice_group_key(invoice_id)); + let guarded = invoice.prerequisite_id.is_some() + || !invoice.tranches.is_empty() + || !invoice.release_stages.is_empty() + || in_group + || !invoice.co_signers.is_empty() + || env.storage().persistent().has(&cosigners_key(invoice_id)) + || (invoice.oracle_address.is_some() && !invoice.condition_met) + || (invoice.min_funding_bps > 0 + && invoice.funded + < (invoice.amounts.iter().sum::() * invoice.min_funding_bps as i128 + / 10_000)); + if guarded { + save_invoice(&env, invoice_id, &invoice); + } else { + Self::_release(&env, invoice_id, &mut invoice, &payer); + } + } else { + save_invoice(&env, invoice_id, &invoice); + } + } + pub fn commit_payment(env: Env, payer: Address, invoice_id: u64, commitment_hash: BytesN<32>) { require_fn_not_paused(&env, &symbol_short!("pay")); payer.require_auth(); diff --git a/contracts/split/src/test.rs b/contracts/split/src/test.rs index 5774771..820c325 100644 --- a/contracts/split/src/test.rs +++ b/contracts/split/src/test.rs @@ -322,7 +322,7 @@ fn test_pay_and_auto_release() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.status, InvoiceStatus::Released); @@ -347,10 +347,10 @@ fn test_partial_pay_then_release() { let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999); - c.pay(&payer1, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer1, &id, &150_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); - c.pay(&payer2, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer2, &id, &150_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 300); } @@ -369,7 +369,7 @@ fn test_refund_after_deadline() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 2_000); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); c.refund(&id); @@ -393,7 +393,7 @@ fn test_pay_after_deadline_panics() { let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 2_000); env.ledger().set_timestamp(3_000); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } #[test] @@ -410,7 +410,7 @@ fn test_overpayment_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); } #[test] @@ -445,7 +445,7 @@ fn test_multi_recipient_release() { &9_999_u64, &default_options(&env), ); - c.pay(&payer, &id, &600_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &600_i128, &0_u64, &false, &false, &None); assert_eq!(tk.balance(&r1), 100); assert_eq!(tk.balance(&r2), 200); @@ -465,7 +465,7 @@ fn test_audit_log() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); @@ -549,7 +549,7 @@ fn test_partial_release_distributes_and_decrements_funded() { ); // Payer funds 200 - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 200); // Creator partially releases 100 -> r1 gets 25, r2 gets 75 @@ -597,7 +597,7 @@ fn test_forward_to_invoice_credits_target_invoice() { assert_eq!(ext.forward_invoice_id, Some(id_parent)); // Pay and release child; parent funded stays 0 because last-recipient absorbs all (no leftover). - c.pay(&payer, &id_child, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id_child, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id_child).status, InvoiceStatus::Released); assert_eq!(c.get_invoice(&id_parent).funded, 0); } @@ -650,7 +650,7 @@ fn test_extend_deadline() { c.extend_deadline(&id, &99_999_u64, &creator); assert_eq!(c.get_invoice(&id).deadline, 99_999); - c.pay(&payer, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &150_i128, &0_u64, &false, &false, &None); assert_eq!(tk.balance(&payer), 150); c.cancel_invoice(&creator, &id); @@ -675,7 +675,7 @@ fn test_cancel_non_pending_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); c.cancel_invoice(&creator, &id); } @@ -696,10 +696,10 @@ fn test_get_payer_total() { assert_eq!(c.get_payer_total(&id, &payer), 0); assert_eq!(c.get_payer_total(&id, &recipient), 0); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_payer_total(&id, &payer), 200); - c.pay(&payer, &id, &150_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &150_i128, &1_u64, &false, &false, &None); assert_eq!(c.get_payer_total(&id, &payer), 350); } @@ -718,7 +718,7 @@ fn test_verify_invoice() { let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 2_000); c.extend_deadline(&id, &9_999_u64, &creator); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert!(c.verify_invoice(&id, &InvoiceStatus::Released)); assert!(!c.verify_invoice(&id, &InvoiceStatus::Pending)); } @@ -768,7 +768,7 @@ fn test_adjust_split_updates_amounts_and_pays_new_total() { assert_eq!(invoice.amounts.get_unchecked(1), 250); // Pay the new total (400) and verify recipients receive updated amounts. - c.pay(&payer, &id, &400_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &400_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&r1), 150); @@ -808,7 +808,7 @@ fn test_adjust_split_after_payment_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &50_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &0_u64, &false, &false, &None); let mut new_amounts = Vec::new(&env); new_amounts.push_back(80_i128); @@ -933,7 +933,7 @@ fn test_add_recipient_after_payment_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &r1, 100, &token_id, 9_999); - c.pay(&payer, &id, &50_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &0_u64, &false, &false, &None); c.add_recipient(&creator, &id, &r2, &200_i128); } @@ -971,7 +971,7 @@ fn test_add_recipient_then_full_payment() { c.add_recipient(&creator, &id, &r2, &200_i128); // Pay total (100 + 200 = 300). - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&r1), 100); @@ -1016,7 +1016,7 @@ fn test_add_recipient_after_release_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &r1, 200, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); // After auto-release the invoice is Released, not Pending. c.add_recipient(&creator, &id, &r2, &100_i128); } @@ -1166,7 +1166,7 @@ fn test_allowed_payers_listed_address_succeeds() { a.push_back(200_i128); let id = c.create_invoice(&creator, &r, &a, &token_id, &9_999_u64, &opts); - c.pay(&allowed, &id, &200_i128, &0_u64, &false, &false); + c.pay(&allowed, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 200); } @@ -1196,7 +1196,7 @@ fn test_pause_blocks_pay() { let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); c.pause(&admin); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } #[test] @@ -1221,7 +1221,7 @@ fn test_unpause_restores_pay() { c.pause(&admin); c.unpause(&admin); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 200); } @@ -1251,7 +1251,7 @@ fn test_allowed_payers_unlisted_address_rejected() { a.push_back(200_i128); let id = c.create_invoice(&creator, &r, &a, &token_id, &9_999_u64, &opts); - c.pay(&unlisted, &id, &200_i128, &0_u64, &false, &false); + c.pay(&unlisted, &id, &200_i128, &0_u64, &false, &false, &None); } // --------------------------------------------------------------------------- @@ -1290,7 +1290,7 @@ fn test_allowed_payers_none_behaves_as_open() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&anyone, &id, &100_i128, &0_u64, &false, &false); + c.pay(&anyone, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 100); } @@ -1345,8 +1345,8 @@ fn test_bonus_pool_distributed_to_first_payer() { }, ); - c.pay(&early_payer, &id, &150_i128, &0_u64, &false, &false); - c.pay(&late_payer, &id, &150_i128, &0_u64, &false, &false); + c.pay(&early_payer, &id, &150_i128, &0_u64, &false, &false, &None); + c.pay(&late_payer, &id, &150_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&early_payer), 50); @@ -1390,7 +1390,7 @@ fn test_bonus_pool_zero_behaves_identically() { assert!(c.get_invoice_ext(&id).allowed_payers.is_none()); // Pay and verify it releases normally (bonus_pool=0 has no effect). - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 100); } @@ -1423,7 +1423,7 @@ fn test_group_partial_fund_blocks_release() { ids.push_back(id2); c.create_invoice_group(&ids, &false); - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); c.release(&id1); } @@ -1452,8 +1452,8 @@ fn test_group_all_funded_releases_both() { ids.push_back(id2); c.create_invoice_group(&ids, &false); - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); - c.pay(&payer, &id2, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer, &id2, &200_i128, &0_u64, &false, &false, &None); c.release(&id1); @@ -1479,7 +1479,7 @@ fn test_non_grouped_invoice_unaffected() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999); - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 300); @@ -1505,13 +1505,13 @@ fn test_nonce_increments_per_payer_per_invoice() { assert_eq!(c.get_nonce(&id, &payer), 0); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_nonce(&id, &payer), 1); - c.pay(&payer, &id, &200_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &1_u64, &false, &false, &None); assert_eq!(c.get_nonce(&id, &payer), 2); - c.pay(&payer, &id, &200_i128, &2_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &2_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -1530,10 +1530,10 @@ fn test_wrong_nonce_panics() { let id = make_invoice(&env, &c, &creator, &recipient, 600, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); // nonce should be 2 now — submitting 1 again must panic. - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); } #[test] @@ -1553,8 +1553,8 @@ fn test_nonce_is_independent_per_invoice() { let id2 = make_invoice(&env, &c, &creator, &r2, 100, &token_id, 9_999); // Both invoices start at nonce 0 for the same payer. - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); - c.pay(&payer, &id2, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer, &id2, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_nonce(&id1, &payer), 1); assert_eq!(c.get_nonce(&id2, &payer), 1); @@ -1687,7 +1687,7 @@ fn test_release_blocked_by_prerequisite() { ); // Fund B fully but don't touch A. - c.pay(&payer, &id_b, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id_b, &200_i128, &0_u64, &false, &false, &None); // release() on B should panic because A is still Pending. c.release(&id_b); @@ -1737,11 +1737,11 @@ fn test_release_succeeds_after_prerequisite_released() { ); // Release A (auto-releases on full funding). - c.pay(&payer, &id_a, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id_a, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id_a).status, InvoiceStatus::Released); // Fund B fully (stays pending because it has a prerequisite). - c.pay(&payer, &id_b, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id_b, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id_b).status, InvoiceStatus::Pending); // Now release B — prerequisite is satisfied. @@ -1763,7 +1763,7 @@ fn test_no_prerequisite_behaves_like_normal() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); // Auto-releases because no prerequisite. assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); @@ -1826,7 +1826,7 @@ fn test_tranches_partial_then_full_release() { ); // Fund fully — no auto-release for tranche invoices. - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); // At t=1_600 first tranche is unlocked, second is not. @@ -1893,7 +1893,7 @@ fn test_release_before_any_tranche_unlocked_panics() { }, ); - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); // t=1_000 < tranche timestamp 5_000 — should panic. c.release(&id); } @@ -1947,7 +1947,7 @@ fn test_release_tranche_full_vesting_schedule() { }, ); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); // Before the cliff, nothing has been released. assert_eq!(c.get_invoice(&id).released_bps, 0); @@ -2011,7 +2011,7 @@ fn test_release_tranche_before_time_panics() { }, ); - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); // t=2_000 < tranche timestamp 5_000 — should panic. env.ledger().set_timestamp(2_000); @@ -2058,7 +2058,7 @@ fn test_release_tranche_double_release_panics() { }, ); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(1_600); c.release_tranche(&id, &0_u32); @@ -2134,13 +2134,13 @@ fn test_reputation_increments_across_invoices() { assert_eq!(c.get_reputation(&payer), 0); - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_reputation(&payer), 1); - c.pay(&payer, &id2, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id2, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_reputation(&payer), 2); - c.pay(&payer, &id3, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id3, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_reputation(&payer), 3); } @@ -2161,10 +2161,10 @@ fn test_reputation_is_per_address() { let id = make_invoice(&env, &c, &creator, &recipient, 400, &token_id, 9_999); - c.pay(&payer_a, &id, &100_i128, &0_u64, &false, &false); - c.pay(&payer_a, &id, &100_i128, &1_u64, &false, &false); - c.pay(&payer_b, &id, &100_i128, &0_u64, &false, &false); - c.pay(&payer_b, &id, &100_i128, &1_u64, &false, &false); + c.pay(&payer_a, &id, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer_a, &id, &100_i128, &1_u64, &false, &false, &None); + c.pay(&payer_b, &id, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer_b, &id, &100_i128, &1_u64, &false, &false, &None); // payer_a paid twice, payer_b paid twice. assert_eq!(c.get_reputation(&payer_a), 2); @@ -2210,7 +2210,7 @@ fn test_reputation_release_updates_creator_and_payer() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); // After release (auto-triggered on full payment): creator reputation updated with invoices_released = 1 let creator_rep = c.get_rep(&creator); @@ -2254,10 +2254,10 @@ fn test_reputation_repeated_releases_accumulate() { env.ledger().set_timestamp(1_000); let id1 = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999); - c.pay(&payer, &id1, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &500_i128, &0_u64, &false, &false, &None); let id2 = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999); - c.pay(&payer, &id2, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id2, &500_i128, &0_u64, &false, &false, &None); let creator_rep = c.get_rep(&creator); assert_eq!(creator_rep.invoices_released, 2); @@ -2300,7 +2300,7 @@ fn test_reputation_min_payer_rep_gate_succeeds() { // Build 1 reputation score let id1 = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999); - c.pay(&payer, &id1, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &500_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_rep(&payer).paid_on_time, 1); // Create invoice requiring min_payer_rep = 1 @@ -2317,7 +2317,7 @@ fn test_reputation_min_payer_rep_gate_succeeds() { ); // Payment should succeed since payer has reputation 1 >= 1 - c.pay(&payer, &id2, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id2, &500_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_rep(&payer).paid_on_time, 2); } @@ -2348,7 +2348,7 @@ fn test_reputation_min_payer_rep_gate_rejects_low_reputation() { ); // low_rep_payer has 0 reputation, should fail with panic - c.pay(&low_rep_payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&low_rep_payer, &id, &500_i128, &0_u64, &false, &false, &None); } #[test] @@ -2364,7 +2364,7 @@ fn test_reputation_event_emission() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); // Check emitted events for rep_upd let events = env.events().all(); @@ -2719,7 +2719,7 @@ fn test_rollover_invoice_creates_new_with_carried_payments() { let id1 = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 2_000); // Partially fund the invoice. - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id1).funded, 100); assert_eq!(c.get_invoice(&id1).status, InvoiceStatus::Pending); @@ -2768,13 +2768,13 @@ fn test_rollover_invoice_then_complete_payment() { env.ledger().set_timestamp(1_000); let id1 = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 2_000); - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); let id2 = c.rollover_invoice(&creator, &id1, &5_000_u64); // Complete the payment on the new invoice. - c.pay(&payer, &id2, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id2, &200_i128, &0_u64, &false, &false, &None); // New invoice should be fully funded and released. assert_eq!(c.get_invoice(&id2).status, InvoiceStatus::Released); @@ -2798,7 +2798,7 @@ fn test_rollover_invoice_non_pending_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // Invoice is now Released, not Pending. env.ledger().set_timestamp(3_000); @@ -2819,7 +2819,7 @@ fn test_rollover_invoice_before_deadline_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 5_000); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // Still before deadline (3_000 < 5_000). env.ledger().set_timestamp(3_000); @@ -2841,7 +2841,7 @@ fn test_rollover_invoice_non_creator_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 2_000); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); c.rollover_invoice(&other, &id, &5_000_u64); @@ -2861,7 +2861,7 @@ fn test_rollover_invoice_past_deadline_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 2_000); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); // Try to set new deadline to 2_500, which is in the past. @@ -2881,7 +2881,7 @@ fn test_rollover_invoice_audit_entries() { env.ledger().set_timestamp(1_000); let id1 = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 2_000); - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); let id2 = c.rollover_invoice(&creator, &id1, &5_000_u64); @@ -2931,7 +2931,7 @@ fn test_rollover_invoice_preserves_recipients_and_amounts() { &2_000_u64, &default_options(&env), ); - c.pay(&payer, &id1, &150_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &150_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); let id2 = c.rollover_invoice(&creator, &id1, &5_000_u64); @@ -3103,7 +3103,7 @@ fn test_platform_fee_bps_deducted_on_release() { ); // 10% let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999); - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); // Recipient gets 500 - 10% = 450. @@ -3152,7 +3152,7 @@ fn test_platform_fee_bps_multi_recipient() { &9_999_u64, &default_options(&env), ); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); // 200 - 5% = 190, 300 - 5% = 285, 500 - 5% = 475 → sum = 950 @@ -3200,7 +3200,7 @@ fn test_early_bird_within_window_uses_discounted_fee() { ); // Paid immediately, well within the 100-ledger early-bird window. - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); // discount = 500 * (10% - 2%) = 40 let events = env.events().all(); @@ -3253,7 +3253,7 @@ fn test_early_bird_outside_window_uses_standard_fee() { // Advance past the 5-ledger early-bird window before paying. set_ledger(&env, 20, 1_100); - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); let events = env.events().all(); let has_early_bird_event = events.iter().any(|e| { @@ -3305,7 +3305,7 @@ fn test_early_bird_window_zero_disables_discount() { // Paid immediately — would be "within window" by timing alone, but the // window is disabled so the standard fee must apply. - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); let events = env.events().all(); let has_early_bird_event = events.iter().any(|e| { @@ -3409,7 +3409,7 @@ fn test_platform_fee_bps_with_tranches() { }, ); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); // First tranche: 500 unlocked. @@ -3477,7 +3477,7 @@ fn test_penalty_not_applied_before_penalty_deadline() { ); // Pay at t=1_000 which is before penalty_deadline. - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); // Recipient gets full 500, no penalty. assert_eq!(tk.balance(&recipient), 500); @@ -3530,7 +3530,7 @@ fn test_penalty_applied_after_penalty_deadline() { // Advance past penalty deadline. env.ledger().set_timestamp(3_000); - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); // Recipient gets 500 (normal) + 50 (penalty) = 550. assert_eq!(tk.balance(&recipient), 550); @@ -3589,7 +3589,7 @@ fn test_penalty_distributed_proportionally_multi_recipient() { // Pay after penalty deadline. env.ledger().set_timestamp(3_000); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); // Penalty = 1000 * 10% = 100 // Distribution: r1=10, r2=20, r3=70 @@ -3645,7 +3645,7 @@ fn test_penalty_bps_zero_no_penalty_even_after_deadline() { ); env.ledger().set_timestamp(3_000); - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); // Recipient gets full 500, no penalty. assert_eq!(tk.balance(&recipient), 500); @@ -3673,12 +3673,12 @@ fn test_min_funding_bps_zero_requires_full_funding() { let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999); // Partial fund (300 of 500) — release should fail. - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 300); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); // Fund the rest. - c.pay(&payer, &id, &200_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &1_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -3725,7 +3725,7 @@ fn test_min_funding_bps_blocks_early_release() { ); // Fund 500 of 1000 (50% — below 80% threshold). Release should panic. - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 500); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); } @@ -3774,7 +3774,7 @@ fn test_min_funding_bps_panics_below_threshold() { ); // Fund 700 of 1000 (70% — below 80%). Try to release — must panic. - c.pay(&payer, &id, &700_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &700_i128, &0_u64, &false, &false, &None); // Guarded (has min_funding_bps), so auto-release won't fire. c.release(&id); } @@ -3823,7 +3823,7 @@ fn test_min_funding_bps_allows_release_above_threshold() { ); // Fund 900 of 1000 (90% >= 80%). Release should succeed. - c.pay(&payer, &id, &900_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &900_i128, &0_u64, &false, &false, &None); // Guarded (has min_funding_bps), so we must manually release. c.release(&id); @@ -3847,8 +3847,8 @@ fn test_payment_proof_multiple_payments() { StellarAssetClient::new(&env, &token_id).mint(&payer, &1_000); let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); - c.pay(&payer, &id, &150_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer, &id, &150_i128, &1_u64, &false, &false, &None); let proof = c.generate_payment_proof(&id, &payer); assert_eq!(proof.invoice_id, id); @@ -3883,7 +3883,7 @@ fn test_payment_proof_hash_deterministic() { StellarAssetClient::new(&env, &token_id).mint(&payer, &500); let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); let proof1 = c.generate_payment_proof(&id, &payer); let proof2 = c.generate_payment_proof(&id, &payer); @@ -3932,7 +3932,7 @@ fn test_stage_release_3_stages() { ); // Fully fund the invoice. - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); // Invoice should still be Pending (guarded by release_stages). assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); @@ -3990,7 +3990,7 @@ fn test_stage_release_after_all_stages_panics() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); c.stage_release(&id, &creator); c.stage_release(&id, &creator); @@ -4031,7 +4031,7 @@ fn test_stage_release_non_creator_panics() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); // Non-creator should not be able to call stage_release. c.stage_release(&id, &other); @@ -4070,7 +4070,7 @@ fn test_stage_release_not_fully_funded_panics() { &opts, ); // Only partially fund. - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); // Should panic — not fully funded. c.stage_release(&id, &creator); @@ -4157,7 +4157,7 @@ fn test_oracle_none_behaviour_identical_to_current() { assert_eq!(c.get_invoice_ext(&id).base_amounts.get(0).unwrap(), 100); // Full payment of 100 should succeed (no oracle adjustment). - c.pay(&payer, &id, &100, &0, &false, &false); + c.pay(&payer, &id, &100, &0, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.funded, 100); @@ -4193,7 +4193,7 @@ fn test_oracle_price_1_000_000_produces_same_amounts_as_base() { assert_eq!(c.get_invoice_ext(&id).base_amounts.get(0).unwrap(), 100); // adjusted_total = 100 * 1_000_000 / 1_000_000 = 100 — identical to base - c.pay(&payer, &id, &100, &0, &false, &false); + c.pay(&payer, &id, &100, &0, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.funded, 100); assert_eq!(invoice.status, InvoiceStatus::Released); @@ -4228,13 +4228,13 @@ fn test_oracle_2x_price_doubles_required_amount() { // adjusted_total = 100 * 2_000_000 / 1_000_000 = 200 // Paying only 100 should NOT release (remaining = 200 - 100 = 100 still owed). - c.pay(&payer, &id, &100, &0, &false, &false); + c.pay(&payer, &id, &100, &0, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.funded, 100); assert_eq!(invoice.status, InvoiceStatus::Pending); // not yet fully funded // Paying the remaining 100 (total 200 = adjusted_total) should release. - c.pay(&payer, &id, &100, &1, &false, &false); + c.pay(&payer, &id, &100, &1, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.funded, 200); assert_eq!(invoice.status, InvoiceStatus::Released); @@ -4431,14 +4431,14 @@ fn test_oracle_price_changes_between_payments() { ); // At $0.10/XLM, $100 requires 1000 XLM. Pay 400 of it. - c.pay(&payer, &id, &400_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &400_i128, &0_u64, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.funded, 400); assert_eq!(invoice.status, InvoiceStatus::Pending); // Price rises to $0.20/XLM -> only 500 XLM needed in total; remaining = 100. oracle_client.set_rate(&20_000_000_i128); - c.pay(&payer, &id, &100_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &1_u64, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.funded, 500); assert_eq!(invoice.status, InvoiceStatus::Released); @@ -4478,7 +4478,7 @@ fn test_oracle_emits_price_fetched_event() { &default_options(&env), &opts2, ); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); let found = env .events() @@ -4523,7 +4523,7 @@ fn test_oracle_unavailable_panics() { &opts2, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } #[test] @@ -4562,7 +4562,7 @@ fn test_oracle_zero_rate_panics() { &opts2, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } // --------------------------------------------------------------------------- @@ -4635,7 +4635,7 @@ fn test_analytics_pay_and_release_increments_volume() { ); // Pay and auto-release (full payment) - c.pay(&payer, &id, &invoice_amount, &0_u64, &false, &false); + c.pay(&payer, &id, &invoice_amount, &0_u64, &false, &false, &None); let (total_invoices, total_volume, total_released, total_refunded) = c.get_stats(); assert_eq!(total_invoices, 1); @@ -4673,7 +4673,7 @@ fn test_analytics_partial_pay_then_release() { ); // Partial payment from payer1 - c.pay(&payer1, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer1, &id, &150_i128, &0_u64, &false, &false, &None); let (total_invoices, total_volume, total_released, total_refunded) = c.get_stats(); assert_eq!(total_invoices, 1); assert_eq!(total_volume, 0); @@ -4681,7 +4681,7 @@ fn test_analytics_partial_pay_then_release() { assert_eq!(total_refunded, 0); // Completion payment from payer2 triggers auto-release - c.pay(&payer2, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer2, &id, &150_i128, &0_u64, &false, &false, &None); let (total_invoices, total_volume, total_released, total_refunded) = c.get_stats(); assert_eq!(total_invoices, 1); assert_eq!(total_volume, 300); @@ -4715,7 +4715,7 @@ fn test_analytics_refund_increments_counter() { ); // Pay but don't complete - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); let (total_invoices, total_volume, total_released, total_refunded) = c.get_stats(); assert_eq!(total_invoices, 1); @@ -4753,7 +4753,7 @@ fn test_analytics_multiple_operations() { // Create and release invoice 1 let id1 = make_invoice(&env, &c, &creator, &recipient1, 100, &token_id, 9_999); - c.pay(&payer1, &id1, &100_i128, &0_u64, &false, &false); + c.pay(&payer1, &id1, &100_i128, &0_u64, &false, &false, &None); let (ti, tv, tr, tref) = c.get_stats(); assert_eq!(ti, 1); @@ -4763,7 +4763,7 @@ fn test_analytics_multiple_operations() { // Create invoice 2 and refund it let id2 = make_invoice(&env, &c, &creator, &recipient2, 200, &token_id, 2_000); - c.pay(&payer2, &id2, &50_i128, &0_u64, &false, &false); + c.pay(&payer2, &id2, &50_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); c.refund(&id2); @@ -4775,7 +4775,7 @@ fn test_analytics_multiple_operations() { // Create invoice 3 and release it let id3 = make_invoice(&env, &c, &creator, &recipient1, 300, &token_id, 9_999); - c.pay(&payer1, &id3, &300_i128, &0_u64, &false, &false); + c.pay(&payer1, &id3, &300_i128, &0_u64, &false, &false, &None); let (ti, tv, tr, tref) = c.get_stats(); assert_eq!(ti, 3); @@ -4801,7 +4801,7 @@ fn test_archive_released_invoice_still_readable() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); // Archive it. @@ -4844,7 +4844,7 @@ fn test_events_emitted_on_create_and_pay() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // Events were emitted (create + pay + release = at least 3). assert!(env.events().all().len() >= 3); @@ -5002,10 +5002,10 @@ fn test_compress_payments() { let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999); - c.pay(&payer1, &id, &50_i128, &0_u64, &false, &false); - c.pay(&payer2, &id, &100_i128, &0_u64, &false, &false); - c.pay(&payer1, &id, &75_i128, &1_u64, &false, &false); - c.pay(&payer2, &id, &25_i128, &1_u64, &false, &false); + c.pay(&payer1, &id, &50_i128, &0_u64, &false, &false, &None); + c.pay(&payer2, &id, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer1, &id, &75_i128, &1_u64, &false, &false, &None); + c.pay(&payer2, &id, &25_i128, &1_u64, &false, &false, &None); let inv_before = c.get_invoice(&id); assert_eq!(inv_before.payments.len(), 4); @@ -5197,7 +5197,7 @@ fn test_convert_to_stream_calls_stream_contract() { let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999, &opts); // Trigger release by fully paying the invoice. - c.pay(&payer, &id, &200_i128, &0, &false, &false); + c.pay(&payer, &id, &200_i128, &0, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.status, InvoiceStatus::Released); @@ -5221,7 +5221,7 @@ fn test_convert_to_stream_false_uses_direct_transfer() { // convert_to_stream defaults to false let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0, &false, &false); + c.pay(&payer, &id, &200_i128, &0, &false, &false, &None); let tk = token_client(&env, &token_id); // Direct transfer: recipient gets the tokens, not the stream contract. @@ -5331,7 +5331,7 @@ fn test_overflow_behavior_refund_accepts_excess() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.funded, 100); @@ -5372,7 +5372,7 @@ fn test_overflow_behavior_donate_sends_excess_to_treasury() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.funded, 100); @@ -5445,7 +5445,7 @@ fn test_notification_contract_receives_pay_release_and_refund() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert!(notifier.was_notified(&id, &symbol_short!("pay"))); assert!(notifier.was_notified(&id, &symbol_short!("release"))); @@ -5604,7 +5604,7 @@ fn test_pool_pay_invalid_invoice_reverts_all() { let id1 = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); // Pay id1 so it releases, making it no longer Pending. - c.pay(&payer, &id1, &100_i128, &0, &false, &false); + c.pay(&payer, &id1, &100_i128, &0, &false, &false, &None); let id2 = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); @@ -5772,9 +5772,9 @@ fn test_cooldown_blocks_same_payer_within_window() { invoice_options(&env, Some(60), None, None), ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); - c.pay(&other_payer, &id, &100_i128, &0_u64, &false, &false); - c.pay(&payer, &id, &100_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); + c.pay(&other_payer, &id, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer, &id, &100_i128, &1_u64, &false, &false, &None); } #[test] @@ -5796,7 +5796,7 @@ fn test_rate_limit_blocks_after_n_payments() { for _ in 0..3 { let payer = Address::generate(&env); stellar_asset.mint(&payer, &100); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } } @@ -5818,13 +5818,13 @@ fn test_rate_limit_window_resets() { for _ in 0..2 { let payer = Address::generate(&env); stellar_asset.mint(&payer, &100); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } env.ledger().set_timestamp(1_061); let payer = Address::generate(&env); stellar_asset.mint(&payer, &100); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } #[test] @@ -5853,8 +5853,8 @@ fn test_cooldown_and_rate_limit_independent() { assert_eq!(ext.max_payments_per_window, Some(1)); assert_eq!(ext.payment_window_secs, Some(60)); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); - c.pay(&other_payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); + c.pay(&other_payer, &id, &100_i128, &0_u64, &false, &false, &None); } // --------------------------------------------------------------------------- @@ -5909,7 +5909,7 @@ fn invariant_funded_never_exceeds_total() { let mut nonce: u64 = 0; for &payment in *payments { - c.pay(&payer, &id, &payment, &nonce, &false, &false); + c.pay(&payer, &id, &payment, &nonce, &false, &false, &None); nonce += 1; // Invariant: funded must never exceed total at any point. @@ -5943,7 +5943,7 @@ fn invariant_status_monotonic() { let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999_999); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); - c.pay(&payer, &id, &200, &0, &false, &false); + c.pay(&payer, &id, &200, &0, &false, &false, &None); let status = c.get_invoice(&id).status; assert_eq!(status, InvoiceStatus::Released); // Must not go back to Pending. @@ -5964,7 +5964,7 @@ fn invariant_status_monotonic() { let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 2_000); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); - c.pay(&payer, &id, &100, &0, &false, &false); + c.pay(&payer, &id, &100, &0, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); env.ledger().set_timestamp(3_000); @@ -6007,10 +6007,10 @@ fn invariant_status_monotonic() { let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999_999); for (nonce, amount) in [(0u64, 100i128), (1, 100)] { - c.pay(&payer, &id, &amount, &nonce, &false, &false); + c.pay(&payer, &id, &amount, &nonce, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); } - c.pay(&payer, &id, &100, &2, &false, &false); + c.pay(&payer, &id, &100, &2, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } } @@ -6056,7 +6056,7 @@ fn invariant_balance_matches_funded() { let last_idx = payments.len() - 1; let mut nonce: u64 = 0; for (i, &payment) in payments.iter().enumerate() { - c.pay(&payer, &id, &payment, &nonce, &false, &false); + c.pay(&payer, &id, &payment, &nonce, &false, &false, &None); nonce += 1; let inv = c.get_invoice(&id); @@ -6116,7 +6116,7 @@ fn test_pause_blocks_payment_with_reason() { assert!(c.get_invoice(&id).frozen); // This should panic with "invoice is frozen" - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } #[test] @@ -6143,7 +6143,7 @@ fn test_auto_resume_allows_payment_after_timestamp() { env.ledger().set_timestamp(2_000); // Payment should succeed because lazy auto-resume fires. - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); let invoice = c.get_invoice(&id); assert_eq!(invoice.status, InvoiceStatus::Released); @@ -6194,7 +6194,7 @@ fn test_admin_force_resume_overrides_creator_pause() { assert_eq!(ext.auto_resume_at, None); // Payment now succeeds. - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -6381,7 +6381,7 @@ fn test_clone_resets_payment_state() { let source_id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); // Partially fund the source invoice. - c.pay(&payer, &source_id, &50_i128, &0_u64, &false, &false); + c.pay(&payer, &source_id, &50_i128, &0_u64, &false, &false, &None); let source = c.get_invoice(&source_id); assert_eq!(source.funded, 50); @@ -6429,7 +6429,7 @@ fn test_sharded_payment_storage() { // Each payer pays 100 for i in 0..16 { let payer = payers.get(i as u32).unwrap(); - c.pay(&payer, &invoice_id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &invoice_id, &100_i128, &0_u64, &false, &false, &None); } // Verify invoice is partially funded (not auto-released) @@ -6494,7 +6494,7 @@ fn test_donate_on_failure_sends_to_creator() { // Invoice needs 500 tokens; donor contributes 300 with donate_on_failure=true. let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 2_000); - c.pay(&donor, &id, &300_i128, &0_u64, &false, &true); + c.pay(&donor, &id, &300_i128, &0_u64, &false, &true, &None); env.ledger().set_timestamp(3_000); c.refund(&id); @@ -6522,8 +6522,8 @@ fn test_donate_on_failure_mixed_payers() { // Invoice needs 500; partially funded by a donor and a normal payer. let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 2_000); - c.pay(&donor, &id, &100_i128, &0_u64, &false, &true); // donate - c.pay(&refundee, &id, &100_i128, &0_u64, &false, &false); // normal + c.pay(&donor, &id, &100_i128, &0_u64, &false, &true, &None); // donate + c.pay(&refundee, &id, &100_i128, &0_u64, &false, &false, &None); // normal env.ledger().set_timestamp(3_000); c.refund(&id); @@ -6565,8 +6565,8 @@ fn test_majority_group_releases_when_majority_funded() { c.create_invoice_group(&ids, &true); // Fund 2 out of 3 (>50%) - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); - c.pay(&payer, &id2, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer, &id2, &100_i128, &0_u64, &false, &false, &None); // id1 is fully funded and majority condition is met — release should succeed. c.release(&id1); @@ -6600,7 +6600,7 @@ fn test_majority_group_blocks_when_minority_funded() { c.create_invoice_group(&ids, &true); // Only 1 out of 3 funded — not a majority. - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); c.release(&id1); // should panic } @@ -6627,7 +6627,7 @@ fn test_all_or_nothing_group_still_requires_all_funded() { c.create_invoice_group(&ids, &false); // AllOrNothing // Only id1 funded — id2 is not. - c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &100_i128, &0_u64, &false, &false, &None); c.release(&id1); // should panic } @@ -6682,7 +6682,7 @@ fn test_platform_volume_milestone_emitted() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // total_volume = 100, milestone 1 crossed assert!( @@ -6711,7 +6711,7 @@ fn test_platform_volume_milestone_not_emitted_below_threshold() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // total_volume = 100, threshold = 500 → no milestone yet assert!( @@ -6742,7 +6742,7 @@ fn test_platform_volume_milestone_fires_multiple_times() { let cr = Address::generate(&env); let rc = Address::generate(&env); let id = make_invoice(&env, &c, &cr, &rc, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert!( has_platform_milestone_event(&env), "expected platform milestone {} to fire", @@ -6771,7 +6771,7 @@ fn test_creator_volume_milestone_emitted() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert!( has_creator_milestone_event(&env), @@ -6801,7 +6801,7 @@ fn test_milestone_disabled_when_threshold_zero() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert!( !has_platform_milestone_event(&env), @@ -6984,7 +6984,7 @@ fn test_circuit_breaker_blocks_pay() { let reason = String::from_str(&env, "vulnerability discovered"); c.activate_circuit_breaker(&admin, &reason); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); } #[test] @@ -7151,7 +7151,7 @@ fn test_state_changed_event_emitted_on_release() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert!( has_state_changed_event(&env), @@ -7178,7 +7178,7 @@ fn test_307_xlm_invoice_payment() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 100); @@ -7197,7 +7197,7 @@ fn test_state_changed_event_emitted_on_refund() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 2_000); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); c.refund(&id); @@ -7222,7 +7222,7 @@ fn test_307_usdc_invoice_payment() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999); - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 500); @@ -7380,7 +7380,7 @@ fn test_fee_waiver_zeroes_platform_fee_at_release() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!( tk.balance(&recipient), @@ -7421,7 +7421,7 @@ fn test_state_changed_full_lifecycle_release() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); // Exactly one state_changed event (Pending → Released). assert_eq!(state_changed_count(&env), 1); @@ -7447,7 +7447,7 @@ fn test_payment_at_exact_deadline_succeeds() { // Pay just before deadline — should succeed. env.ledger().set_timestamp(9_998); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -7466,7 +7466,7 @@ fn test_307_wrong_token_rejected() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999); - c.pay(&payer, &id, &500_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &500_i128, &0_u64, &false, &false, &None); } // --------------------------------------------------------------------------- @@ -7495,7 +7495,7 @@ fn test_308_claim_refund_after_expiry() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // With fee waiver the recipient should receive the full 100 (no 10% deducted). assert_eq!( @@ -7526,7 +7526,7 @@ fn test_no_fee_waiver_deducts_platform_fee_normally() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // 10% fee deducted → recipient gets 90 assert_eq!( @@ -7554,7 +7554,7 @@ fn test_payment_one_second_after_deadline_fails() { // One second after deadline — should panic env.ledger().set_timestamp(deadline + 1); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } // --------------------------------------------------------------------------- @@ -7646,7 +7646,7 @@ fn test_pay_after_deadline_rejected() { let deadline = 5_000_u64; let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, deadline); env.ledger().set_timestamp(deadline + 1); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); } #[test] @@ -7673,7 +7673,7 @@ fn test_refund_available_after_deadline() { &default_options(&env), ); - c.pay(&payer, &id, &200_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &0_u64, &false, &false, &None); assert_eq!(tk.balance(&payer), 0); // Advance past deadline @@ -7699,7 +7699,7 @@ fn test_308_claim_refund_idempotent() { let deadline = 5_000_u64; let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, deadline); - c.pay(&payer, &id, &50_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &0_u64, &false, &false, &None); // After deadline refund should succeed. env.ledger().set_timestamp(deadline + 1); @@ -7721,7 +7721,7 @@ fn test_refund_before_deadline_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &50_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &0_u64, &false, &false, &None); // Deadline not passed — refund should panic c.refund(&id); @@ -7946,8 +7946,8 @@ fn test_308_partial_payments_refunded_correctly() { &default_options(&env), ); - c.pay(&payer1, &id, &100_i128, &0_u64, &false, &false); - c.pay(&payer2, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer1, &id, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer2, &id, &150_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); c.claim_refund(&payer1, &id); @@ -7984,7 +7984,7 @@ fn test_scheduled_release_blocked_before_timestamp() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // Before scheduled time — should panic env.ledger().set_timestamp(release_at - 1); @@ -8005,7 +8005,7 @@ fn test_308_claim_refund_before_deadline_panics() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &50_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &0_u64, &false, &false, &None); // Deadline hasn't passed — claim_refund should panic c.claim_refund(&payer, &id); @@ -8098,7 +8098,7 @@ fn test_fee_waiver_exempts_from_fees() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!( tk.balance(&recipient), @@ -8261,7 +8261,7 @@ fn test_payment_window_unset_no_restriction() { let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999); assert_eq!(c.get_payment_window(&id), (None, None)); - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -8288,7 +8288,7 @@ fn test_payment_before_open_fails() { None, ); - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); } #[test] @@ -8315,7 +8315,7 @@ fn test_payment_within_window_succeeds() { assert_eq!(c.get_payment_window(&id), (Some(1_000), Some(5_000))); env.ledger().set_timestamp(2_000); - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -8343,7 +8343,7 @@ fn test_payment_after_close_fails() { ); env.ledger().set_timestamp(3_000); - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); } #[test] @@ -8370,7 +8370,7 @@ fn test_payment_only_open_set_no_close_restriction() { // Far past the open timestamp, with no close bound to trip. env.ledger().set_timestamp(9_000); - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -8397,7 +8397,7 @@ fn test_payment_only_close_set_no_open_restriction() { ); // Immediately payable since there is no open bound. - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -8594,7 +8594,7 @@ fn test_multisig_release_requires_threshold() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); c.sign_release(&id, &signer1); // Only 1 of 2 — still pending assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); @@ -8633,7 +8633,7 @@ fn test_multisig_release_panics_below_threshold() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); c.sign_release(&id, &signer1); // only 1 of 2 c.release(&id); // should panic: not enough co-signer approvals } @@ -8668,7 +8668,7 @@ fn cosigner_invoice( opts.cosigner_threshold = Some(threshold); let id = c.create_invoice(&creator, &recipients, &amounts, token_id, &9_999_u64, &opts); - c.pay(payer, &id, &amount, &0_u64, &false, &false); + c.pay(payer, &id, &amount, &0_u64, &false, &false, &None); id } @@ -8786,7 +8786,7 @@ fn test_309_allowlist_restricts_payers() { &9_999_u64, &opts, ); - c.pay(&allowed, &id, &100_i128, &0_u64, &false, &false); + c.pay(&allowed, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -8838,7 +8838,7 @@ fn test_309_blocked_payer_rejected() { &9_999_u64, &opts, ); - c.pay(&blocked, &id, &100_i128, &0_u64, &false, &false); // should panic + c.pay(&blocked, &id, &100_i128, &0_u64, &false, &false, &None); // should panic } #[test] @@ -8855,7 +8855,7 @@ fn test_creator_stats_on_payment() { let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); let stats = c.get_creator_stats(&creator); assert_eq!( stats.total_raised, 100, @@ -8875,7 +8875,7 @@ fn test_creator_stats_on_release() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); let stats = c.get_creator_stats(&creator); assert_eq!( @@ -8916,7 +8916,7 @@ fn test_multisig_release_succeeds_at_threshold() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); c.sign_release(&id, &signer1); c.sign_release(&id, &signer2); c.release(&id); @@ -8960,7 +8960,7 @@ fn test_309_add_allowed_payer_initializes_list() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); // Both signers sign — release should succeed. c.sign_release(&id, &signer1); @@ -9003,7 +9003,7 @@ fn test_multisig_non_signer_cannot_sign() { &9_999_u64, &opts, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); c.sign_release(&id, &imposter); // not in co_signers — should panic } @@ -9020,7 +9020,7 @@ fn test_309_remove_allowed_payer_emits_event() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 250, &token_id, 9_999); - c.pay(&payer, &id, &250_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &250_i128, &0_u64, &false, &false, &None); let stats = c.get_creator_stats(&creator); assert_eq!( @@ -9421,7 +9421,7 @@ fn test_circuit_breaker_prevents_refund() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 2_000); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); env.ledger().set_timestamp(3_000); @@ -9803,11 +9803,11 @@ fn test_twafr_single_and_multiple_payments() { ); set_ledger(&env, 20, 1_100); - c.pay(&payer, &id, &50_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_twafr(&id), 5); set_ledger(&env, 30, 1_200); - c.pay(&payer, &id, &50_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &1_u64, &false, &false, &None); assert!(c.get_twafr(&id) > 5); } @@ -9888,6 +9888,180 @@ fn test_commit_reveal_expired_rejected() { c.reveal_payment(&payer, &id, &100_i128, &salt, &0_u64, &false, &false); } +// --------------------------------------------------------------------------- +// Confidential payment settlement — Pedersen commitments (BLS12-381 G1). +// --------------------------------------------------------------------------- + +fn init_confidential(env: &Env, c: &SplitContractClient, admin: &Address, token_id: &Address) { + let treasury = Address::generate(env); + c.initialize( + admin, &0_i128, &treasury, token_id, &0_u32, &None, &0_u32, &0_u32, &0_u64, + ); +} + +#[test] +fn test_confidential_pay_stores_commitment_without_moving_funds() { + let (env, contract_id, token_id) = setup(); + let c = client(&env, &contract_id); + let tk = token_client(&env, &token_id); + let admin = Address::generate(&env); + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + let payer = Address::generate(&env); + let blinding = BytesN::from_array(&env, &[11u8; 32]); + + StellarAssetClient::new(&env, &token_id).mint(&payer, &500); + set_ledger(&env, 10, 1_000); + init_confidential(&env, &c, &admin, &token_id); + let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); + + let digest = pedersen_commitment_digest(&env, 100, &blinding); + c.pay(&payer, &id, &0_i128, &0_u64, &false, &false, &Some(digest)); + + // Committing hides the amount: no tokens moved, invoice not funded yet. + assert_eq!(tk.balance(&payer), 500); + assert_eq!(c.get_invoice(&id).funded, 0); + assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); +} + +#[test] +fn test_confidential_commit_then_reveal_settles_and_releases() { + let (env, contract_id, token_id) = setup(); + let c = client(&env, &contract_id); + let tk = token_client(&env, &token_id); + let admin = Address::generate(&env); + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + let payer = Address::generate(&env); + let blinding = BytesN::from_array(&env, &[11u8; 32]); + + StellarAssetClient::new(&env, &token_id).mint(&payer, &500); + set_ledger(&env, 10, 1_000); + init_confidential(&env, &c, &admin, &token_id); + let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); + + let digest = pedersen_commitment_digest(&env, 100, &blinding); + c.pay(&payer, &id, &0_i128, &0_u64, &false, &false, &Some(digest)); + + c.reveal_confidential_payment(&id, &payer, &100_i128, &blinding); + + // Only now does the real amount become visible on-chain. + assert_eq!(tk.balance(&payer), 400); + assert_eq!(tk.balance(&recipient), 100); + let invoice = c.get_invoice(&id); + assert_eq!(invoice.funded, 100); + assert_eq!(invoice.status, InvoiceStatus::Released); + + // The settlement event fired, and (by construction — the event function + // takes no amount parameter) it carries no amount, only (payer, event_seq). + let events = env.events().all(); + let has_conf_rev_event = events.iter().any(|e| { + let topics = e.1; + if topics.len() >= 2 { + if let Ok(sym) = Symbol::try_from_val(&env, &topics.get(1).unwrap()) { + return sym == Symbol::new(&env, "conf_rev"); + } + } + false + }); + assert!( + has_conf_rev_event, + "ConfidentialPaymentRevealed event should be emitted" + ); +} + +#[test] +#[should_panic(expected = "ConfidentialCommitmentMismatch")] +fn test_confidential_reveal_tampered_value_rejected() { + let (env, contract_id, token_id) = setup(); + let c = client(&env, &contract_id); + let admin = Address::generate(&env); + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + let payer = Address::generate(&env); + let blinding = BytesN::from_array(&env, &[11u8; 32]); + + StellarAssetClient::new(&env, &token_id).mint(&payer, &500); + set_ledger(&env, 10, 1_000); + init_confidential(&env, &c, &admin, &token_id); + let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); + + let digest = pedersen_commitment_digest(&env, 100, &blinding); + c.pay(&payer, &id, &0_i128, &0_u64, &false, &false, &Some(digest)); + + // Correct blinding, wrong value: recomputed commitment must not match. + c.reveal_confidential_payment(&id, &payer, &101_i128, &blinding); +} + +#[test] +#[should_panic(expected = "ConfidentialCommitmentMismatch")] +fn test_confidential_reveal_tampered_blinding_rejected() { + let (env, contract_id, token_id) = setup(); + let c = client(&env, &contract_id); + let admin = Address::generate(&env); + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + let payer = Address::generate(&env); + let blinding = BytesN::from_array(&env, &[11u8; 32]); + let wrong_blinding = BytesN::from_array(&env, &[12u8; 32]); + + StellarAssetClient::new(&env, &token_id).mint(&payer, &500); + set_ledger(&env, 10, 1_000); + init_confidential(&env, &c, &admin, &token_id); + let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); + + let digest = pedersen_commitment_digest(&env, 100, &blinding); + c.pay(&payer, &id, &0_i128, &0_u64, &false, &false, &Some(digest)); + + // Correct value, wrong blinding: recomputed commitment must not match. + c.reveal_confidential_payment(&id, &payer, &100_i128, &wrong_blinding); +} + +#[test] +#[should_panic(expected = "NoConfidentialCommitment")] +fn test_confidential_double_reveal_rejected() { + let (env, contract_id, token_id) = setup(); + let c = client(&env, &contract_id); + let admin = Address::generate(&env); + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + let payer = Address::generate(&env); + let blinding = BytesN::from_array(&env, &[11u8; 32]); + + StellarAssetClient::new(&env, &token_id).mint(&payer, &500); + set_ledger(&env, 10, 1_000); + init_confidential(&env, &c, &admin, &token_id); + let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); + + let digest = pedersen_commitment_digest(&env, 100, &blinding); + c.pay(&payer, &id, &0_i128, &0_u64, &false, &false, &Some(digest)); + + c.reveal_confidential_payment(&id, &payer, &100_i128, &blinding); + // The commitment was removed on first reveal; a second attempt has nothing to open. + c.reveal_confidential_payment(&id, &payer, &100_i128, &blinding); +} + +#[test] +#[should_panic(expected = "ConfidentialCommitmentExists")] +fn test_confidential_double_commit_rejected() { + let (env, contract_id, token_id) = setup(); + let c = client(&env, &contract_id); + let admin = Address::generate(&env); + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + let payer = Address::generate(&env); + + StellarAssetClient::new(&env, &token_id).mint(&payer, &500); + set_ledger(&env, 10, 1_000); + init_confidential(&env, &c, &admin, &token_id); + let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); + + let d1 = pedersen_commitment_digest(&env, 100, &BytesN::from_array(&env, &[1u8; 32])); + let d2 = pedersen_commitment_digest(&env, 100, &BytesN::from_array(&env, &[2u8; 32])); + c.pay(&payer, &id, &0_i128, &0_u64, &false, &false, &Some(d1)); + c.pay(&payer, &id, &0_i128, &0_u64, &false, &false, &Some(d2)); +} + #[test] fn test_recipient_cap_surplus_and_claim() { let (env, contract_id, token_id) = setup(); @@ -9912,7 +10086,7 @@ fn test_recipient_cap_surplus_and_claim() { &options, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(tk.balance(&recipient), 60); c.claim_surplus(&id, &payer); assert_eq!(tk.balance(&payer), 140); @@ -9947,11 +10121,11 @@ fn test_milestones_auto_release() { &options, ); - c.pay(&payer, &id, &50_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &0_u64, &false, &false, &None); assert_eq!(tk.balance(&recipient), 50); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); - c.pay(&payer, &id, &50_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &50_i128, &1_u64, &false, &false, &None); assert_eq!(tk.balance(&recipient), 100); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } @@ -9982,7 +10156,7 @@ fn test_trusted_caller_exempt_from_platform_fee() { let id = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999); c.add_trusted_caller(&admin, &trusted_payer); - c.pay(&trusted_payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&trusted_payer, &id, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 1_000, "no platform fee deducted"); @@ -10097,7 +10271,7 @@ fn test_set_overfunding_policy_rejected_after_funding() { set_ledger(&env, 10, 1_000); let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &id, &40_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &40_i128, &0_u64, &false, &false, &None); c.set_overfunding_policy(&creator, &id, &types::OverfundingPolicy::AcceptAll); } @@ -10124,7 +10298,7 @@ fn test_overfunding_cap_exact_payment() { let id = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 900); @@ -10149,7 +10323,7 @@ fn test_remove_trusted_caller_restores_platform_fee() { types::OverfundingPolicy::Cap, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 100); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); @@ -10179,7 +10353,7 @@ fn test_overfunding_cap_under_payment() { types::OverfundingPolicy::Cap, ); - c.pay(&payer, &id, &60_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &60_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 60); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); @@ -10209,7 +10383,7 @@ fn test_overfunding_cap_over_payment_rejected() { types::OverfundingPolicy::Cap, ); - c.pay(&payer, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &150_i128, &0_u64, &false, &false, &None); } // --- AcceptAll ------------------------------------------------------------ @@ -10236,13 +10410,13 @@ fn test_overfunding_accept_all_exact_payment() { c.add_trusted_caller(&admin, &payer); let id1 = make_invoice(&env, &c, &creator, &recipient1, 1_000, &token_id, 9_999); - c.pay(&payer, &id1, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id1, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(tk.balance(&recipient1), 1_000, "exempt while trusted"); c.remove_trusted_caller(&admin, &payer); let id2 = make_invoice(&env, &c, &creator, &recipient2, 1_000, &token_id, 9_999); // Nonce is scoped per (invoice_id, payer), so this fresh invoice starts back at 0. - c.pay(&payer, &id2, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id2, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(tk.balance(&recipient2), 900, "fee restored after removal"); assert_eq!(tk.balance(&treasury), 100); } @@ -10269,7 +10443,7 @@ fn test_trusting_contract_self_does_not_waive_other_payers_fee() { types::OverfundingPolicy::AcceptAll, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 100); assert_eq!(tk.balance(&recipient), 100); @@ -10297,7 +10471,7 @@ fn test_overfunding_accept_all_under_payment() { env.ledger().set_timestamp(1_000); let id = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999); - c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &1_000_i128, &0_u64, &false, &false, &None); assert_eq!(tk.balance(&recipient), 900, "fee still charged for untrusted payer"); assert_eq!(tk.balance(&treasury), 100); @@ -10342,7 +10516,7 @@ fn test_get_invoice_stats_cumulative_contributed_survives_withdrawal() { types::OverfundingPolicy::AcceptAll, ); - c.pay(&payer, &id, &60_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &60_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 60); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Pending); @@ -10371,7 +10545,7 @@ fn test_overfunding_accept_all_over_payment_keeps_surplus() { types::OverfundingPolicy::AcceptAll, ); - c.pay(&payer, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &150_i128, &0_u64, &false, &false, &None); // funded is allowed past the 100 target, and the whole 150 reaches the // sole recipient at release — nothing is returned to the payer. @@ -10417,7 +10591,7 @@ fn test_overfunding_accept_all_releases_surplus_pro_rata() { &options, ); - c.pay(&payer, &id, &400_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &400_i128, &0_u64, &false, &false, &None); let stats = c.get_invoice_stats(&id); assert_eq!(stats.funded, 400); assert_eq!(stats.cumulative_contributed, 400); @@ -10430,7 +10604,7 @@ fn test_overfunding_accept_all_releases_surplus_pro_rata() { "cumulative_contributed must never decrease" ); - c.pay(&payer, &id, &400_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &400_i128, &1_u64, &false, &false, &None); let stats_final = c.get_invoice_stats(&id); assert_eq!(stats_final.funded, 400); assert_eq!(stats_final.cumulative_contributed, 800); @@ -10638,7 +10812,7 @@ fn test_sweep_unclaimed_funds_uses_funding_token_not_recipient_token() { assert_eq!(swept, 500); assert_eq!(tk.balance(&treasury), 500); // 900 against a 600 target: each recipient receives 1.5x their share. - c.pay(&payer, &id, &900_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &900_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 900); assert_eq!(tk.balance(&r1), 150); @@ -10778,7 +10952,7 @@ fn test_funding_checkpoint_single_hit() { types::OverfundingPolicy::ReturnSurplus, ); - c.pay(&payer, &id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 100); assert_eq!(tk.balance(&recipient), 100); @@ -10807,7 +10981,7 @@ fn test_overfunding_return_surplus_under_payment() { types::OverfundingPolicy::ReturnSurplus, ); - c.pay(&payer, &id, &60_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &60_i128, &0_u64, &false, &false, &None); // Nothing to return — the payment fits entirely under the target. assert_eq!(c.get_invoice(&id).funded, 60); @@ -10830,7 +11004,7 @@ fn test_overfunding_return_surplus_over_payment_refunds_remainder() { let id = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999); assert_eq!(c.get_last_funding_checkpoint(&id), 0); - c.pay(&payer, &id, &250_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &250_i128, &0_u64, &false, &false, &None); let events = funding_checkpoint_events(&env); assert_eq!(events.len(), 1); @@ -10863,7 +11037,7 @@ fn test_funding_checkpoint_multiple_in_one_payment() { types::OverfundingPolicy::ReturnSurplus, ); - c.pay(&payer, &id, &150_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &150_i128, &0_u64, &false, &false, &None); // Only 100 is credited; the 50 surplus goes straight back to the payer. assert_eq!(c.get_invoice(&id).funded, 100); @@ -10888,7 +11062,7 @@ fn test_overfunding_return_surplus_partial_then_over_payment() { c.set_funding_checkpoints(&admin, &checkpoints); let id = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999); - c.pay(&payer, &id, &800_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &800_i128, &0_u64, &false, &false, &None); let events = funding_checkpoint_events(&env); assert_eq!(events.len(), 4); @@ -10925,11 +11099,11 @@ fn test_funding_checkpoint_not_reemitted_on_subsequent_payments() { types::OverfundingPolicy::ReturnSurplus, ); - c.pay(&payer, &id, &70_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &70_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 70); // Second payment of 80 has only 30 of headroom; 50 is returned. - c.pay(&payer, &id, &80_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &80_i128, &1_u64, &false, &false, &None); assert_eq!(c.get_invoice(&id).funded, 100); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); @@ -10944,10 +11118,10 @@ fn test_funding_checkpoint_not_reemitted_on_subsequent_payments() { c.set_funding_checkpoints(&admin, &checkpoints); let id = make_invoice(&env, &c, &creator, &recipient, 1_000, &token_id, 9_999); - c.pay(&payer, &id, &300_i128, &0_u64, &false, &false); + c.pay(&payer, &id, &300_i128, &0_u64, &false, &false, &None); assert_eq!(funding_checkpoint_events(&env).len(), 1); - c.pay(&payer, &id, &200_i128, &1_u64, &false, &false); + c.pay(&payer, &id, &200_i128, &1_u64, &false, &false, &None); let events = funding_checkpoint_events(&env); assert_eq!(events.len(), 2); assert_eq!(events.get(0).unwrap().threshold_bps, 2_500); @@ -10995,11 +11169,11 @@ fn test_linear_dependency_chain_blocks_payment() { assert_eq!(invoice_b.prerequisite_id, Some(invoice_a_id)); // Pay invoice A to release it - c.pay(&payer, &invoice_a_id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &invoice_a_id, &100_i128, &0_u64, &false, &false, &None); assert_eq!(c.get_invoice(&invoice_a_id).status, InvoiceStatus::Released); // Now paying invoice B should succeed - c.pay(&payer, &invoice_b_id, &100_i128, &1_u64, &false, &false); + c.pay(&payer, &invoice_b_id, &100_i128, &1_u64, &false, &false, &None); assert_eq!(c.get_invoice(&invoice_b_id).status, InvoiceStatus::Released); } @@ -11032,9 +11206,9 @@ fn test_three_level_dependency_chain() { assert_eq!(c.get_invoice(&invoice_b).prerequisite_id, Some(invoice_a)); assert_eq!(c.get_invoice(&invoice_c).prerequisite_id, Some(invoice_b)); - c.pay(&payer, &invoice_a, &100_i128, &0_u64, &false, &false); - c.pay(&payer, &invoice_b, &100_i128, &1_u64, &false, &false); - c.pay(&payer, &invoice_c, &100_i128, &2_u64, &false, &false); + c.pay(&payer, &invoice_a, &100_i128, &0_u64, &false, &false, &None); + c.pay(&payer, &invoice_b, &100_i128, &1_u64, &false, &false, &None); + c.pay(&payer, &invoice_c, &100_i128, &2_u64, &false, &false, &None); assert_eq!(c.get_invoice(&invoice_a).status, InvoiceStatus::Released); assert_eq!(c.get_invoice(&invoice_b).status, InvoiceStatus::Released); @@ -11109,10 +11283,10 @@ fn test_integrity_checksum_updates_on_payment() { let invoice_id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); // Make first payment - c.pay(&payer1, &invoice_id, &100_i128, &0_u64, &false, &false); + c.pay(&payer1, &invoice_id, &100_i128, &0_u64, &false, &false, &None); // Make second payment - this should update the checksum - c.pay(&payer2, &invoice_id, &100_i128, &1_u64, &false, &false); + c.pay(&payer2, &invoice_id, &100_i128, &1_u64, &false, &false, &None); let invoice = c.get_invoice(&invoice_id); assert_eq!(invoice.funded, 200); @@ -11133,7 +11307,7 @@ fn test_verify_integrity_with_correct_history() { let invoice_id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); - c.pay(&payer, &invoice_id, &100_i128, &0_u64, &false, &false); + c.pay(&payer, &invoice_id, &100_i128, &0_u64, &false, &false, &None); let invoice = c.get_invoice(&invoice_id); assert_eq!(invoice.payments.len(), 1);