Skip to content

Commit 1c50a6f

Browse files
committed
feat: include token in payment event, add invoice_frozen/allowlist_removed events
- payment_received event now carries the funding token address alongside payer/amount/seq, updated across all seven call sites (standard pay, multi-recipient pay, token pay, bridge pay, pool pay, delegated pay, and pay-on-behalf-of). - invoice_cloned event now emits the ledger sequence number instead of empty data, so listeners can correlate clones to a point in time. - pause_invoice now also emits a new invoice_frozen event (creator, ledger) alongside the existing invoice_paused event. - Add remove_allowlist entrypoint to clear an invoice's entire payer allowlist in one call (reopening it to any payer) and emit a new allowlist_removed event; mirrors the existing add/remove_allowed_payer auth and pause checks.
1 parent c6cda3f commit 1c50a6f

3 files changed

Lines changed: 154 additions & 13 deletions

File tree

contracts/split/src/events.rs

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ pub fn invoice_created(
4141

4242
/// Emitted when a payment is received toward an invoice.
4343
/// Topics: (split, paid, invoice_id)
44-
/// Data: (payer, amount, event_seq)
45-
pub fn payment_received(env: &Env, invoice_id: u64, payer: &Address, amount: i128) {
44+
/// Data: (payer, amount, token, event_seq)
45+
pub fn payment_received(env: &Env, invoice_id: u64, payer: &Address, amount: i128, token: &Address) {
4646
let event_seq = next_seq(env, invoice_id);
4747
env.events().publish(
4848
(symbol_short!("split"), symbol_short!("paid"), invoice_id),
49-
(payer.clone(), amount, event_seq),
49+
(payer.clone(), amount, token.clone(), event_seq),
5050
);
5151
}
5252

@@ -313,10 +313,12 @@ pub fn payment_matched(env: &Env, invoice_id: u64, memo: u64, payer: &Address) {
313313

314314
/// Emitted when an invoice is cloned.
315315
/// Topics: (cloned, source_id, new_id)
316-
/// Data: ()
316+
/// Data: ledger_sequence
317317
pub fn invoice_cloned(env: &Env, source_id: u64, new_id: u64) {
318-
env.events()
319-
.publish((symbol_short!("cloned"), source_id, new_id), ());
318+
env.events().publish(
319+
(symbol_short!("cloned"), source_id, new_id),
320+
(env.ledger().sequence(),),
321+
);
320322
}
321323

322324
/// Emitted when an invoice is paused.
@@ -335,6 +337,16 @@ pub fn invoice_paused(
335337
);
336338
}
337339

340+
/// Emitted whenever an invoice's `frozen` flag transitions to true.
341+
/// Topics: (split, frozen, invoice_id)
342+
/// Data: (creator, ledger)
343+
pub fn invoice_frozen(env: &Env, invoice_id: u64, creator: &Address) {
344+
env.events().publish(
345+
(symbol_short!("split"), symbol_short!("frozen"), invoice_id),
346+
(creator.clone(), env.ledger().sequence()),
347+
);
348+
}
349+
338350
/// Emitted when an invoice is resumed.
339351
/// Topics: (split, resumed, invoice_id)
340352
/// Data: creator
@@ -672,6 +684,17 @@ pub fn allowlist_updated(
672684
);
673685
}
674686

687+
/// Emitted when a creator clears an invoice's entire payer allowlist,
688+
/// transitioning it from restricted to open (allowed_payers set to None).
689+
/// Topics: (split, al_open, invoice_id)
690+
/// Data: (creator, ledger)
691+
pub fn allowlist_removed(env: &Env, invoice_id: u64, creator: &Address) {
692+
env.events().publish(
693+
(symbol_short!("split"), symbol_short!("al_open"), invoice_id),
694+
(creator.clone(), env.ledger().sequence()),
695+
);
696+
}
697+
675698
/// Issue #308: Emitted when a payer claims their per-payer refund.
676699
/// Topics: (split, ref_clm, invoice_id)
677700
/// Data: (payer, amount)

contracts/split/src/lib.rs

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6469,7 +6469,7 @@ impl SplitContract {
64696469
.set(&cumulative_key, &(cumulative + net_paid));
64706470

64716471
// In real app we might handle penalty/oracle, but for simplicity:
6472-
events::payment_received(&env, invoice_id, &payer, net_paid);
6472+
events::payment_received(&env, invoice_id, &payer, net_paid, &funding_token_for(&invoice));
64736473

64746474
let total: i128 = invoice.amounts.iter().sum();
64756475
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
@@ -7259,7 +7259,7 @@ impl SplitContract {
72597259
.set(&credit_key(payer), &(credit + 1));
72607260

72617261
append_audit_entry(env, invoice_id, symbol_short!("pay"), payer);
7262-
events::payment_received(env, invoice_id, payer, credited_amount);
7262+
events::payment_received(env, invoice_id, payer, credited_amount, &funding_token_for(&invoice));
72637263
// Issue #333: emit milestone events for any thresholds crossed by this payment.
72647264
{
72657265
let total_for_milestone: i128 = total; // already computed above
@@ -7485,7 +7485,7 @@ impl SplitContract {
74857485
.set(&cumulative_key, &(cumulative + credited_amount));
74867486

74877487
append_audit_entry(&env, invoice_id, symbol_short!("pay_tok"), &payer);
7488-
events::payment_received(&env, invoice_id, &payer, credited_amount);
7488+
events::payment_received(&env, invoice_id, &payer, credited_amount, &funding_token_for(&invoice));
74897489
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
74907490
Self::record_invoice_rate_limit(&env, invoice_id, &payer);
74917491
notify_invoice(
@@ -7592,7 +7592,7 @@ impl SplitContract {
75927592
.set(&cumulative_key, &(cumulative + converted));
75937593

75947594
append_audit_entry(&env, invoice_id, symbol_short!("brdg_pay"), &payer);
7595-
events::payment_received(&env, invoice_id, &payer, converted);
7595+
events::payment_received(&env, invoice_id, &payer, converted, &invoice_token);
75967596
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
75977597
Self::record_invoice_rate_limit(&env, invoice_id, &payer);
75987598
notify_invoice(
@@ -7710,7 +7710,7 @@ impl SplitContract {
77107710
.set(&cumulative_key, &(cumulative + p.amount));
77117711

77127712
append_audit_entry(&env, p.invoice_id, symbol_short!("pool_pay"), &payer);
7713-
events::payment_received(&env, p.invoice_id, &payer, p.amount);
7713+
events::payment_received(&env, p.invoice_id, &payer, p.amount, &shared_token);
77147714

77157715
let inv_total: i128 = inv.amounts.iter().sum();
77167716
if inv.funded >= inv_total {
@@ -8403,6 +8403,7 @@ impl SplitContract {
84038403
save_invoice(&env, invoice_id, &invoice);
84048404

84058405
append_audit_entry(&env, invoice_id, symbol_short!("paused"), &creator);
8406+
events::invoice_frozen(&env, invoice_id, &creator);
84068407
events::invoice_paused(&env, invoice_id, &creator, &reason, &auto_resume_at);
84078408
}
84088409

@@ -8494,6 +8495,29 @@ impl SplitContract {
84948495
}
84958496
}
84968497

8498+
/// Remove the invoice's entire payer allowlist, reopening it to any payer.
8499+
///
8500+
/// Only the creator (or a co-creator) may call this. Sets `allowed_payers`
8501+
/// to `None`. If the invoice is already open, this is a no-op and does not
8502+
/// emit an event.
8503+
pub fn remove_allowlist(env: Env, creator: Address, invoice_id: u64) {
8504+
require_not_paused(&env);
8505+
creator.require_auth();
8506+
8507+
let mut invoice = load_invoice(&env, invoice_id);
8508+
assert!(
8509+
invoice.creator == creator || invoice.co_creators.iter().any(|c| c == creator),
8510+
"only creator can modify allowlist"
8511+
);
8512+
8513+
if invoice.allowed_payers.is_some() {
8514+
invoice.allowed_payers = None;
8515+
save_invoice(&env, invoice_id, &invoice);
8516+
append_audit_entry(&env, invoice_id, symbol_short!("al_open"), &creator);
8517+
events::allowlist_removed(&env, invoice_id, &creator);
8518+
}
8519+
}
8520+
84978521
/// Issue #329: Update the off-chain metadata hash for an invoice.
84988522
///
84998523
/// Only the creator may call this. Emits `MetadataUpdated` with old and new hash.
@@ -13054,7 +13078,7 @@ impl SplitContract {
1305413078
.set(&cumulative_key, &(cumulative + amount));
1305513079

1305613080
append_audit_entry(&env, invoice_id, symbol_short!("del_pay"), &delegate);
13057-
events::payment_received(&env, invoice_id, &beneficiary, amount);
13081+
events::payment_received(&env, invoice_id, &beneficiary, amount, &funding_token_for(&invoice));
1305813082
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
1305913083
Self::record_invoice_rate_limit(&env, invoice_id, &beneficiary);
1306013084
notify_invoice(
@@ -13760,7 +13784,7 @@ impl SplitContract {
1376013784
.set(&cumulative_key, &(cumulative + amount));
1376113785

1376213786
events::delegated_payment(&env, invoice_id, &on_behalf_of, &executor, amount);
13763-
events::payment_received(&env, invoice_id, &on_behalf_of, amount);
13787+
events::payment_received(&env, invoice_id, &on_behalf_of, amount, &funding_token_for(&invoice));
1376413788
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
1376513789
Self::record_invoice_rate_limit(&env, invoice_id, &on_behalf_of);
1376613790
append_audit_entry(&env, invoice_id, symbol_short!("dlgt_pay"), &executor);

contracts/split/src/test.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4786,6 +4786,28 @@ fn test_pause_blocks_payment_with_reason() {
47864786
c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None);
47874787
}
47884788

4789+
#[test]
4790+
fn test_pause_invoice_emits_frozen_event() {
4791+
let (env, contract_id, token_id) = setup_initialized();
4792+
let c = client(&env, &contract_id);
4793+
4794+
let creator = Address::generate(&env);
4795+
let recipient = Address::generate(&env);
4796+
4797+
env.ledger().set_timestamp(1_000);
4798+
let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999);
4799+
4800+
let reason = soroban_sdk::String::from_str(&env, "legal review pending");
4801+
c.pause_invoice(&creator, &id, &reason, &None);
4802+
4803+
let has_frozen_event = env
4804+
.events()
4805+
.all()
4806+
.iter()
4807+
.any(|(_c, topics, _d)| topic1_is(&env, &topics, "frozen"));
4808+
assert!(has_frozen_event, "expected an invoice_frozen event on pause");
4809+
}
4810+
47894811
#[test]
47904812
fn test_auto_resume_allows_payment_after_timestamp() {
47914813
let (env, contract_id, token_id) = setup_initialized();
@@ -4944,6 +4966,42 @@ fn test_clone_copies_recipients_and_amounts() {
49444966
assert_eq!(clone_ext.parent_invoice_id, Some(source_id));
49454967
}
49464968

4969+
#[test]
4970+
fn test_clone_invoice_emits_ledger_sequence_in_event_data() {
4971+
use soroban_sdk::TryIntoVal;
4972+
4973+
let (env, contract_id, token_id) = setup_initialized();
4974+
let c = client(&env, &contract_id);
4975+
4976+
let creator = Address::generate(&env);
4977+
let recipient = Address::generate(&env);
4978+
4979+
env.ledger().set_timestamp(1_000);
4980+
4981+
let source_id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999);
4982+
4983+
env.ledger().set_sequence_number(42);
4984+
let overrides = types::CloneOverrides {
4985+
new_deadline: None,
4986+
new_amounts: None,
4987+
new_recipients: None,
4988+
new_overflow_behavior: None,
4989+
new_metadata_hash: None,
4990+
};
4991+
let _clone_id = c.clone_invoice(&creator, &source_id, &overrides);
4992+
4993+
let cloned_event = env
4994+
.events()
4995+
.all()
4996+
.iter()
4997+
.find(|(_c, topics, _d)| topic0_is(&env, topics, "cloned"))
4998+
.expect("expected an invoice_cloned event");
4999+
5000+
let (_contract, _topics, data) = cloned_event;
5001+
let (ledger_seq,): (u32,) = data.try_into_val(&env).unwrap();
5002+
assert_eq!(ledger_seq, 42);
5003+
}
5004+
49475005
#[test]
49485006
fn test_clone_with_overrides_replaces_fields() {
49495007
let (env, contract_id, token_id) = setup_initialized();
@@ -7121,6 +7179,42 @@ fn test_309_remove_allowed_payer_emits_event() {
71217179
assert_eq!(payers.len(), 0, "allowed_payers should be empty after removal");
71227180
}
71237181

7182+
#[test]
7183+
fn test_remove_allowlist_opens_invoice_and_emits_event() {
7184+
let (env, contract_id, token_id) = setup_initialized();
7185+
let c = client(&env, &contract_id);
7186+
let tk = token_client(&env, &token_id);
7187+
7188+
let creator = Address::generate(&env);
7189+
let allowed_payer = Address::generate(&env);
7190+
let other_payer = Address::generate(&env);
7191+
let recipient = Address::generate(&env);
7192+
7193+
StellarAssetClient::new(&env, &token_id).mint(&other_payer, &300);
7194+
env.ledger().set_timestamp(1_000);
7195+
7196+
let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999);
7197+
c.add_allowed_payer(&creator, &id, &allowed_payer);
7198+
assert!(c.get_invoice_ext(&id).allowed_payers.is_some());
7199+
7200+
c.remove_allowlist(&creator, &id);
7201+
assert!(c.get_invoice_ext(&id).allowed_payers.is_none());
7202+
7203+
let has_allowlist_removed_event = env
7204+
.events()
7205+
.all()
7206+
.iter()
7207+
.any(|(_c, topics, _d)| topic1_is(&env, &topics, "al_open"));
7208+
assert!(
7209+
has_allowlist_removed_event,
7210+
"expected an allowlist_removed event"
7211+
);
7212+
7213+
// The invoice is now open — a previously non-allowed payer can pay.
7214+
c.pay(&other_payer, &id, &300_i128, &0_u64, &false, &false, &None);
7215+
assert_eq!(tk.balance(&recipient), 300);
7216+
}
7217+
71247218
#[test]
71257219
fn test_creator_stats_unique_payers() {
71267220
let (env, contract_id, token_id) = setup_initialized();

0 commit comments

Comments
 (0)