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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 29 additions & 6 deletions contracts/split/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ pub fn invoice_created(

/// Emitted when a payment is received toward an invoice.
/// Topics: (split, paid, invoice_id)
/// Data: (payer, amount, event_seq)
pub fn payment_received(env: &Env, invoice_id: u64, payer: &Address, amount: i128) {
/// Data: (payer, amount, token, event_seq)
pub fn payment_received(env: &Env, invoice_id: u64, payer: &Address, amount: i128, token: &Address) {
let event_seq = next_seq(env, invoice_id);
env.events().publish(
(symbol_short!("split"), symbol_short!("paid"), invoice_id),
(payer.clone(), amount, event_seq),
(payer.clone(), amount, token.clone(), event_seq),
);
}

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

/// Emitted when an invoice is cloned.
/// Topics: (cloned, source_id, new_id)
/// Data: ()
/// Data: ledger_sequence
pub fn invoice_cloned(env: &Env, source_id: u64, new_id: u64) {
env.events()
.publish((symbol_short!("cloned"), source_id, new_id), ());
env.events().publish(
(symbol_short!("cloned"), source_id, new_id),
(env.ledger().sequence(),),
);
}

/// Emitted when an invoice is paused.
Expand All @@ -335,6 +337,16 @@ pub fn invoice_paused(
);
}

/// Emitted whenever an invoice's `frozen` flag transitions to true.
/// Topics: (split, frozen, invoice_id)
/// Data: (creator, ledger)
pub fn invoice_frozen(env: &Env, invoice_id: u64, creator: &Address) {
env.events().publish(
(symbol_short!("split"), symbol_short!("frozen"), invoice_id),
(creator.clone(), env.ledger().sequence()),
);
}

/// Emitted when an invoice is resumed.
/// Topics: (split, resumed, invoice_id)
/// Data: creator
Expand Down Expand Up @@ -672,6 +684,17 @@ pub fn allowlist_updated(
);
}

/// Emitted when a creator clears an invoice's entire payer allowlist,
/// transitioning it from restricted to open (allowed_payers set to None).
/// Topics: (split, al_open, invoice_id)
/// Data: (creator, ledger)
pub fn allowlist_removed(env: &Env, invoice_id: u64, creator: &Address) {
env.events().publish(
(symbol_short!("split"), symbol_short!("al_open"), invoice_id),
(creator.clone(), env.ledger().sequence()),
);
}

