@@ -66,6 +66,23 @@ fn group_key(group_id: u64) -> (Symbol, u64) {
6666fn invoice_group_key ( invoice_id : u64 ) -> ( Symbol , u64 ) {
6767 ( symbol_short ! ( "invgrp" ) , invoice_id)
6868}
69+
70+ fn invoice_treasury_key ( invoice_id : u64 ) -> ( Symbol , u64 ) {
71+ ( symbol_short ! ( "inv_tr" ) , invoice_id)
72+ }
73+
74+ fn treasury_group_counter_key ( ) -> Symbol {
75+ symbol_short ! ( "grp_tr_cnt" )
76+ }
77+
78+ fn reminder_key ( invoice_id : u64 , address : & Address ) -> ( Symbol , u64 , Address ) {
79+ ( symbol_short ! ( "rem" ) , invoice_id, address. clone ( ) )
80+ }
81+
82+ fn group_treasury_key ( group_id : u64 ) -> ( Symbol , u64 ) {
83+ ( symbol_short ! ( "grp_tr" ) , group_id)
84+ }
85+
6986fn template_key ( creator : & Address , name : & Symbol ) -> ( Symbol , Address , Symbol ) {
7087 ( symbol_short ! ( "tmpl" ) , creator. clone ( ) , name. clone ( ) )
7188}
@@ -283,6 +300,26 @@ fn group_all_funded(env: &Env, group_id: u64) -> bool {
283300 true
284301}
285302
303+ fn treasury_record_for_invoice ( env : & Env , invoice_id : u64 ) -> Option < ( u64 , TreasuryRecord ) > {
304+ if let Some ( group_id) = env
305+ . storage ( )
306+ . persistent ( )
307+ . get :: < ( Symbol , u64 ) , u64 > ( & invoice_treasury_key ( invoice_id) )
308+ {
309+ if let Some ( record) = env. storage ( ) . persistent ( ) . get ( & group_treasury_key ( group_id) ) {
310+ return Some ( ( group_id, record) ) ;
311+ }
312+ }
313+ None
314+ }
315+
316+ fn load_treasury_record ( env : & Env , group_id : u64 ) -> TreasuryRecord {
317+ env. storage ( )
318+ . persistent ( )
319+ . get ( & group_treasury_key ( group_id) )
320+ . expect ( "treasury record not found" )
321+ }
322+
286323// ---------------------------------------------------------------------------
287324// Contract
288325// ---------------------------------------------------------------------------
@@ -581,6 +618,8 @@ impl SplitContract {
581618 options. velocity_window ,
582619 options. split_rules ,
583620 options. auto_resolve_rules ,
621+ options. oracle_address ,
622+ options. cross_chain_ref ,
584623 )
585624 }
586625
@@ -606,6 +645,7 @@ impl SplitContract {
606645 release_stages : Vec < u32 > ,
607646 price_oracle : Option < Address > ,
608647 swap_tokens : Vec < Option < Address > > ,
648+ oracle_address : Option < Address > ,
609649 tax_bps : u32 ,
610650 tax_authority : Option < Address > ,
611651 insurance_premium_bps : u32 ,
@@ -619,6 +659,7 @@ impl SplitContract {
619659 velocity_window : u64 ,
620660 split_rules : Vec < SplitRule > ,
621661 auto_resolve_rules : Vec < ResolveRule > ,
662+ cross_chain_ref : Option < String > ,
622663 ) -> u64 {
623664 assert ! (
624665 recipients. len( ) == amounts. len( ) ,
@@ -824,6 +865,8 @@ impl SplitContract {
824865 tax_authority,
825866 insurance_premium_bps,
826867 insurance_fund : 0 ,
868+ oracle_address,
869+ condition_met : false ,
827870 smart_route,
828871 convert_to_stream,
829872 accepted_tokens,
@@ -953,6 +996,9 @@ impl SplitContract {
953996 Vec :: new ( & env) ,
954997 Vec :: new ( & env) ,
955998 Vec :: new ( & env) ,
999+ None ,
1000+ None ,
1001+ None ,
9561002 ) ;
9571003 ids. push_back ( id) ;
9581004 }
@@ -1014,6 +1060,9 @@ impl SplitContract {
10141060 0 ,
10151061 Vec :: new ( & env) ,
10161062 Vec :: new ( & env) ,
1063+ None ,
1064+ None ,
1065+ None ,
10171066 ) ;
10181067
10191068 if months > 1 {
@@ -1145,7 +1194,8 @@ impl SplitContract {
11451194 || !invoice. tranches . is_empty ( )
11461195 || !invoice. release_stages . is_empty ( )
11471196 || in_group
1148- || !invoice. co_signers . is_empty ( ) ;
1197+ || !invoice. co_signers . is_empty ( )
1198+ || ( invoice. oracle_address . is_some ( ) && !invoice. condition_met ) ;
11491199 if guarded {
11501200 save_invoice ( & env, invoice_id, & invoice) ;
11511201 } else {
@@ -1362,7 +1412,8 @@ impl SplitContract {
13621412 || !invoice. tranches . is_empty ( )
13631413 || !invoice. release_stages . is_empty ( )
13641414 || in_group
1365- || !invoice. co_signers . is_empty ( ) ;
1415+ || !invoice. co_signers . is_empty ( )
1416+ || ( invoice. oracle_address . is_some ( ) && !invoice. condition_met ) ;
13661417 if guarded {
13671418 save_invoice ( env, invoice_id, & invoice) ;
13681419 } else {
@@ -1460,7 +1511,8 @@ impl SplitContract {
14601511 || !invoice. tranches . is_empty ( )
14611512 || !invoice. release_stages . is_empty ( )
14621513 || in_group
1463- || !invoice. co_signers . is_empty ( ) ;
1514+ || !invoice. co_signers . is_empty ( )
1515+ || ( invoice. oracle_address . is_some ( ) && !invoice. condition_met ) ;
14641516 if guarded {
14651517 save_invoice ( & env, invoice_id, & invoice) ;
14661518 } else {
@@ -1535,7 +1587,8 @@ impl SplitContract {
15351587 || !inv. tranches . is_empty ( )
15361588 || !inv. release_stages . is_empty ( )
15371589 || in_group
1538- || !inv. co_signers . is_empty ( ) ;
1590+ || !inv. co_signers . is_empty ( )
1591+ || ( inv. oracle_address . is_some ( ) && !inv. condition_met ) ;
15391592 if guarded {
15401593 save_invoice ( & env, p. invoice_id , & inv) ;
15411594 } else {
@@ -1687,6 +1740,75 @@ impl SplitContract {
16871740 append_audit_entry ( & env, invoice_id, symbol_short ! ( "aprv" ) , approver) ;
16881741 }
16891742
1743+ /// Oracle confirms a condition for a gated invoice.
1744+ /// Requires the configured oracle address to authenticate.
1745+ pub fn confirm_condition ( env : Env , invoice_id : u64 ) {
1746+ require_not_paused ( & env) ;
1747+ let mut invoice = load_invoice ( & env, invoice_id) ;
1748+ let oracle = invoice. oracle_address . as_ref ( ) . expect ( "no oracle set for invoice" ) ;
1749+ oracle. require_auth ( ) ;
1750+ invoice. condition_met = true ;
1751+ save_invoice ( & env, invoice_id, & invoice) ;
1752+ append_audit_entry ( & env, invoice_id, symbol_short ! ( "oracle_ok" ) , oracle) ;
1753+ }
1754+
1755+ /// Set a payment reminder for an address on a specific invoice.
1756+ /// The `who` address must authenticate.
1757+ pub fn set_reminder ( env : Env , who : Address , invoice_id : u64 , remind_at : u64 ) {
1758+ require_not_paused ( & env) ;
1759+ who. require_auth ( ) ;
1760+ env. storage ( )
1761+ . persistent ( )
1762+ . set ( & reminder_key ( invoice_id, & who) , & remind_at) ;
1763+ append_audit_entry ( & env, invoice_id, symbol_short ! ( "set_rmd" ) , & who) ;
1764+ }
1765+
1766+ /// Trigger a previously set reminder; must be called at or after `remind_at`.
1767+ pub fn trigger_reminder ( env : Env , invoice_id : u64 , who : Address ) {
1768+ require_not_paused ( & env) ;
1769+ let remind_at: u64 = env
1770+ . storage ( )
1771+ . persistent ( )
1772+ . get ( & reminder_key ( invoice_id, & who) )
1773+ . expect ( "reminder not set" ) ;
1774+ assert ! ( env. ledger( ) . timestamp( ) >= remind_at, "reminder not due" ) ;
1775+ events:: payment_reminder ( & env, invoice_id, & who) ;
1776+ env. storage ( ) . persistent ( ) . remove ( & reminder_key ( invoice_id, & who) ) ;
1777+ append_audit_entry ( & env, invoice_id, symbol_short ! ( "trig_rmd" ) , & who) ;
1778+ }
1779+
1780+ /// Create a treasury group linking multiple invoice IDs to a single treasury address.
1781+ /// Returns the new group id.
1782+ pub fn group_treasury_create ( env : Env , creator : Address , invoice_ids : Vec < u64 > , treasury : Address ) -> u64 {
1783+ require_not_paused ( & env) ;
1784+ creator. require_auth ( ) ;
1785+ let id: u64 = env
1786+ . storage ( )
1787+ . persistent ( )
1788+ . get ( & treasury_group_counter_key ( ) )
1789+ . unwrap_or ( 0u64 )
1790+ + 1 ;
1791+ env. storage ( ) . persistent ( ) . set ( & treasury_group_counter_key ( ) , & id) ;
1792+ let record = types:: TreasuryRecord { invoice_ids : invoice_ids. clone ( ) , treasury : treasury. clone ( ) } ;
1793+ env. storage ( ) . persistent ( ) . set ( & group_treasury_key ( id) , & record) ;
1794+ for iid in invoice_ids. iter ( ) {
1795+ env. storage ( ) . persistent ( ) . set ( & invoice_treasury_key ( * iid) , & id) ;
1796+ append_audit_entry ( & env, * iid, symbol_short ! ( "grp_tr" ) , & creator) ;
1797+ }
1798+ id
1799+ }
1800+
1801+ /// Pay toward an invoice using a memo that encodes the invoice id.
1802+ /// Requires payer auth and emits a payment_matched event on success.
1803+ pub fn pay_with_memo ( env : Env , payer : Address , memo : u64 , amount : i128 , nonce : u64 , auto_convert : bool ) {
1804+ require_not_paused ( & env) ;
1805+ payer. require_auth ( ) ;
1806+ // Validate memo corresponds to an existing invoice.
1807+ let _ = load_invoice ( & env, memo) ;
1808+ Self :: _pay ( & env, & payer, memo, amount, nonce, auto_convert) ;
1809+ events:: payment_matched ( & env, memo, memo, & payer) ;
1810+ }
1811+
16901812 /// Claim vesting cliff share after cliff timestamp has passed (issue #27).
16911813 ///
16921814 /// Requires that the invoice status is Released and the cliff (if set) has passed.
@@ -2078,6 +2200,7 @@ impl SplitContract {
20782200 let mut distributed: i128 = 0 ;
20792201 let mut total_fee: i128 = 0 ;
20802202 let mut total_tax: i128 = 0 ;
2203+ let mut payouts: Vec < i128 > = Vec :: new ( env) ;
20812204 for i in 0 ..n {
20822205 let recipient = invoice. recipients . get ( i) . unwrap ( ) ;
20832206 let amount = invoice. amounts . get ( i) . unwrap ( ) ;
@@ -2106,43 +2229,91 @@ impl SplitContract {
21062229 distributed += proportional;
21072230
21082231 let tax = ( proportional as u128 * invoice. tax_bps as u128 / 10_000u128 ) as i128 ;
2109- let post_tax = proportional - tax;
21102232 total_tax += tax;
2233+ let post_tax = proportional - tax;
21112234
21122235 let fee = ( post_tax as u128 * platform_fee_bps as u128 / 10_000u128 ) as i128 ;
2113- let payout = post_tax - fee;
21142236 total_fee += fee;
21152237
2116- // Issue #41: if a swap token is configured for this recipient, invoke DEX swap.
2117- let swap_token: Option < Address > = invoice
2118- . swap_tokens
2119- . get ( i as u32 )
2120- . unwrap_or ( None ) ;
2121- if let Some ( ref out_token) = swap_token {
2122- let from_token = invoice. tokens . get ( 0 ) . expect ( "no token" ) ;
2123- let mut args: Vec < Val > = Vec :: new ( env) ;
2124- args. push_back ( from_token. into_val ( env) ) ;
2125- args. push_back ( out_token. clone ( ) . into_val ( env) ) ;
2126- args. push_back ( payout. into_val ( env) ) ;
2127- args. push_back ( recipient. into_val ( env) ) ;
2128- let _swapped: i128 = env. invoke_contract ( out_token, & Symbol :: new ( env, "swap" ) , args) ;
2129- } else if invoice. smart_route {
2130- // Smart routing: query DEX router for optimal path, fall back to direct transfer.
2131- let from_token = invoice. tokens . get ( 0 ) . expect ( "no token" ) ;
2132- let mut route_args: Vec < Val > = Vec :: new ( env) ;
2133- route_args. push_back ( from_token. into_val ( env) ) ;
2134- route_args. push_back ( payout. into_val ( env) ) ;
2135- route_args. push_back ( recipient. clone ( ) . into_val ( env) ) ;
2136- // Try DEX path-finding via invoke; on failure fall back to direct transfer.
2137- // In production the router address would be stored; here we attempt invoke
2138- // and catch failure by falling back.
2139- token_client. transfer ( & env. current_contract_address ( ) , & recipient, & payout) ;
2140- } else {
2141- let routed = Self :: execute_smart_route ( env, invoice, & recipient, payout) ;
2142- if !routed {
2238+ payouts. push_back ( proportional) ;
2239+ }
2240+
2241+ // If this invoice belongs to a treasury group, route the net payouts to the group's treasury address.
2242+ if let Some ( ( _group_id, record) ) = treasury_record_for_invoice ( env, invoice_id) {
2243+ // Transfer taxes first.
2244+ if total_tax > 0 {
2245+ if let Some ( ref auth) = invoice. tax_authority {
2246+ token_client. transfer ( & env. current_contract_address ( ) , auth, & total_tax) ;
2247+ }
2248+ }
2249+
2250+ // Transfer platform fee to global treasury.
2251+ if total_fee > 0 {
2252+ let treasury: Address = env
2253+ . storage ( )
2254+ . instance ( )
2255+ . get ( & treasury_key ( ) )
2256+ . expect ( "treasury not set" ) ;
2257+ token_client. transfer ( & env. current_contract_address ( ) , & treasury, & total_fee) ;
2258+ }
2259+
2260+ let net = distributed - total_tax - total_fee;
2261+ if net > 0 {
2262+ token_client. transfer ( & env. current_contract_address ( ) , & record. treasury , & net) ;
2263+ }
2264+ } else {
2265+ // Default behavior: transfer to each recipient (or route via DEX/router as configured).
2266+ for i in 0 ..n {
2267+ let recipient = invoice. recipients . get ( i) . unwrap ( ) ;
2268+ let proportional = payouts. get ( i as u32 ) . unwrap ( ) ;
2269+
2270+ let tax = ( proportional as u128 * invoice. tax_bps as u128 / 10_000u128 ) as i128 ;
2271+ let post_tax = proportional - tax;
2272+ let fee = ( post_tax as u128 * platform_fee_bps as u128 / 10_000u128 ) as i128 ;
2273+ let payout = post_tax - fee;
2274+
2275+ // Issue #41: if a swap token is configured for this recipient, invoke DEX swap.
2276+ let swap_token: Option < Address > = invoice
2277+ . swap_tokens
2278+ . get ( i as u32 )
2279+ . unwrap_or ( None ) ;
2280+ if let Some ( ref out_token) = swap_token {
2281+ let from_token = invoice. tokens . get ( 0 ) . expect ( "no token" ) ;
2282+ let mut args: Vec < Val > = Vec :: new ( env) ;
2283+ args. push_back ( from_token. into_val ( env) ) ;
2284+ args. push_back ( out_token. clone ( ) . into_val ( env) ) ;
2285+ args. push_back ( payout. into_val ( env) ) ;
2286+ args. push_back ( recipient. into_val ( env) ) ;
2287+ let _swapped: i128 = env. invoke_contract ( out_token, & Symbol :: new ( env, "swap" ) , args) ;
2288+ } else if invoice. smart_route {
2289+ let from_token = invoice. tokens . get ( 0 ) . expect ( "no token" ) ;
2290+ let mut route_args: Vec < Val > = Vec :: new ( env) ;
2291+ route_args. push_back ( from_token. into_val ( env) ) ;
2292+ route_args. push_back ( payout. into_val ( env) ) ;
2293+ route_args. push_back ( recipient. clone ( ) . into_val ( env) ) ;
21432294 token_client. transfer ( & env. current_contract_address ( ) , & recipient, & payout) ;
2295+ } else {
2296+ let routed = Self :: execute_smart_route ( env, invoice, & recipient, payout) ;
2297+ if !routed {
2298+ token_client. transfer ( & env. current_contract_address ( ) , & recipient, & payout) ;
2299+ }
2300+ }
2301+ }
2302+
2303+ if total_tax > 0 {
2304+ if let Some ( ref auth) = invoice. tax_authority {
2305+ token_client. transfer ( & env. current_contract_address ( ) , auth, & total_tax) ;
21442306 }
21452307 }
2308+
2309+ if total_fee > 0 {
2310+ let treasury: Address = env
2311+ . storage ( )
2312+ . instance ( )
2313+ . get ( & treasury_key ( ) )
2314+ . expect ( "treasury not set" ) ;
2315+ token_client. transfer ( & env. current_contract_address ( ) , & treasury, & total_fee) ;
2316+ }
21462317 }
21472318
21482319 if total_tax > 0 {
@@ -2394,6 +2565,8 @@ impl SplitContract {
23942565 Vec :: new ( env) ,
23952566 Vec :: new ( env) ,
23962567 Vec :: new ( env) ,
2568+ None ,
2569+ None ,
23972570 ) ;
23982571 env. storage ( )
23992572 . persistent ( )
0 commit comments