diff --git a/contracts/split/src/lib.rs b/contracts/split/src/lib.rs
index e71bd8a..ab61d56 100644
--- a/contracts/split/src/lib.rs
+++ b/contracts/split/src/lib.rs
@@ -142,6 +142,11 @@ fn delegate_key(invoice_id: u64) -> (Symbol, u64) {
(symbol_short!("delegate"), invoice_id)
}
+/// Delegate-pay authorization key for a beneficiary.
+fn delegate_pay_key(beneficiary: &Address) -> (Symbol, Address) {
+ (symbol_short!("delegate_pay"), beneficiary.clone())
+}
+
/// Analytics counters (issue #28).
fn total_invoices_key() -> Symbol {
symbol_short!("tot_inv")
@@ -246,6 +251,13 @@ fn append_audit_entry(env: &Env, id: u64, action: Symbol, actor: &Address) {
env.storage().persistent().set(&audit_log_key(id), &log);
}
+fn notify_invoice(env: &Env, invoice_id: u64, event: Symbol, notification_contract: &Option
) {
+ if let Some(contract) = notification_contract {
+ let args = (invoice_id, event).into_val(env);
+ let _ = env.invoke_contract(contract, &Symbol::new(env, "notify"), args);
+ }
+}
+
pub fn get_audit_log(env: &Env, id: u64) -> Vec {
env.storage()
.persistent()
@@ -609,6 +621,8 @@ impl SplitContract {
options.tax_authority,
options.insurance_premium_bps.unwrap_or(0),
options.smart_route.unwrap_or(false),
+ options.notification_contract.clone(),
+ options.overflow_behavior.clone(),
options.convert_to_stream,
options.accepted_tokens,
options.forward_to,
@@ -650,6 +664,8 @@ impl SplitContract {
tax_authority: Option,
insurance_premium_bps: u32,
smart_route: bool,
+ notification_contract: Option,
+ overflow_behavior: OverflowBehavior,
convert_to_stream: bool,
accepted_tokens: Vec,
forward_to: Option,
@@ -868,6 +884,8 @@ impl SplitContract {
oracle_address,
condition_met: false,
smart_route,
+ overflow_behavior,
+ notification_contract,
convert_to_stream,
accepted_tokens,
forward_to,
@@ -1247,7 +1265,6 @@ impl SplitContract {
invoice.amounts.iter().sum()
};
let remaining = total - invoice.funded;
- assert!(amount <= remaining, "payment exceeds remaining balance");
if invoice.require_kyc {
let kyc_contract: Address = env
@@ -1312,23 +1329,54 @@ impl SplitContract {
}
let token_client = token::Client::new(env, &invoice.tokens.get(0).expect("no token"));
-
- let premium = (amount as u128 * invoice.insurance_premium_bps as u128 / 10_000u128) as i128;
- let total_charge = amount + premium;
+
+ let credited_amount = match invoice.overflow_behavior {
+ OverflowBehavior::Reject => {
+ assert!(amount <= remaining, "payment exceeds remaining balance");
+ amount
+ }
+ OverflowBehavior::Refund => {
+ if amount <= remaining {
+ amount
+ } else {
+ remaining
+ }
+ }
+ OverflowBehavior::Donate => {
+ if amount <= remaining {
+ amount
+ } else {
+ remaining
+ }
+ }
+ };
+
+ let premium = (credited_amount as u128 * invoice.insurance_premium_bps as u128 / 10_000u128) as i128;
+ let total_charge = credited_amount + premium;
// Issue #88: Auto-convert if requested.
- let credited_amount = if auto_convert {
- // In production, this would call a DEX swap contract.
- // For now, we assume a 1:1 swap and transfer the amount directly.
- // Mock DEX swap: payer's source asset -> invoice token.
- // The swapped amount is what gets credited.
+ if auto_convert {
token_client.transfer(payer, &env.current_contract_address(), &total_charge);
- amount // In a real implementation, this would be the swapped output amount.
} else {
token_client.transfer(payer, &env.current_contract_address(), &total_charge);
- amount
- };
-
+ }
+
+ let excess = amount - credited_amount;
+ match invoice.overflow_behavior {
+ OverflowBehavior::Refund if excess > 0 => {
+ token_client.transfer(&env.current_contract_address(), payer, &excess);
+ }
+ OverflowBehavior::Donate if excess > 0 => {
+ let treasury: Address = env
+ .storage()
+ .instance()
+ .get(&treasury_key())
+ .expect("treasury not set");
+ token_client.transfer(&env.current_contract_address(), &treasury, &excess);
+ }
+ _ => {}
+ }
+
invoice.insurance_fund += premium;
// Penalty for late payment (issue #42).
@@ -1379,6 +1427,7 @@ impl SplitContract {
append_audit_entry(env, invoice_id, symbol_short!("pay"), payer);
events::payment_received(env, invoice_id, payer, credited_amount);
+ notify_invoice(env, invoice_id, symbol_short!("pay"), &invoice.notification_contract);
// Issue: mint a receipt token to the payer via the receipt factory if configured.
if let Some(factory) = env
@@ -1389,7 +1438,7 @@ impl SplitContract {
let mut args: Vec = Vec::new(env);
args.push_back(invoice_id.into_val(env));
args.push_back(payer.clone().into_val(env));
- args.push_back(amount.into_val(env));
+ args.push_back(credited_amount.into_val(env));
let receipt_addr: Address = env.invoke_contract(
&factory,
&Symbol::new(env, "mint_receipt"),
@@ -1503,6 +1552,68 @@ impl SplitContract {
append_audit_entry(&env, invoice_id, symbol_short!("pay_tok"), &payer);
events::payment_received(&env, invoice_id, &payer, credited_amount);
+ notify_invoice(&env, invoice_id, symbol_short!("pay"), &invoice.notification_contract);
+
+ if invoice.funded >= total {
+ let in_group = env.storage().persistent().has(&invoice_group_key(invoice_id));
+ let guarded =
+ invoice.prerequisite_id.is_some()
+ || !invoice.tranches.is_empty()
+ || !invoice.release_stages.is_empty()
+ || in_group
+ || !invoice.co_signers.is_empty();
+ if guarded {
+ save_invoice(&env, invoice_id, &invoice);
+ } else {
+ Self::_release(&env, invoice_id, &mut invoice, &payer);
+ }
+ } else {
+ save_invoice(&env, invoice_id, &invoice);
+ }
+ }
+
+ /// Pay with an alternate token by swapping via the configured DEX contract.
+ /// The resulting invoice token amount is credited to the invoice.
+ pub fn bridge_pay(
+ env: Env,
+ payer: Address,
+ invoice_id: u64,
+ source_token: Address,
+ source_amount: i128,
+ ) {
+ require_not_paused(&env);
+ payer.require_auth();
+
+ let mut invoice = load_invoice(&env, invoice_id);
+ assert!(invoice.status == InvoiceStatus::Pending, "invoice is not pending");
+ assert!(env.ledger().timestamp() <= invoice.deadline, "invoice deadline has passed");
+ assert!(source_amount > 0, "payment amount must be positive");
+
+ let invoice_token = invoice.tokens.get(0).expect("no token");
+ let src_client = token::Client::new(&env, &source_token);
+ src_client.transfer(&payer, &env.current_contract_address(), &source_amount);
+
+ let dex: Address = env
+ .storage()
+ .persistent()
+ .get(&soroban_sdk::symbol_short!("dex_ctr"))
+ .expect("dex contract not set");
+ let mut args: Vec = Vec::new(&env);
+ args.push_back(source_token.into_val(&env));
+ args.push_back(invoice_token.clone().into_val(&env));
+ args.push_back(source_amount.into_val(&env));
+ let converted: i128 = env.invoke_contract(&dex, &Symbol::new(&env, "swap"), args);
+
+ let total: i128 = invoice.amounts.iter().sum();
+ let remaining = total - invoice.funded;
+ assert!(converted <= remaining, "payment exceeds remaining balance");
+
+ invoice.payments.push_back(Payment { payer: payer.clone(), amount: converted, tip: 0 });
+ invoice.funded += converted;
+
+ append_audit_entry(&env, invoice_id, symbol_short!("bridge_pay"), &payer);
+ events::payment_received(&env, invoice_id, &payer, converted);
+ notify_invoice(&env, invoice_id, symbol_short!("pay"), &invoice.notification_contract);
if invoice.funded >= total {
let in_group = env.storage().persistent().has(&invoice_group_key(invoice_id));
@@ -2002,6 +2113,7 @@ impl SplitContract {
}
append_audit_entry(env, invoice_id, symbol_short!("release"), actor);
events::invoice_released(env, invoice_id, &invoice.recipients);
+ notify_invoice(env, invoice_id, symbol_short!("release"), &invoice.notification_contract);
}
save_invoice(env, invoice_id, invoice);
@@ -2126,6 +2238,7 @@ impl SplitContract {
}
append_audit_entry(&env, invoice_id, symbol_short!("stg_rel"), &creator);
events::invoice_released(&env, invoice_id, &invoice.recipients);
+ notify_invoice(&env, invoice_id, symbol_short!("release"), &invoice.notification_contract);
} else {
append_audit_entry(&env, invoice_id, symbol_short!("stg_rel"), &creator);
}
@@ -2480,6 +2593,7 @@ impl SplitContract {
save_invoice(env, invoice_id, invoice);
append_audit_entry(env, invoice_id, symbol_short!("release"), actor);
events::invoice_released(env, invoice_id, &invoice.recipients);
+ notify_invoice(env, invoice_id, symbol_short!("release"), &invoice.notification_contract);
// Increment total_volume and total_released counters (issue #28).
let total_volume: i128 = env
@@ -2631,6 +2745,7 @@ impl SplitContract {
let actor = env.current_contract_address();
append_audit_entry(&env, invoice_id, symbol_short!("auto_ref"), &actor);
events::invoice_refunded(&env, invoice_id);
+ notify_invoice(&env, invoice_id, symbol_short!("refund"), &invoice.notification_contract);
let total_refunded: i128 = env
.storage()
.persistent()
@@ -2711,6 +2826,7 @@ impl SplitContract {
let actor = env.current_contract_address();
append_audit_entry(&env, invoice_id, symbol_short!("refund"), &actor);
events::invoice_refunded(&env, invoice_id);
+ notify_invoice(&env, invoice_id, symbol_short!("refund"), &invoice.notification_contract);
// Increment total_refunded counter (issue #28).
let total_refunded: i128 = env
@@ -3079,6 +3195,8 @@ impl SplitContract {
old_invoice.tax_authority.clone(),
old_invoice.insurance_premium_bps,
old_invoice.smart_route,
+ old_invoice.notification_contract.clone(),
+ old_invoice.overflow_behavior.clone(),
old_invoice.convert_to_stream,
old_invoice.accepted_tokens.clone(),
old_invoice.forward_to,
@@ -3744,4 +3862,78 @@ impl SplitContract {
.persistent()
.get(&delegate_key(invoice_id))
}
+
+ /// Authorise an address to pay on behalf of the beneficiary.
+ /// Requires beneficiary auth.
+ pub fn authorise_delegate(env: Env, beneficiary: Address, delegate: Address) {
+ require_not_paused(&env);
+ beneficiary.require_auth();
+
+ let mut delegates: Vec = env
+ .storage()
+ .persistent()
+ .get(&delegate_pay_key(&beneficiary))
+ .unwrap_or_else(|| Vec::new(&env));
+
+ if !delegates.iter().any(|d| d == delegate) {
+ delegates.push_back(delegate.clone());
+ env.storage().persistent().set(&delegate_pay_key(&beneficiary), &delegates);
+ }
+ }
+
+ /// Pay toward an invoice using an authorised delegate.
+ /// The invoice records the beneficiary as the payer.
+ pub fn delegate_pay(
+ env: Env,
+ delegate: Address,
+ beneficiary: Address,
+ invoice_id: u64,
+ amount: i128,
+ ) {
+ require_not_paused(&env);
+ delegate.require_auth();
+
+ let delegates: Vec = env
+ .storage()
+ .persistent()
+ .get(&delegate_pay_key(&beneficiary))
+ .unwrap_or_else(|| Vec::new(&env));
+ assert!(delegates.iter().any(|d| d == delegate), "not authorised");
+
+ let mut invoice = load_invoice(&env, invoice_id);
+ assert!(invoice.status == InvoiceStatus::Pending, "invoice is not pending");
+ assert!(env.ledger().timestamp() <= invoice.deadline, "invoice deadline has passed");
+ assert!(amount > 0, "payment amount must be positive");
+
+ let total: i128 = invoice.amounts.iter().sum();
+ let remaining = total - invoice.funded;
+ assert!(amount <= remaining, "payment exceeds remaining balance");
+
+ let token_client = token::Client::new(&env, &invoice.tokens.get(0).expect("no token"));
+ token_client.transfer(&delegate, &env.current_contract_address(), &amount);
+
+ invoice.payments.push_back(Payment { payer: beneficiary.clone(), amount, tip: 0 });
+ invoice.funded += amount;
+
+ append_audit_entry(&env, invoice_id, symbol_short!("del_pay"), &delegate);
+ events::payment_received(&env, invoice_id, &beneficiary, amount);
+ notify_invoice(&env, invoice_id, symbol_short!("pay"), &invoice.notification_contract);
+
+ let in_group = env.storage().persistent().has(&invoice_group_key(invoice_id));
+ let guarded =
+ invoice.prerequisite_id.is_some()
+ || !invoice.tranches.is_empty()
+ || !invoice.release_stages.is_empty()
+ || in_group
+ || !invoice.co_signers.is_empty();
+ if invoice.funded >= total {
+ if guarded {
+ save_invoice(&env, invoice_id, &invoice);
+ } else {
+ Self::_release(&env, invoice_id, &mut invoice, &delegate);
+ }
+ } else {
+ save_invoice(&env, invoice_id, &invoice);
+ }
+ }
}
diff --git a/contracts/split/src/test.rs b/contracts/split/src/test.rs
index 86caa1e..740d473 100644
--- a/contracts/split/src/test.rs
+++ b/contracts/split/src/test.rs
@@ -4,7 +4,7 @@ use super::*;
use soroban_sdk::{
testutils::{Address as _, Ledger},
token::{Client as TokenClient, StellarAssetClient},
- Address, Env, Vec,
+ Address, Env, Symbol, Vec,
};
use types::InvoiceOptions;
@@ -58,6 +58,8 @@ fn default_options(env: &Env) -> InvoiceOptions {
tax_authority: None,
insurance_premium_bps: None,
smart_route: None,
+ notification_contract: None,
+ overflow_behavior: types::OverflowBehavior::Reject,
convert_to_stream: false,
accepted_tokens: Vec::new(env),
forward_to: None,
@@ -855,7 +857,7 @@ fn test_pause_blocks_pay() {
env.ledger().set_timestamp(1_000);
let treasury = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999);
c.pause(&admin);
@@ -876,7 +878,7 @@ fn test_unpause_restores_pay() {
env.ledger().set_timestamp(1_000);
let treasury = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999);
let id = c.create_invoice(
@@ -908,7 +910,7 @@ fn test_allowed_payers_unlisted_address_rejected() {
env.ledger().set_timestamp(1_000);
let treasury = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999);
c.pause(&admin);
@@ -1587,7 +1589,7 @@ fn test_creation_fee_charged_to_treasury() {
env.ledger().set_timestamp(1_000);
- c.initialize(&admin, &50_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &50_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
assert_eq!(c.get_creation_fee(), 50);
assert_eq!(c.get_treasury(), treasury);
@@ -1618,7 +1620,7 @@ fn test_creation_fee_zero_by_default() {
env.ledger().set_timestamp(1_000);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
let id = make_invoice(&env, &c, &creator, &recipient, 200, &token_id, 9_999);
@@ -1636,7 +1638,7 @@ fn test_set_creation_fee_updates_fee() {
let admin = Address::generate(&env);
let treasury = Address::generate(&env);
- c.initialize(&admin, &10_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &10_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
assert_eq!(c.get_creation_fee(), 10);
c.set_creation_fee(&admin, &25_i128);
@@ -1652,7 +1654,7 @@ fn test_set_treasury_updates_treasury() {
let treasury1 = Address::generate(&env);
let treasury2 = Address::generate(&env);
- c.initialize(&admin, &10_i128, &treasury1, &token_id, &0_u32, &None);
+ c.initialize(&admin, &10_i128, &treasury1, &token_id, &0_u32, &None, &None, &0_u32);
assert_eq!(c.get_treasury(), treasury1);
c.set_treasury(&admin, &treasury2);
@@ -1675,7 +1677,7 @@ fn test_creation_fee_charged_per_invoice_in_batch() {
env.ledger().set_timestamp(1_000);
- c.initialize(&admin, &10_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &10_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
// create_batch creates 2 invoices, each should incur a 10 unit fee.
let mut recipients = Vec::new(&env);
@@ -2063,7 +2065,7 @@ fn test_platform_fee_bps_defaults_to_zero() {
let admin = Address::generate(&env);
let treasury = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
assert_eq!(c.get_platform_fee_bps(), 0);
}
@@ -2085,7 +2087,7 @@ fn test_platform_fee_bps_deducted_on_release() {
env.ledger().set_timestamp(1_000);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &1_000_u32, &None); // 10%
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &1_000_u32, &None, &None, &0_u32); // 10%
let id = make_invoice(&env, &c, &creator, &recipient, 500, &token_id, 9_999);
c.pay(&payer, &id, &500_i128, &0_u64, &false);
@@ -2116,7 +2118,7 @@ fn test_platform_fee_bps_multi_recipient() {
env.ledger().set_timestamp(1_000);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &500_u32, &None); // 5%
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &500_u32, &None, &None, &0_u32); // 5%
let mut recipients = Vec::new(&env);
recipients.push_back(r1.clone());
@@ -2158,7 +2160,7 @@ fn test_platform_fee_bps_with_tranches() {
env.ledger().set_timestamp(1_000);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &1_000_u32, &None); // 10%
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &1_000_u32, &None, &None, &0_u32); // 10%
let mut tranches = Vec::new(&env);
tranches.push_back(types::Tranche { timestamp: 1_500, basis_points: 5_000 });
@@ -3451,7 +3453,7 @@ fn test_governance_approval() {
let gov_id = env.register(MockGovernance, ());
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &Some(gov_id));
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &Some(gov_id), &0_u32);
env.ledger().set_timestamp(1_000);
@@ -3473,7 +3475,7 @@ fn test_governance_rejection() {
let gov_id = env.register(MockGovernance, ());
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &Some(gov_id));
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &Some(gov_id), &0_u32);
env.ledger().set_timestamp(1_000);
@@ -3561,7 +3563,7 @@ fn test_convert_to_stream_calls_stream_contract() {
let payer = Address::generate(&env);
let recipient = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
let stream_id = env.register(MockStream, ());
c.set_stream_contract(&admin, &stream_id);
@@ -3626,6 +3628,194 @@ impl MockDex {
}
}
+#[contract]
+struct MockNotification;
+
+#[contractimpl]
+impl MockNotification {
+ pub fn notify(env: Env, invoice_id: u64, event: Symbol) {
+ let key = (symbol_short!("notif"), invoice_id, event.clone());
+ env.storage().persistent().set(&key, &true);
+ }
+
+ pub fn was_notified(env: Env, invoice_id: u64, event: Symbol) -> bool {
+ let key = (symbol_short!("notif"), invoice_id, event.clone());
+ env.storage()
+ .persistent()
+ .get(&key)
+ .unwrap_or(false)
+ }
+}
+
+#[test]
+fn test_authorise_delegate_and_delegate_pay_records_beneficiary_as_payer() {
+ let (env, contract_id, token_id) = setup();
+ let c = client(&env, &contract_id);
+ let tk = token_client(&env, &token_id);
+
+ let creator = Address::generate(&env);
+ let beneficiary = Address::generate(&env);
+ let delegate = Address::generate(&env);
+ let recipient = Address::generate(&env);
+
+ StellarAssetClient::new(&env, &token_id).mint(&delegate, &200);
+ env.ledger().set_timestamp(1_000);
+
+ let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999);
+ c.authorise_delegate(&beneficiary, &delegate);
+ c.delegate_pay(&delegate, &beneficiary, &id, &100_i128);
+
+ let invoice = c.get_invoice(&id);
+ assert_eq!(invoice.funded, 100);
+ assert_eq!(invoice.payments.get(0).unwrap().payer, beneficiary);
+ assert_eq!(invoice.payments.get(0).unwrap().amount, 100);
+ assert_eq!(tk.balance(&recipient), 100);
+}
+
+#[test]
+#[should_panic(expected = "not authorised")]
+fn test_delegate_pay_unauthorised_panics() {
+ let (env, contract_id, token_id) = setup();
+ let c = client(&env, &contract_id);
+
+ let creator = Address::generate(&env);
+ let beneficiary = Address::generate(&env);
+ let unauthorized = Address::generate(&env);
+ let recipient = Address::generate(&env);
+
+ StellarAssetClient::new(&env, &token_id).mint(&unauthorized, &200);
+ env.ledger().set_timestamp(1_000);
+
+ let id = make_invoice(&env, &c, &creator, &recipient, 100, &token_id, 9_999);
+ c.delegate_pay(&unauthorized, &beneficiary, &id, &100_i128);
+}
+
+#[test]
+fn test_overflow_behavior_refund_accepts_excess() {
+ let (env, contract_id, token_id) = setup();
+ let c = client(&env, &contract_id);
+ let tk = token_client(&env, &token_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 mut recipients = Vec::new(&env);
+ recipients.push_back(recipient.clone());
+ let mut amounts = Vec::new(&env);
+ amounts.push_back(100_i128);
+
+ let mut opts = default_options(&env);
+ opts.overflow_behavior = types::OverflowBehavior::Refund;
+
+ let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64, &opts);
+ c.pay(&payer, &id, &200_i128, &0_u64, &false);
+
+ let invoice = c.get_invoice(&id);
+ assert_eq!(invoice.funded, 100);
+ assert_eq!(tk.balance(&payer), 100);
+}
+
+#[test]
+fn test_overflow_behavior_donate_sends_excess_to_treasury() {
+ let (env, contract_id, token_id) = setup();
+ let c = client(&env, &contract_id);
+ let tk = token_client(&env, &token_id);
+
+ let admin = Address::generate(&env);
+ let treasury = Address::generate(&env);
+ let creator = Address::generate(&env);
+ let payer = Address::generate(&env);
+ let recipient = Address::generate(&env);
+
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
+ StellarAssetClient::new(&env, &token_id).mint(&payer, &200);
+ env.ledger().set_timestamp(1_000);
+
+ let mut recipients = Vec::new(&env);
+ recipients.push_back(recipient.clone());
+ let mut amounts = Vec::new(&env);
+ amounts.push_back(100_i128);
+
+ let mut opts = default_options(&env);
+ opts.overflow_behavior = types::OverflowBehavior::Donate;
+
+ let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64, &opts);
+ c.pay(&payer, &id, &200_i128, &0_u64, &false);
+
+ let invoice = c.get_invoice(&id);
+ assert_eq!(invoice.funded, 100);
+ assert_eq!(tk.balance(&treasury), 100);
+}
+
+#[test]
+fn test_bridge_pay_credits_invoice_after_swap() {
+ let (env, contract_id, token_id) = setup();
+ let c = client(&env, &contract_id);
+
+ let admin = Address::generate(&env);
+ let creator = Address::generate(&env);
+ let payer = Address::generate(&env);
+ let recipient = Address::generate(&env);
+
+ let treasury = Address::generate(&env);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
+
+ let alt_token_admin = Address::generate(&env);
+ let alt_token_id = env
+ .register_stellar_asset_contract_v2(alt_token_admin.clone())
+ .address();
+ StellarAssetClient::new(&env, &alt_token_id).mint(&payer, &300);
+
+ let dex_id = env.register(MockDex, ());
+ c.set_dex_contract(&admin, &dex_id);
+
+ env.ledger().set_timestamp(1_000);
+ let id = make_invoice(&env, &c, &creator, &recipient, 300, &token_id, 9_999);
+
+ c.bridge_pay(&payer, &id, &alt_token_id, &300_i128);
+
+ let invoice = c.get_invoice(&id);
+ assert_eq!(invoice.funded, 300);
+}
+
+#[test]
+fn test_notification_contract_receives_pay_release_and_refund() {
+ 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);
+ let notifier_id = env.register(MockNotification, ());
+ let notifier = MockNotificationClient::new(&env, ¬ifier_id);
+
+ StellarAssetClient::new(&env, &token_id).mint(&payer, &200);
+ env.ledger().set_timestamp(1_000);
+
+ let mut recipients = Vec::new(&env);
+ recipients.push_back(recipient.clone());
+ let mut amounts = Vec::new(&env);
+ amounts.push_back(100_i128);
+
+ let mut opts = default_options(&env);
+ opts.notification_contract = Some(notifier_id.clone());
+
+ let id = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64, &opts);
+ c.pay(&payer, &id, &100_i128, &0_u64, &false);
+
+ assert!(notifier.was_notified(&id, &symbol_short!("pay")));
+ assert!(notifier.was_notified(&id, &symbol_short!("release")));
+
+ let id2 = c.create_invoice(&creator, &recipients, &amounts, &token_id, &9_999_u64, &opts);
+ env.ledger().set_timestamp(12_000);
+ c.refund(&id2);
+ assert!(notifier.was_notified(&id2, &symbol_short!("refund")));
+}
+
#[test]
fn test_pay_with_token_accepted_token_credited() {
let (env, contract_id, token_id) = setup();
@@ -3637,7 +3827,7 @@ fn test_pay_with_token_accepted_token_credited() {
let payer = Address::generate(&env);
let recipient = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
// Register alternate token and DEX.
let alt_token_admin = Address::generate(&env);
@@ -3778,7 +3968,7 @@ fn test_whitelist_empty_allows_any_creator() {
let creator = Address::generate(&env);
let recipient = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
env.ledger().set_timestamp(1_000);
// No whitelist set — any creator may create.
@@ -3798,7 +3988,7 @@ fn test_non_whitelisted_creator_rejected() {
let not_whitelisted = Address::generate(&env);
let recipient = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
c.whitelist_creator(&admin, &whitelisted);
env.ledger().set_timestamp(1_000);
@@ -3817,7 +4007,7 @@ fn test_whitelisted_creator_can_create() {
let creator = Address::generate(&env);
let recipient = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
c.whitelist_creator(&admin, &creator);
env.ledger().set_timestamp(1_000);
@@ -3836,7 +4026,7 @@ fn test_remove_creator_from_whitelist() {
let creator = Address::generate(&env);
let recipient = Address::generate(&env);
- c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None);
+ c.initialize(&admin, &0_i128, &treasury, &token_id, &0_u32, &None, &None, &0_u32);
c.whitelist_creator(&admin, &creator);
c.remove_creator(&admin, &creator);
diff --git a/contracts/split/src/types.rs b/contracts/split/src/types.rs
index c8aa14f..bb02438 100644
--- a/contracts/split/src/types.rs
+++ b/contracts/split/src/types.rs
@@ -29,6 +29,14 @@ pub struct ResolveRule {
pub action: ResolveAction,
}
+#[contracttype]
+#[derive(Clone, Debug, PartialEq)]
+pub enum OverflowBehavior {
+ Reject,
+ Refund,
+ Donate,
+}
+
/// Issue #: A single (invoice_id, amount) pair for pool_pay.
#[contracttype]
#[derive(Clone, Debug)]
@@ -174,6 +182,8 @@ pub struct InvoiceOptions {
pub tax_authority: Option,
pub insurance_premium_bps: Option,
pub smart_route: Option,
+ pub notification_contract: Option,
+ pub overflow_behavior: OverflowBehavior,
/// Issue #1: when true, _release() registers funds with the stream contract instead of direct transfer.
pub convert_to_stream: bool,
/// Issue #2: tokens accepted in pay_with_token(); base token is always accepted implicitly.