/// Issue #308: Emitted when a payer claims their per-payer refund.
/// Topics: (split, ref_clm, invoice_id)
/// Data: (payer, amount)
Expand Down
38 changes: 31 additions & 7 deletions contracts/split/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6469,7 +6469,7 @@ impl SplitContract {
.set(&cumulative_key, &(cumulative + net_paid));

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

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

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

append_audit_entry(&env, invoice_id, symbol_short!("pay_tok"), &payer);
events::payment_received(&env, invoice_id, &payer, credited_amount);
events::payment_received(&env, invoice_id, &payer, credited_amount, &funding_token_for(&invoice));
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
Self::record_invoice_rate_limit(&env, invoice_id, &payer);
notify_invoice(
Expand Down Expand Up @@ -7592,7 +7592,7 @@ impl SplitContract {
.set(&cumulative_key, &(cumulative + converted));

append_audit_entry(&env, invoice_id, symbol_short!("brdg_pay"), &payer);
events::payment_received(&env, invoice_id, &payer, converted);
events::payment_received(&env, invoice_id, &payer, converted, &invoice_token);
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
Self::record_invoice_rate_limit(&env, invoice_id, &payer);
notify_invoice(
Expand Down Expand Up @@ -7710,7 +7710,7 @@ impl SplitContract {
.set(&cumulative_key, &(cumulative + p.amount));

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

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

append_audit_entry(&env, invoice_id, symbol_short!("paused"), &creator);
events::invoice_frozen(&env, invoice_id, &creator);
events::invoice_paused(&env, invoice_id, &creator, &reason, &auto_resume_at);
}

Expand Down Expand Up @@ -8494,6 +8495,29 @@ impl SplitContract {
}
}

/// Remove the invoice's entire payer allowlist, reopening it to any payer.
///
/// Only the creator (or a co-creator) may call this. Sets `allowed_payers`
/// to `None`. If the invoice is already open, this is a no-op and does not
/// emit an event.
pub fn remove_allowlist(env: Env, creator: Address, invoice_id: u64) {
require_not_paused(&env);
creator.require_auth();

let mut invoice = load_invoice(&env, invoice_id);
assert!(
invoice.creator == creator || invoice.co_creators.iter().any(|c| c == creator),
"only creator can modify allowlist"
);

if invoice.allowed_payers.is_some() {
invoice.allowed_payers = None;
save_invoice(&env, invoice_id, &invoice);
append_audit_entry(&env, invoice_id, symbol_short!("al_open"), &creator);
events::allowlist_removed(&env, invoice_id, &creator);
}
}

/// Issue #329: Update the off-chain metadata hash for an invoice.
///
/// Only the creator may call this. Emits `MetadataUpdated` with old and new hash.
Expand Down Expand Up @@ -13054,7 +13078,7 @@ impl SplitContract {
.set(&cumulative_key, &(cumulative + amount));

append_audit_entry(&env, invoice_id, symbol_short!("del_pay"), &delegate);
events::payment_received(&env, invoice_id, &beneficiary, amount);
events::payment_received(&env, invoice_id, &beneficiary, amount, &funding_token_for(&invoice));
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
Self::record_invoice_rate_limit(&env, invoice_id, &beneficiary);
notify_invoice(
Expand Down Expand Up @@ -13760,7 +13784,7 @@ impl SplitContract {
.set(&cumulative_key, &(cumulative + amount));

events::delegated_payment(&env, invoice_id, &on_behalf_of, &executor, amount);
events::payment_received(&env, invoice_id, &on_behalf_of, amount);
events::payment_received(&env, invoice_id, &on_behalf_of, amount, &funding_token_for(&invoice));
check_and_emit_funding_checkpoints(&env, invoice_id, invoice.funded, total);
Self::record_invoice_rate_limit(&env, invoice_id, &on_behalf_of);
append_audit_entry(&env, invoice_id, symbol_short!("dlgt_pay"), &executor);
Expand Down
94 changes: 94 additions & 0 deletions contracts/split/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4786,6 +4786,28 @@ fn test_pause_blocks_payment_with_reason() {
c.pay(&payer, &id, &100_i128, &0_u64, &false, &false, &None);
}

#[test]
fn test_pause_invoice_emits_frozen_event() {
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);
let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999);

let reason = soroban_sdk::String::from_str(&env, "legal review pending");
c.pause_invoice(&creator, &id, &reason, &None);

let has_frozen_event = env
.events()
.all()
.iter()
.any(|(_c, topics, _d)| topic1_is(&env, &topics, "frozen"));
assert!(has_frozen_event, "expected an invoice_frozen event on pause");
}

#[test]
fn test_auto_resume_allows_payment_after_timestamp() {
let (env, contract_id, token_id) = setup_initialized();
Expand Down Expand Up @@ -4944,6 +4966,42 @@ fn test_clone_copies_recipients_and_amounts() {
assert_eq!(clone_ext.parent_invoice_id, Some(source_id));
}

#[test]
fn test_clone_invoice_emits_ledger_sequence_in_event_data() {
use soroban_sdk::TryIntoVal;

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);

let source_id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999);

env.ledger().set_sequence_number(42);
let overrides = types::CloneOverrides {
new_deadline: None,
new_amounts: None,
new_recipients: None,
new_overflow_behavior: None,
new_metadata_hash: None,
};
let _clone_id = c.clone_invoice(&creator, &source_id, &overrides);

let cloned_event = env
.events()
.all()
.iter()
.find(|(_c, topics, _d)| topic0_is(&env, topics, "cloned"))
.expect("expected an invoice_cloned event");

let (_contract, _topics, data) = cloned_event;
let (ledger_seq,): (u32,) = data.try_into_val(&env).unwrap();
assert_eq!(ledger_seq, 42);
}

#[test]
fn test_clone_with_overrides_replaces_fields() {
let (env, contract_id, token_id) = setup_initialized();
Expand Down Expand Up @@ -7121,6 +7179,42 @@ fn test_309_remove_allowed_payer_emits_event() {
assert_eq!(payers.len(), 0, "allowed_payers should be empty after removal");
}

#[test]
fn test_remove_allowlist_opens_invoice_and_emits_event() {
let (env, contract_id, token_id) = setup_initialized();
let c = client(&env, &contract_id);
let tk = token_client(&env, &token_id);

let creator = Address::generate(&env);
let allowed_payer = Address::generate(&env);
let other_payer = Address::generate(&env);
let recipient = Address::generate(&env);

StellarAssetClient::new(&env, &token_id).mint(&other_payer, &300);
env.ledger().set_timestamp(1_000);

let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999);
c.add_allowed_payer(&creator, &id, &allowed_payer);
assert!(c.get_invoice_ext(&id).allowed_payers.is_some());

c.remove_allowlist(&creator, &id);
assert!(c.get_invoice_ext(&id).allowed_payers.is_none());

let has_allowlist_removed_event = env
.events()
.all()
.iter()
.any(|(_c, topics, _d)| topic1_is(&env, &topics, "al_open"));
assert!(
has_allowlist_removed_event,
"expected an allowlist_removed event"
);

// The invoice is now open — a previously non-allowed payer can pay.
c.pay(&other_payer, &id, &300_i128, &0_u64, &false, &false, &None);
assert_eq!(tk.balance(&recipient), 300);
}

#[test]
fn test_creator_stats_unique_payers() {
let (env, contract_id, token_id) = setup_initialized();
Expand Down
Loading