Skip to content
Open
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
13 changes: 12 additions & 1 deletion contracts/split/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use soroban_sdk::{symbol_short, Address, Bytes, Env, Vec};
use soroban_sdk::{symbol_short, Address, Bytes, Env, Symbol, Vec};

/// Emitted when a new invoice is created.
pub fn invoice_created(env: &Env, invoice_id: u64, creator: &Address, total: i128, metadata: &Option<Bytes>) {
Expand Down Expand Up @@ -45,3 +45,14 @@ pub fn recipient_added(env: &Env, invoice_id: u64, recipient: &Address, amount:
(recipient.clone(), amount),
);
}

/// Emitted at the start of every public entry point for real-time contract health observability.
///
/// Topic: `(symbol_short!("monitor"), function_name)`
/// Data: `(invoice_id, actor_address, ledger_timestamp)`
pub fn monitor_event(env: &Env, function: Symbol, invoice_id: u64, actor: &Address, timestamp: u64) {
env.events().publish(
(symbol_short!("monitor"), function),
(invoice_id, actor.clone(), timestamp),
);
}
28 changes: 28 additions & 0 deletions contracts/split/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,13 @@ impl SplitContract {
) -> u64 {
require_not_paused(&env);
creator.require_auth();
events::monitor_event(
&env,
Symbol::new(&env, "create_invoice"),
0,
&creator,
env.ledger().timestamp(),
);
Self::_create_invoice_inner(
&env,
creator,
Expand Down Expand Up @@ -621,6 +628,13 @@ impl SplitContract {
pub fn pay(env: Env, payer: Address, invoice_id: u64, amount: i128, nonce: u64, auto_convert: bool) {
require_not_paused(&env);
payer.require_auth();
events::monitor_event(
&env,
Symbol::new(&env, "pay"),
invoice_id,
&payer,
env.ledger().timestamp(),
);
Self::_pay(&env, &payer, invoice_id, amount, nonce, auto_convert);
}

Expand Down Expand Up @@ -785,6 +799,13 @@ impl SplitContract {
require_not_paused(&env);
let caller = env.current_contract_address();
let mut invoice = load_invoice(&env, invoice_id);
events::monitor_event(
&env,
Symbol::new(&env, "release"),
invoice_id,
&caller,
env.ledger().timestamp(),
);

assert!(!invoice.frozen, "invoice is frozen");
assert!(
Expand Down Expand Up @@ -1113,6 +1134,13 @@ impl SplitContract {
pub fn refund(env: Env, invoice_id: u64) {
require_not_paused(&env);
let mut invoice = load_invoice(&env, invoice_id);
events::monitor_event(
&env,
Symbol::new(&env, "refund"),
invoice_id,
&env.current_contract_address(),
env.ledger().timestamp(),
);

assert!(
invoice.status == InvoiceStatus::Pending,
Expand Down
124 changes: 124 additions & 0 deletions contracts/split/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2330,3 +2330,127 @@ fn test_min_funding_bps_allows_release_above_threshold() {
assert_eq!(c.get_invoice(&id).status, InvoiceStatus::Released);
assert_eq!(tk.balance(&recipient), 900);
}

// ---------------------------------------------------------------------------
// Monitoring hooks tests (issue #180)
// ---------------------------------------------------------------------------

#[test]
fn test_monitor_event_on_create_invoice() {
use soroban_sdk::testutils::Events;

let (env, contract_id, token_id) = setup();
let c = client(&env, &contract_id);
let creator = Address::generate(&env);
let recipient = Address::generate(&env);

env.ledger().set_timestamp(1_000);
make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 2_000);

// At least one monitor event must have been emitted for "create_invoice".
let found = env.events().all().iter().any(|(_, topics, _)| {
topics.get(0) == Some(soroban_sdk::Val::from(symbol_short!("monitor")))
&& topics.get(1)
== Some(soroban_sdk::Val::from(Symbol::new(&env, "create_invoice")))
});
assert!(found, "monitor event for create_invoice not found");
}

#[test]
fn test_monitor_event_on_pay() {
use soroban_sdk::testutils::Events;

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, &200);
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);

let found = env.events().all().iter().any(|(_, topics, _)| {
topics.get(0) == Some(soroban_sdk::Val::from(symbol_short!("monitor")))
&& topics.get(1) == Some(soroban_sdk::Val::from(Symbol::new(&env, "pay")))
});
assert!(found, "monitor event for pay not found");
}

#[test]
fn test_monitor_event_on_release() {
use soroban_sdk::testutils::Events;

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, &500);
env.ledger().set_timestamp(1_000);

// Use a co-signer to prevent auto-release so we can call release() explicitly.
let co_signer = Address::generate(&env);
let mut co_signers = soroban_sdk::Vec::new(&env);
co_signers.push_back(co_signer.clone());

let mut recipients = soroban_sdk::Vec::new(&env);
recipients.push_back(recipient.clone());
let mut amounts = soroban_sdk::Vec::new(&env);
amounts.push_back(100_i128);

let opts = InvoiceOptions {
co_creators: soroban_sdk::Vec::new(&env),
allow_early_withdrawal: false,
bonus_pool: 0,
bonus_max_payers: 0,
prerequisite_id: None,
tranches: soroban_sdk::Vec::new(&env),
co_signers,
required_signatures: 1,
penalty_bps: None,
penalty_deadline: None,
min_funding_bps: None,
};
let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64, &opts);

c.pay(&payer, &id, &100_i128, &0_u64);
c.sign_release(&id, &co_signer);
c.release(&id);

let found = env.events().all().iter().any(|(_, topics, _)| {
topics.get(0) == Some(soroban_sdk::Val::from(symbol_short!("monitor")))
&& topics.get(1) == Some(soroban_sdk::Val::from(Symbol::new(&env, "release")))
});
assert!(found, "monitor event for release not found");
}

#[test]
fn test_monitor_event_on_refund() {
use soroban_sdk::testutils::Events;

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, &500);
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);

// Advance past deadline so refund is allowed.
env.ledger().set_timestamp(3_000);
c.refund(&id);

let found = env.events().all().iter().any(|(_, topics, _)| {
topics.get(0) == Some(soroban_sdk::Val::from(symbol_short!("monitor")))
&& topics.get(1) == Some(soroban_sdk::Val::from(Symbol::new(&env, "refund")))
});
assert!(found, "monitor event for refund not found");
}