From 2d7f0c88db20d54f141db07dec1805ab948bcd2d Mon Sep 17 00:00:00 2001 From: GenesisPray <125413943+GenesisPray@users.noreply.github.com> Date: Fri, 28 Aug 2026 09:28:30 +0000 Subject: [PATCH] Enrich refund/expiry events and fix funds_unlocked gap - invoice_refunded now carries the total amount returned to all payers instead of an empty payload, so indexers can reconcile refunds without cross-referencing individual payer_refunded events. Updated all four emission sites (arbiter resolve-refund, auto-resolve refund, deadline refund, dispute-refund) to pass the summed total. - invoice_expired now includes the invoice creator's address alongside (deadline, funded), letting indexers attribute expired invoices to creators without a follow-up get_invoice call. - release_to_recipient enforced the InvoiceExt3::release_delay_ledgers time-lock but never emitted funds_unlocked when the lock cleared, unlike release_invoice. Added the missing emission so the event fires consistently across both release paths at the exact unlock ledger. No InvoiceExt2::creator_fee_bps field or mutation path exists in this contract (fees are governed by a global platform_fee_bps plus a boolean per-creator waiver list, which already emits fee_waiver_granted/revoked), so no changes were needed there. Added test coverage: invoice_expired creator payload, invoice_refunded total payload, and funds_unlocked firing at the exact unlock ledger via release_to_recipient (plus a should_panic guard for the still-locked case). --- contracts/split/src/events.rs | 12 +-- contracts/split/src/lib.rs | 21 +++- contracts/split/src/test.rs | 180 +++++++++++++++++++++++++++++++++- 3 files changed, 201 insertions(+), 12 deletions(-) diff --git a/contracts/split/src/events.rs b/contracts/split/src/events.rs index 025cdcd..38ee0e8 100644 --- a/contracts/split/src/events.rs +++ b/contracts/split/src/events.rs @@ -76,15 +76,15 @@ pub fn invoice_released(env: &Env, invoice_id: u64, recipients: &Vec
) { /// Emitted when an invoice is refunded after deadline. /// Topics: (split, refunded, invoice_id) -/// Data: () -pub fn invoice_refunded(env: &Env, invoice_id: u64) { +/// Data: total_amount (sum returned to all payers, excluding any creator payout) +pub fn invoice_refunded(env: &Env, invoice_id: u64, total_amount: i128) { env.events().publish( ( symbol_short!("split"), symbol_short!("refunded"), invoice_id, ), - (), + total_amount, ); } @@ -104,15 +104,15 @@ pub fn condition_verified(env: &Env, invoice_id: u64, preimage_hash: &BytesN<32> /// Emitted when an invoice expires. /// Topics: (split, expired, invoice_id) -/// Data: (deadline, funded) -pub fn invoice_expired(env: &Env, invoice_id: u64, deadline: u64, funded: i128) { +/// Data: (deadline, funded, creator) +pub fn invoice_expired(env: &Env, invoice_id: u64, deadline: u64, funded: i128, creator: &Address) { env.events().publish( ( symbol_short!("split"), symbol_short!("expired"), invoice_id, ), - (deadline, funded), + (deadline, funded, creator.clone()), ); } diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index be40701..6ea3b14 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -2419,7 +2419,7 @@ impl SplitContract { invoice.completion_time = Some(env.ledger().timestamp()); save_invoice(&env, invoice_id, &invoice); append_audit_entry(&env, invoice_id, symbol_short!("resolve"), &arbiter); - events::invoice_refunded(&env, invoice_id); + events::invoice_refunded(&env, invoice_id, total_refunded_amount); events::invoice_state_changed( &env, invoice_id, @@ -6364,6 +6364,9 @@ impl SplitContract { { let unlock_at = funded_at.saturating_add(delay_ledgers); assert!(env.ledger().sequence() >= unlock_at, "FundsLockedUntil"); + // Issue #327: emit the same unlock event as `release_invoice` so + // indexers observe funds_unlocked regardless of which release path is used. + events::funds_unlocked(&env, invoice_id, unlock_at); } } @@ -8035,7 +8038,7 @@ impl SplitContract { save_invoice(&env, invoice_id, &invoice); let actor = env.current_contract_address(); append_audit_entry(&env, invoice_id, symbol_short!("auto_ref"), &actor); - events::invoice_refunded(&env, invoice_id); + events::invoice_refunded(&env, invoice_id, total_refunded_amount); events::invoice_state_changed( &env, invoice_id, @@ -8128,7 +8131,13 @@ impl SplitContract { invoice.status = InvoiceStatus::Expired; save_invoice(&env, invoice_id, &invoice); - events::invoice_expired(&env, invoice_id, invoice.deadline, invoice.funded); + events::invoice_expired( + &env, + invoice_id, + invoice.deadline, + invoice.funded, + &invoice.creator, + ); append_audit_entry( &env, invoice_id, @@ -8220,7 +8229,7 @@ impl SplitContract { save_invoice(&env, invoice_id, &invoice); let actor = env.current_contract_address(); append_audit_entry(&env, invoice_id, symbol_short!("refund"), &actor); - events::invoice_refunded(&env, invoice_id); + events::invoice_refunded(&env, invoice_id, total_refunded_amount); events::invoice_state_changed( &env, invoice_id, @@ -11104,8 +11113,10 @@ impl SplitContract { let prev = totals.get(payment.payer.clone()).unwrap_or(0); totals.set(payment.payer.clone(), prev + payment.amount); } + let mut total_refunded_amount: i128 = 0; for (payer, amount) in totals.iter() { token_client.transfer(&env.current_contract_address(), &payer, &amount); + total_refunded_amount += amount; events::payer_refunded(&env, invoice_id, &payer, amount); } @@ -11114,7 +11125,7 @@ impl SplitContract { invoice.completion_time = Some(env.ledger().timestamp()); save_invoice(&env, invoice_id, &invoice); events::dispute_resolved(&env, invoice_id, &admin_addr, &DisputeOutcome::Refunded); - events::invoice_refunded(&env, invoice_id); + events::invoice_refunded(&env, invoice_id, total_refunded_amount); events::invoice_state_changed( &env, invoice_id, diff --git a/contracts/split/src/test.rs b/contracts/split/src/test.rs index 78ff5b0..dae7301 100644 --- a/contracts/split/src/test.rs +++ b/contracts/split/src/test.rs @@ -6,7 +6,7 @@ use super::*; use soroban_sdk::{ testutils::{Address as _, Events as _, Ledger, LedgerInfo}, token::{Client as TokenClient, StellarAssetClient}, - Address, Bytes, BytesN, Env, String, Symbol, TryFromVal, Vec, + Address, Bytes, BytesN, Env, String, Symbol, TryFromVal, Val, Vec, }; use types::{InvoiceOptions, InvoiceOptions2}; @@ -9521,3 +9521,181 @@ fn test_milestones_auto_release() { assert_eq!(tk.balance(&recipient), 100); assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released); } + +// --------------------------------------------------------------------------- +// invoice_expired: creator address included in the event payload +// --------------------------------------------------------------------------- + +#[test] +fn test_invoice_expired_event_includes_creator() { + let (env, contract_id, token_id) = setup(); + let c = client(&env, &contract_id); + + let creator = Address::generate(&env); + let payer = Address::generate(&env); + let recipient = Address::generate(&env); + + StellarAssetClient::new(&env, &token_id).mint(&payer, &100); + 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); + + env.ledger().set_timestamp(3_000); + c.notify_expired(&id); + + let event_data = env + .events() + .all() + .iter() + .find_map(|(_contract, topics, data)| { + if topic1_is(&env, &topics, "expired") { + Some(data) + } else { + None + } + }) + .expect("invoice_expired event not emitted"); + + let items = Vec::