diff --git a/contracts/split/src/events.rs b/contracts/split/src/events.rs index d3bb7b2..72aff97 100644 --- a/contracts/split/src/events.rs +++ b/contracts/split/src/events.rs @@ -66,6 +66,17 @@ pub fn invoice_created( ); } +/// Emitted at invoice creation when `forward_to` is configured, making +/// surplus-forwarding visible to indexers without waiting for a release. +/// Topics: (split, fwd_cfg, invoice_id) +/// Data: forward_to +pub fn forward_configured(env: &Env, invoice_id: u64, forward_to: &Address) { + env.events().publish( + (symbol_short!("split"), symbol_short!("fwd_cfg"), invoice_id), + forward_to.clone(), + ); +} + /// Emitted when a payment is received toward an invoice. /// Topics: (split, paid, invoice_id) /// Data: (payer, amount, token, event_seq) @@ -395,6 +406,18 @@ pub fn invoice_resumed(env: &Env, invoice_id: u64, creator: &Address) { ); } +/// Emitted when a paused invoice is automatically resumed because +/// `auto_resume_at` has passed (checked lazily on the next `pay()` call). +/// Distinct from `invoice_resumed`, which is only for manual `resume_invoice`. +/// Topics: (split, auto_res, invoice_id) +/// Data: auto_resume_at +pub fn invoice_auto_resumed(env: &Env, invoice_id: u64, auto_resume_at: u64) { + env.events().publish( + (symbol_short!("split"), symbol_short!("auto_res"), invoice_id), + auto_resume_at, + ); +} + /// Emitted when an invoice is force resumed. /// Topics: (split, forced, invoice_id) /// Data: admin_addr @@ -405,6 +428,18 @@ pub fn invoice_force_resumed(env: &Env, invoice_id: u64, admin_addr: &Address) { ); } +/// Emitted when the per-invoice contributor allowlist gating is toggled on +/// (first entry added, list goes None -> Some) or off (last entry removed, +/// list goes Some -> None). +/// Topics: (split, al_tog, invoice_id) +/// Data: (creator, enabled) +pub fn contributor_allowlist_toggled(env: &Env, invoice_id: u64, creator: &Address, enabled: bool) { + env.events().publish( + (symbol_short!("split"), symbol_short!("al_tog"), invoice_id), + (creator.clone(), enabled), + ); +} + /// Emitted when a pending payout is claimed by a recipient (issue #209). /// Topics: (split, pend_pay, invoice_id) /// Data: (recipient, amount) diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs index 3bc35c5..cbaf26c 100644 --- a/contracts/split/src/lib.rs +++ b/contracts/split/src/lib.rs @@ -5886,6 +5886,9 @@ impl SplitContract { } events::invoice_created(env, id, &creator, total, &invoice.cross_chain_ref); + if let Some(ref addr) = invoice.forward_to { + events::forward_configured(env, id, addr); + } maybe_record_created(env, &creator, total); update_creator_stats_on_creation(env, &creator); @@ -7023,6 +7026,7 @@ impl SplitContract { invoice.pause_reason = None; invoice.auto_resume_at = None; save_invoice(env, invoice_id, &invoice); + events::invoice_auto_resumed(env, invoice_id, auto_at); } } } @@ -8480,6 +8484,7 @@ impl SplitContract { invoice.creator == creator || invoice.co_creators.contains(&creator), "NotAuthorized" ); + let was_disabled = invoice.contributor_allowlist.is_none(); let mut list = invoice .contributor_allowlist .unwrap_or_else(|| Vec::new(&env)); @@ -8489,6 +8494,9 @@ impl SplitContract { invoice.contributor_allowlist = Some(list); save_invoice(&env, invoice_id, &invoice); append_audit_entry(&env, invoice_id, symbol_short!("al_add"), &creator); + if was_disabled { + events::contributor_allowlist_toggled(&env, invoice_id, &creator, true); + } } /// Remove `contributor` from the per-invoice contributor allowlist. @@ -8507,6 +8515,7 @@ impl SplitContract { invoice.creator == creator || invoice.co_creators.contains(&creator), "NotAuthorized" ); + let mut became_disabled = false; if let Some(old_list) = invoice.contributor_allowlist { let mut new_list: Vec
= Vec::new(&env); for addr in old_list.iter() { @@ -8514,6 +8523,7 @@ impl SplitContract { new_list.push_back(addr); } } + became_disabled = new_list.is_empty(); invoice.contributor_allowlist = if new_list.is_empty() { None } else { @@ -8522,6 +8532,9 @@ impl SplitContract { } save_invoice(&env, invoice_id, &invoice); append_audit_entry(&env, invoice_id, symbol_short!("al_rm"), &creator); + if became_disabled { + events::contributor_allowlist_toggled(&env, invoice_id, &creator, false); + } } // ----------------------------------------------------------------------- diff --git a/contracts/split/src/test.rs b/contracts/split/src/test.rs index f503e18..498ea3b 100644 --- a/contracts/split/src/test.rs +++ b/contracts/split/src/test.rs @@ -547,6 +547,61 @@ fn test_forward_to_invoice_credits_target_invoice() { assert_eq!(c.get_invoice(&id_parent).funded, 0); } +#[test] +fn test_forward_configured_event_emitted_when_forward_to_set() { + let (env, contract_id, token_id) = setup_initialized(); + let c = client(&env, &contract_id); + + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + let forward_target = Address::generate(&env); + + env.ledger().set_timestamp(1_000); + + let mut opts = default_options(&env); + opts.forward_to = Some(forward_target.clone()); + + let mut recipients = Vec::new(&env); + recipients.push_back(recipient.clone()); + let mut amounts = Vec::new(&env); + amounts.push_back(100_i128); + c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64, &opts); + + let has_forward_configured_event = env + .events() + .all() + .iter() + .any(|(_c, topics, _d)| topic1_is(&env, &topics, "fwd_cfg")); + assert!( + has_forward_configured_event, + "forward_configured event should be emitted when forward_to is set at creation" + ); +} + +#[test] +fn test_forward_configured_event_absent_when_forward_to_unset() { + let (env, contract_id, token_id) = setup_initialized(); + let c = client(&env, &contract_id); + + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + + env.ledger().set_timestamp(1_000); + + // default_options() leaves forward_to as None. + let _id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999); + + let has_forward_configured_event = env + .events() + .all() + .iter() + .any(|(_c, topics, _d)| topic1_is(&env, &topics, "fwd_cfg")); + assert!( + !has_forward_configured_event, + "forward_configured event should not fire when forward_to is not set" + ); +} + #[test] fn test_template_overwrite() { let (env, contract_id, token_id) = setup_initialized(); @@ -4838,6 +4893,28 @@ fn test_auto_resume_allows_payment_after_timestamp() { let invoice = c.get_invoice(&id); assert_eq!(invoice.status, InvoiceStatus::Released); assert_eq!(tk.balance(&recipient), 200); + + // A distinct `invoice_auto_resumed` event fires for the timer-triggered + // resume; the manual `resumed` event must NOT fire (it wasn't a manual resume). + let has_auto_resumed_event = env + .events() + .all() + .iter() + .any(|(_c, topics, _d)| topic1_is(&env, &topics, "auto_res")); + assert!( + has_auto_resumed_event, + "invoice_auto_resumed event should be emitted on lazy auto-resume" + ); + + let has_manual_resumed_event = env + .events() + .all() + .iter() + .any(|(_c, topics, _d)| topic1_is(&env, &topics, "resumed")); + assert!( + !has_manual_resumed_event, + "manual invoice_resumed should not fire for an automatic resume" + ); } #[test] @@ -6942,6 +7019,87 @@ fn test_309_allowlist_restricts_payers() { let _ = blocked_payer; } +#[test] +fn test_contributor_allowlist_toggle_events() { + let (env, contract_id, token_id) = setup_initialized(); + let c = client(&env, &contract_id); + + let creator = Address::generate(&env); + let recipient = Address::generate(&env); + let contributor = Address::generate(&env); + + env.ledger().set_timestamp(1_000); + + let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999); + + let ext_before = c.get_invoice_ext2(&id); + assert!(ext_before.contributor_allowlist.is_none()); + + // Adding the first contributor turns gating ON (None -> Some). + c.add_contributor_to_allowlist(&creator, &id, &contributor); + + let ext_enabled = c.get_invoice_ext2(&id); + assert!(ext_enabled.contributor_allowlist.is_some()); + + let toggled_on_count = env + .events() + .all() + .iter() + .filter(|(_c, topics, _d)| topic1_is(&env, topics, "al_tog")) + .count(); + assert_eq!( + toggled_on_count, 1, + "contributor_allowlist_toggled(enabled=true) should fire exactly once on first add" + ); + + // Removing the only contributor turns gating OFF (Some -> None). + c.remove_contributor_allowlist(&creator, &id, &contributor); + + let ext_disabled = c.get_invoice_ext2(&id); + assert!(ext_disabled.contributor_allowlist.is_none()); + + let toggled_total_count = env + .events() + .all() + .iter() + .filter(|(_c, topics, _d)| topic1_is(&env, topics, "al_tog")) + .count(); + assert_eq!( + toggled_total_count, 2, + "one toggle event for enabling, one for disabling" + ); +} + +#[test] +fn test_payment_received_event_includes_tip() { + let (env, contract_id, token_id) = setup_initialized(); + 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, &500); + 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, &None); + + use soroban_sdk::TryIntoVal; + let mut found_tip: Option = None; + for (_contract, topics, data) in env.events().all().iter() { + if topic1_is(&env, &topics, "paid") { + let decoded: (Address, i128, i128, u64) = data.try_into_val(&env).unwrap(); + found_tip = Some(decoded.2); + } + } + assert_eq!( + found_tip, + Some(0), + "payment_received event data should include the tip amount" + ); +} + #[test] fn test_creator_stats_on_invoice_creation() { let (env, contract_id, token_id) = setup_initialized();