@@ -41,6 +41,22 @@ fn paused_key() -> Symbol {
4141fn paused_fns_key ( ) -> Symbol {
4242 symbol_short ! ( "ps_fns" )
4343}
44+
45+ fn pause_exempt_key ( address : & Address ) -> ( Symbol , Address ) {
46+ ( symbol_short ! ( "p_exempt" ) , address. clone ( ) )
47+ }
48+
49+ fn global_payer_limit_key ( ) -> Symbol {
50+ symbol_short ! ( "g_vel_lim" )
51+ }
52+
53+ fn global_payer_window_key ( ) -> Symbol {
54+ symbol_short ! ( "g_vel_win" )
55+ }
56+
57+ fn global_vel_key ( payer : & Address ) -> ( Symbol , Address ) {
58+ ( symbol_short ! ( "g_vel" ) , payer. clone ( ) )
59+ }
4460fn creation_fee_key ( ) -> Symbol {
4561 symbol_short ! ( "crt_fee" )
4662}
@@ -708,6 +724,25 @@ impl SplitContract {
708724 env. storage ( ) . persistent ( ) . set ( & paused_fns_key ( ) , & new_list) ;
709725 }
710726
727+ /// Set an address as exempt from the global pause for invoice creation.
728+ /// Requires admin auth.
729+ pub fn set_pause_exempt ( env : Env , admin : Address , address : Address , exempt : bool ) {
730+ require_role ( & env, & admin, AdminRole :: Operator ) ;
731+ if exempt {
732+ env. storage ( ) . persistent ( ) . set ( & pause_exempt_key ( & address) , & true ) ;
733+ } else {
734+ env. storage ( ) . persistent ( ) . remove ( & pause_exempt_key ( & address) ) ;
735+ }
736+ }
737+
738+ /// Set the global payer aggregate limit and window. Requires admin auth.
739+ pub fn set_global_payer_limit ( env : Env , admin : Address , limit : i128 , window_secs : u64 ) {
740+ require_role ( & env, & admin, AdminRole :: Operator ) ;
741+ assert ! ( limit >= 0 , "limit must be non-negative" ) ;
742+ env. storage ( ) . persistent ( ) . set ( & global_payer_limit_key ( ) , & limit) ;
743+ env. storage ( ) . persistent ( ) . set ( & global_payer_window_key ( ) , & window_secs) ;
744+ }
745+
711746 /// Update the creation fee. Requires admin auth.
712747 pub fn set_creation_fee ( env : Env , admin : Address , creation_fee : i128 ) {
713748 require_role ( & env, & admin, AdminRole :: Operator ) ;
@@ -1331,7 +1366,12 @@ impl SplitContract {
13311366 deadline : u64 ,
13321367 options : InvoiceOptions ,
13331368 ) -> u64 {
1334- require_not_paused ( & env) ;
1369+ // Check if contract is paused, but allow exempt creators
1370+ let is_paused = is_paused ( & env) ;
1371+ let is_exempt = env. storage ( ) . persistent ( ) . get :: < _ , bool > ( & pause_exempt_key ( & creator) ) . unwrap_or ( false ) ;
1372+ if is_paused && !is_exempt {
1373+ panic ! ( "contract is paused" ) ;
1374+ }
13351375 creator. require_auth ( ) ;
13361376 Self :: _apply_rate_limit ( & env, & creator) ;
13371377
@@ -1400,6 +1440,7 @@ impl SplitContract {
14001440 options. refund_grace_secs ,
14011441 options. priorities ,
14021442 options. require_kyc ,
1443+ options. scheduled_release_at ,
14031444 )
14041445 }
14051446
@@ -1449,6 +1490,7 @@ impl SplitContract {
14491490 refund_grace_secs : Option < u64 > ,
14501491 priorities : Vec < u32 > ,
14511492 require_kyc : bool ,
1493+ scheduled_release_at : Option < u64 > ,
14521494 ) -> u64 {
14531495 assert ! (
14541496 recipients. len( ) == amounts. len( ) ,
@@ -1742,6 +1784,7 @@ impl SplitContract {
17421784 payment_cooldown_secs,
17431785 max_payments_per_window,
17441786 payment_window_secs,
1787+ scheduled_release_at,
17451788 refund_grace_secs,
17461789 cross_chain_ref,
17471790 require_kyc,
@@ -2176,7 +2219,7 @@ impl SplitContract {
21762219 let mut new_payments: Vec < Payment > = Vec :: new ( & env) ;
21772220 for ( payer, amount) in payer_amounts. iter ( ) {
21782221 let tip = payer_tips. get ( payer. clone ( ) ) . unwrap_or ( 0 ) ;
2179- new_payments. push_back ( Payment { payer, amount, tip, donate_on_failure : false } ) ;
2222+ new_payments. push_back ( Payment { payer, amount, tip, attestation_hash : None } ) ;
21802223 }
21812224
21822225 // Verify total funded is unchanged (optional assertion, as asked by Acceptance Criteria)
@@ -2259,7 +2302,7 @@ impl SplitContract {
22592302 . persistent ( )
22602303 . get :: < ( Symbol , u64 , u64 ) , Vec < Payment > > ( & pay_shard_key ( invoice_id, shard_id) )
22612304 . unwrap_or_else ( || Vec :: new ( & env) ) ;
2262- shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : net_paid, tip : 0 , donate_on_failure : false } ) ;
2305+ shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : net_paid, tip : 0 , attestation_hash : None } ) ;
22632306 env. storage ( ) . persistent ( ) . set ( & pay_shard_key ( invoice_id, shard_id) , & shard_payments) ;
22642307
22652308 invoice. funded += net_paid;
@@ -2300,10 +2343,22 @@ impl SplitContract {
23002343 pub fn pay ( env : Env , payer : Address , invoice_id : u64 , amount : i128 , nonce : u64 , _auto_convert : bool , donate_on_failure : bool ) {
23012344 require_fn_not_paused ( & env, & symbol_short ! ( "pay" ) ) ;
23022345 payer. require_auth ( ) ;
2303- Self :: _pay ( & env, & payer, invoice_id, amount, nonce, _auto_convert, donate_on_failure ) ;
2346+ Self :: _pay ( & env, & payer, invoice_id, amount, nonce, _auto_convert, None ) ;
23042347 }
23052348
2306- fn _pay ( env : & Env , payer : & Address , invoice_id : u64 , amount : i128 , nonce : u64 , _auto_convert : bool , donate_on_failure : bool ) {
2349+ /// Pay with a signed attestation binding the payment to an off-chain identity
2350+ pub fn pay_with_attestation ( env : Env , payer : Address , invoice_id : u64 , amount : i128 , nonce : u64 , attestation_hash : BytesN < 32 > , signature : BytesN < 64 > , signer_pubkey : BytesN < 32 > , _auto_convert : bool ) {
2351+ require_fn_not_paused ( & env, & symbol_short ! ( "pay" ) ) ;
2352+ payer. require_auth ( ) ;
2353+
2354+ // Verify ed25519 signature over attestation_hash
2355+ env. crypto ( ) . ed25519_verify ( & signer_pubkey, & attestation_hash, & signature) ;
2356+
2357+ // Proceed with payment, storing the attestation hash
2358+ Self :: _pay ( & env, & payer, invoice_id, amount, nonce, _auto_convert, Some ( attestation_hash) ) ;
2359+ }
2360+
2361+ fn _pay ( env : & Env , payer : & Address , invoice_id : u64 , amount : i128 , nonce : u64 , _auto_convert : bool , attestation_hash : Option < BytesN < 32 > > ) {
23072362 let mut invoice = load_invoice ( env, invoice_id) ;
23082363
23092364 assert ! (
@@ -2418,6 +2473,26 @@ impl SplitContract {
24182473 env. storage ( ) . persistent ( ) . set ( & vel_key ( invoice_id, payer) , & window) ;
24192474 }
24202475
2476+ // Global cross-invoice velocity limiting per payer
2477+ let global_limit: i128 = env. storage ( ) . persistent ( ) . get ( & global_payer_limit_key ( ) ) . unwrap_or ( 0i128 ) ;
2478+ if global_limit > 0 {
2479+ let global_window_secs: u64 = env. storage ( ) . persistent ( ) . get ( & global_payer_window_key ( ) ) . unwrap_or ( 0u64 ) ;
2480+ let now = env. ledger ( ) . timestamp ( ) ;
2481+ let mut global_window: ( u64 , i128 ) = env
2482+ . storage ( )
2483+ . persistent ( )
2484+ . get ( & global_vel_key ( payer) )
2485+ . unwrap_or ( ( 0u64 , 0i128 ) ) ;
2486+ if now > global_window. 0 + global_window_secs {
2487+ // reset global window
2488+ global_window. 0 = now;
2489+ global_window. 1 = 0 ;
2490+ }
2491+ assert ! ( global_window. 1 + amount <= global_limit, "global payer limit exceeded" ) ;
2492+ global_window. 1 += amount;
2493+ env. storage ( ) . persistent ( ) . set ( & global_vel_key ( payer) , & global_window) ;
2494+ }
2495+
24212496 let token_client = token:: Client :: new ( env, & invoice. tokens . get ( 0 ) . expect ( "no token" ) ) ;
24222497
24232498 let credited_amount = match invoice. overflow_behavior {
@@ -2493,7 +2568,7 @@ impl SplitContract {
24932568 . persistent ( )
24942569 . get :: < ( Symbol , u64 , u64 ) , Vec < Payment > > ( & pay_shard_key ( invoice_id, shard_id) )
24952570 . unwrap_or_else ( || Vec :: new ( env) ) ;
2496- shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : credited_amount, tip : 0 , donate_on_failure } ) ;
2571+ shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : credited_amount, tip : 0 , attestation_hash } ) ;
24972572 env. storage ( ) . persistent ( ) . set ( & pay_shard_key ( invoice_id, shard_id) , & shard_payments) ;
24982573
24992574 invoice. funded += credited_amount;
@@ -2652,7 +2727,7 @@ impl SplitContract {
26522727 . persistent ( )
26532728 . get :: < ( Symbol , u64 , u64 ) , Vec < Payment > > ( & pay_shard_key ( invoice_id, shard_id) )
26542729 . unwrap_or_else ( || Vec :: new ( & env) ) ;
2655- shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : credited_amount, tip : 0 , donate_on_failure : false } ) ;
2730+ shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : credited_amount, tip : 0 , attestation_hash } ) ;
26562731 env. storage ( ) . persistent ( ) . set ( & pay_shard_key ( invoice_id, shard_id) , & shard_payments) ;
26572732
26582733 invoice. funded += credited_amount;
@@ -2724,7 +2799,7 @@ impl SplitContract {
27242799 . persistent ( )
27252800 . get :: < ( Symbol , u64 , u64 ) , Vec < Payment > > ( & pay_shard_key ( invoice_id, shard_id) )
27262801 . unwrap_or_else ( || Vec :: new ( & env) ) ;
2727- shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : converted, tip : 0 , donate_on_failure : false } ) ;
2802+ shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : converted, tip : 0 , attestation_hash : None } ) ;
27282803 env. storage ( ) . persistent ( ) . set ( & pay_shard_key ( invoice_id, shard_id) , & shard_payments) ;
27292804
27302805 invoice. funded += converted;
@@ -2811,7 +2886,7 @@ impl SplitContract {
28112886 . persistent ( )
28122887 . get :: < ( Symbol , u64 , u64 ) , Vec < Payment > > ( & pay_shard_key ( p. invoice_id , shard_id) )
28132888 . unwrap_or_else ( || Vec :: new ( & env) ) ;
2814- shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : p. amount , tip : 0 , donate_on_failure : false } ) ;
2889+ shard_payments. push_back ( Payment { payer : payer. clone ( ) , amount : p. amount , tip : 0 , attestation_hash : None } ) ;
28152890 env. storage ( ) . persistent ( ) . set ( & pay_shard_key ( p. invoice_id , shard_id) , & shard_payments) ;
28162891
28172892 inv. funded += p. amount ;
@@ -2952,6 +3027,48 @@ impl SplitContract {
29523027 Self :: _release ( & env, invoice_id, & mut invoice, & caller) ;
29533028 }
29543029
3030+ /// Trigger a scheduled release at the configured timestamp, respecting min_funding_bps
3031+ pub fn trigger_scheduled_release ( env : Env , invoice_id : u64 ) {
3032+ require_not_paused ( & env) ;
3033+ let mut invoice = load_invoice ( & env, invoice_id) ;
3034+
3035+ assert ! ( !invoice. frozen, "invoice is frozen" ) ;
3036+ assert ! ( !invoice. admin_frozen, "invoice frozen by admin" ) ;
3037+ assert ! ( invoice. status == InvoiceStatus :: Pending , "invoice is not pending" ) ;
3038+
3039+ let scheduled_at = invoice. scheduled_release_at . expect ( "no scheduled release time" ) ;
3040+ assert ! ( env. ledger( ) . timestamp( ) >= scheduled_at, "scheduled release time not reached" ) ;
3041+
3042+ // Check min funding requirement if set
3043+ if invoice. min_funding_bps > 0 {
3044+ let total: i128 = invoice. amounts . iter ( ) . sum ( ) ;
3045+ let min_required = ( total as u128 * invoice. min_funding_bps as u128 / 10_000u128 ) as i128 ;
3046+ assert ! ( invoice. funded >= min_required, "minimum funding not reached" ) ;
3047+ }
3048+
3049+ // Approval check (issue #25)
3050+ if invoice. approver . is_some ( ) && !invoice. approved {
3051+ panic ! ( "awaiting approval" ) ;
3052+ }
3053+
3054+ // Prerequisite check (issue #22)
3055+ if let Some ( prereq_id) = invoice. prerequisite_id {
3056+ let prereq = load_invoice ( & env, prereq_id) ;
3057+ assert ! ( prereq. status == InvoiceStatus :: Released , "prerequisite not released" ) ;
3058+ }
3059+
3060+ // Co-signer approval check
3061+ if !invoice. co_signers . is_empty ( ) {
3062+ assert ! (
3063+ invoice. signatures. len( ) >= invoice. required_signatures,
3064+ "not enough co-signer approvals"
3065+ ) ;
3066+ }
3067+
3068+ let caller = env. current_contract_address ( ) ;
3069+ Self :: _release ( & env, invoice_id, & mut invoice, & caller) ;
3070+ }
3071+
29553072 fn _release ( env : & Env , invoice_id : u64 , invoice : & mut Invoice , actor : & Address ) {
29563073 if invoice. tranches . is_empty ( ) {
29573074 Self :: _release_full ( env, invoice_id, invoice, actor) ;
@@ -3979,7 +4096,7 @@ impl SplitContract {
39794096 . persistent ( )
39804097 . get :: < ( Symbol , u64 , u64 ) , Vec < Payment > > ( & pay_shard_key ( target_id, shard_id) )
39814098 . unwrap_or_else ( || Vec :: new ( env) ) ;
3982- shard_payments. push_back ( Payment { payer : env. current_contract_address ( ) , amount : leftover, tip : 0 , donate_on_failure : false } ) ;
4099+ shard_payments. push_back ( Payment { payer : env. current_contract_address ( ) , amount : leftover, tip : 0 , attestation_hash : None } ) ;
39834100 env. storage ( ) . persistent ( ) . set ( & pay_shard_key ( target_id, shard_id) , & shard_payments) ;
39844101
39854102 target. funded += leftover;
0 commit comments