@@ -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}
@@ -253,6 +270,26 @@ fn group_all_funded(env: &Env, group_id: u64) -> bool {
253270 true
254271}
255272
273+ fn treasury_record_for_invoice ( env : & Env , invoice_id : u64 ) -> Option < ( u64 , TreasuryRecord ) > {
274+ if let Some ( group_id) = env
275+ . storage ( )
276+ . persistent ( )
277+ . get :: < ( Symbol , u64 ) , u64 > ( & invoice_treasury_key ( invoice_id) )
278+ {
279+ if let Some ( record) = env. storage ( ) . persistent ( ) . get ( & group_treasury_key ( group_id) ) {
280+ return Some ( ( group_id, record) ) ;
281+ }
282+ }
283+ None
284+ }
285+
286+ fn load_treasury_record ( env : & Env , group_id : u64 ) -> TreasuryRecord {
287+ env. storage ( )
288+ . persistent ( )
289+ . get ( & group_treasury_key ( group_id) )
290+ . expect ( "treasury record not found" )
291+ }
292+
256293// ---------------------------------------------------------------------------
257294// Contract
258295// ---------------------------------------------------------------------------
@@ -533,6 +570,8 @@ impl SplitContract {
533570 options. accepted_tokens ,
534571 options. split_rules ,
535572 options. auto_resolve_rules ,
573+ options. oracle_address ,
574+ options. cross_chain_ref ,
536575 )
537576 }
538577
@@ -558,6 +597,7 @@ impl SplitContract {
558597 release_stages : Vec < u32 > ,
559598 price_oracle : Option < Address > ,
560599 swap_tokens : Vec < Option < Address > > ,
600+ oracle_address : Option < Address > ,
561601 tax_bps : u32 ,
562602 tax_authority : Option < Address > ,
563603 insurance_premium_bps : u32 ,
@@ -566,6 +606,7 @@ impl SplitContract {
566606 accepted_tokens : Vec < Address > ,
567607 split_rules : Vec < SplitRule > ,
568608 auto_resolve_rules : Vec < ResolveRule > ,
609+ cross_chain_ref : Option < String > ,
569610 ) -> u64 {
570611 assert ! (
571612 recipients. len( ) == amounts. len( ) ,
@@ -767,11 +808,14 @@ impl SplitContract {
767808 tax_authority,
768809 insurance_premium_bps,
769810 insurance_fund : 0 ,
811+ oracle_address,
812+ condition_met : false ,
770813 smart_route,
771814 convert_to_stream,
772815 accepted_tokens,
773816 split_rules,
774817 auto_resolve_rules,
818+ cross_chain_ref,
775819 } ;
776820
777821 save_invoice ( env, id, & invoice) ;
@@ -854,6 +898,9 @@ impl SplitContract {
854898 Vec :: new ( & env) ,
855899 Vec :: new ( & env) ,
856900 Vec :: new ( & env) ,
901+ None ,
902+ None ,
903+ None ,
857904 ) ;
858905 ids. push_back ( id) ;
859906 }
@@ -911,6 +958,9 @@ impl SplitContract {
911958 Vec :: new ( & env) ,
912959 Vec :: new ( & env) ,
913960 Vec :: new ( & env) ,
961+ None ,
962+ None ,
963+ None ,
914964 ) ;
915965
916966 if months > 1 {
@@ -1042,7 +1092,8 @@ impl SplitContract {
10421092 || !invoice. tranches . is_empty ( )
10431093 || !invoice. release_stages . is_empty ( )
10441094 || in_group
1045- || !invoice. co_signers . is_empty ( ) ;
1095+ || !invoice. co_signers . is_empty ( )
1096+ || ( invoice. oracle_address . is_some ( ) && !invoice. condition_met ) ;
10461097 if guarded {
10471098 save_invoice ( & env, invoice_id, & invoice) ;
10481099 } else {
@@ -1208,7 +1259,8 @@ impl SplitContract {
12081259 || !invoice. tranches . is_empty ( )
12091260 || !invoice. release_stages . is_empty ( )
12101261 || in_group
1211- || !invoice. co_signers . is_empty ( ) ;
1262+ || !invoice. co_signers . is_empty ( )
1263+ || ( invoice. oracle_address . is_some ( ) && !invoice. condition_met ) ;
12121264 if guarded {
12131265 save_invoice ( env, invoice_id, & invoice) ;
12141266 } else {
@@ -1306,7 +1358,8 @@ impl SplitContract {
13061358 || !invoice. tranches . is_empty ( )
13071359 || !invoice. release_stages . is_empty ( )
13081360 || in_group
1309- || !invoice. co_signers . is_empty ( ) ;
1361+ || !invoice. co_signers . is_empty ( )
1362+ || ( invoice. oracle_address . is_some ( ) && !invoice. condition_met ) ;
13101363 if guarded {
13111364 save_invoice ( & env, invoice_id, & invoice) ;
13121365 } else {
@@ -1381,7 +1434,8 @@ impl SplitContract {
13811434 || !inv. tranches . is_empty ( )
13821435 || !inv. release_stages . is_empty ( )
13831436 || in_group
1384- || !inv. co_signers . is_empty ( ) ;
1437+ || !inv. co_signers . is_empty ( )
1438+ || ( inv. oracle_address . is_some ( ) && !inv. condition_met ) ;
13851439 if guarded {
13861440 save_invoice ( & env, p. invoice_id , & inv) ;
13871441 } else {
@@ -1533,6 +1587,75 @@ impl SplitContract {
15331587 append_audit_entry ( & env, invoice_id, symbol_short ! ( "aprv" ) , approver) ;
15341588 }
15351589
1590+ /// Oracle confirms a condition for a gated invoice.
1591+ /// Requires the configured oracle address to authenticate.
1592+ pub fn confirm_condition ( env : Env , invoice_id : u64 ) {
1593+ require_not_paused ( & env) ;
1594+ let mut invoice = load_invoice ( & env, invoice_id) ;
1595+ let oracle = invoice. oracle_address . as_ref ( ) . expect ( "no oracle set for invoice" ) ;
1596+ oracle. require_auth ( ) ;
1597+ invoice. condition_met = true ;
1598+ save_invoice ( & env, invoice_id, & invoice) ;
1599+ append_audit_entry ( & env, invoice_id, symbol_short ! ( "oracle_ok" ) , oracle) ;
1600+ }
1601+
1602+ /// Set a payment reminder for an address on a specific invoice.
1603+ /// The `who` address must authenticate.
1604+ pub fn set_reminder ( env : Env , who : Address , invoice_id : u64 , remind_at : u64 ) {
1605+ require_not_paused ( & env) ;
1606+ who. require_auth ( ) ;
1607+ env. storage ( )
1608+ . persistent ( )
1609+ . set ( & reminder_key ( invoice_id, & who) , & remind_at) ;
1610+ append_audit_entry ( & env, invoice_id, symbol_short ! ( "set_rmd" ) , & who) ;
1611+ }
1612+
1613+ /// Trigger a previously set reminder; must be called at or after `remind_at`.
1614+ pub fn trigger_reminder ( env : Env , invoice_id : u64 , who : Address ) {
1615+ require_not_paused ( & env) ;
1616+ let remind_at: u64 = env
1617+ . storage ( )
1618+ . persistent ( )
1619+ . get ( & reminder_key ( invoice_id, & who) )
1620+ . expect ( "reminder not set" ) ;
1621+ assert ! ( env. ledger( ) . timestamp( ) >= remind_at, "reminder not due" ) ;
1622+ events:: payment_reminder ( & env, invoice_id, & who) ;
1623+ env. storage ( ) . persistent ( ) . remove ( & reminder_key ( invoice_id, & who) ) ;
1624+ append_audit_entry ( & env, invoice_id, symbol_short ! ( "trig_rmd" ) , & who) ;
1625+ }
1626+
1627+ /// Create a treasury group linking multiple invoice IDs to a single treasury address.
1628+ /// Returns the new group id.
1629+ pub fn group_treasury_create ( env : Env , creator : Address , invoice_ids : Vec < u64 > , treasury : Address ) -> u64 {
1630+ require_not_paused ( & env) ;
1631+ creator. require_auth ( ) ;
1632+ let id: u64 = env
1633+ . storage ( )
1634+ . persistent ( )
1635+ . get ( & treasury_group_counter_key ( ) )
1636+ . unwrap_or ( 0u64 )
1637+ + 1 ;
1638+ env. storage ( ) . persistent ( ) . set ( & treasury_group_counter_key ( ) , & id) ;
1639+ let record = types:: TreasuryRecord { invoice_ids : invoice_ids. clone ( ) , treasury : treasury. clone ( ) } ;
1640+ env. storage ( ) . persistent ( ) . set ( & group_treasury_key ( id) , & record) ;
1641+ for iid in invoice_ids. iter ( ) {
1642+ env. storage ( ) . persistent ( ) . set ( & invoice_treasury_key ( * iid) , & id) ;
1643+ append_audit_entry ( & env, * iid, symbol_short ! ( "grp_tr" ) , & creator) ;
1644+ }
1645+ id
1646+ }
1647+
1648+ /// Pay toward an invoice using a memo that encodes the invoice id.
1649+ /// Requires payer auth and emits a payment_matched event on success.
1650+ pub fn pay_with_memo ( env : Env , payer : Address , memo : u64 , amount : i128 , nonce : u64 , auto_convert : bool ) {
1651+ require_not_paused ( & env) ;
1652+ payer. require_auth ( ) ;
1653+ // Validate memo corresponds to an existing invoice.
1654+ let _ = load_invoice ( & env, memo) ;
1655+ Self :: _pay ( & env, & payer, memo, amount, nonce, auto_convert) ;
1656+ events:: payment_matched ( & env, memo, memo, & payer) ;
1657+ }
1658+
15361659 /// Claim vesting cliff share after cliff timestamp has passed (issue #27).
15371660 ///
15381661 /// Requires that the invoice status is Released and the cliff (if set) has passed.
@@ -1886,6 +2009,7 @@ impl SplitContract {
18862009 let mut distributed: i128 = 0 ;
18872010 let mut total_fee: i128 = 0 ;
18882011 let mut total_tax: i128 = 0 ;
2012+ let mut payouts: Vec < i128 > = Vec :: new ( env) ;
18892013 for i in 0 ..n {
18902014 let recipient = invoice. recipients . get ( i) . unwrap ( ) ;
18912015 let amount = invoice. amounts . get ( i) . unwrap ( ) ;
@@ -1914,43 +2038,91 @@ impl SplitContract {
19142038 distributed += proportional;
19152039
19162040 let tax = ( proportional as u128 * invoice. tax_bps as u128 / 10_000u128 ) as i128 ;
1917- let post_tax = proportional - tax;
19182041 total_tax += tax;
2042+ let post_tax = proportional - tax;
19192043
19202044 let fee = ( post_tax as u128 * platform_fee_bps as u128 / 10_000u128 ) as i128 ;
1921- let payout = post_tax - fee;
19222045 total_fee += fee;
19232046
1924- // Issue #41: if a swap token is configured for this recipient, invoke DEX swap.
1925- let swap_token: Option < Address > = invoice
1926- . swap_tokens
1927- . get ( i as u32 )
1928- . unwrap_or ( None ) ;
1929- if let Some ( ref out_token) = swap_token {
1930- let from_token = invoice. tokens . get ( 0 ) . expect ( "no token" ) ;
1931- let mut args: Vec < Val > = Vec :: new ( env) ;
1932- args. push_back ( from_token. into_val ( env) ) ;
1933- args. push_back ( out_token. clone ( ) . into_val ( env) ) ;
1934- args. push_back ( payout. into_val ( env) ) ;
1935- args. push_back ( recipient. into_val ( env) ) ;
1936- let _swapped: i128 = env. invoke_contract ( out_token, & Symbol :: new ( env, "swap" ) , args) ;
1937- } else if invoice. smart_route {
1938- // Smart routing: query DEX router for optimal path, fall back to direct transfer.
1939- let from_token = invoice. tokens . get ( 0 ) . expect ( "no token" ) ;
1940- let mut route_args: Vec < Val > = Vec :: new ( env) ;
1941- route_args. push_back ( from_token. into_val ( env) ) ;
1942- route_args. push_back ( payout. into_val ( env) ) ;
1943- route_args. push_back ( recipient. clone ( ) . into_val ( env) ) ;
1944- // Try DEX path-finding via invoke; on failure fall back to direct transfer.
1945- // In production the router address would be stored; here we attempt invoke
1946- // and catch failure by falling back.
1947- token_client. transfer ( & env. current_contract_address ( ) , & recipient, & payout) ;
1948- } else {
1949- let routed = Self :: execute_smart_route ( env, invoice, & recipient, payout) ;
1950- if !routed {
2047+ payouts. push_back ( proportional) ;
2048+ }
2049+
2050+ // If this invoice belongs to a treasury group, route the net payouts to the group's treasury address.
2051+ if let Some ( ( _group_id, record) ) = treasury_record_for_invoice ( env, invoice_id) {
2052+ // Transfer taxes first.
2053+ if total_tax > 0 {
2054+ if let Some ( ref auth) = invoice. tax_authority {
2055+ token_client. transfer ( & env. current_contract_address ( ) , auth, & total_tax) ;
2056+ }
2057+ }
2058+
2059+ // Transfer platform fee to global treasury.
2060+ if total_fee > 0 {
2061+ let treasury: Address = env
2062+ . storage ( )
2063+ . instance ( )
2064+ . get ( & treasury_key ( ) )
2065+ . expect ( "treasury not set" ) ;
2066+ token_client. transfer ( & env. current_contract_address ( ) , & treasury, & total_fee) ;
2067+ }
2068+
2069+ let net = distributed - total_tax - total_fee;
2070+ if net > 0 {
2071+ token_client. transfer ( & env. current_contract_address ( ) , & record. treasury , & net) ;
2072+ }
2073+ } else {
2074+ // Default behavior: transfer to each recipient (or route via DEX/router as configured).
2075+ for i in 0 ..n {
2076+ let recipient = invoice. recipients . get ( i) . unwrap ( ) ;
2077+ let proportional = payouts. get ( i as u32 ) . unwrap ( ) ;
2078+
2079+ let tax = ( proportional as u128 * invoice. tax_bps as u128 / 10_000u128 ) as i128 ;
2080+ let post_tax = proportional - tax;
2081+ let fee = ( post_tax as u128 * platform_fee_bps as u128 / 10_000u128 ) as i128 ;
2082+ let payout = post_tax - fee;
2083+
2084+ // Issue #41: if a swap token is configured for this recipient, invoke DEX swap.
2085+ let swap_token: Option < Address > = invoice
2086+ . swap_tokens
2087+ . get ( i as u32 )
2088+ . unwrap_or ( None ) ;
2089+ if let Some ( ref out_token) = swap_token {
2090+ let from_token = invoice. tokens . get ( 0 ) . expect ( "no token" ) ;
2091+ let mut args: Vec < Val > = Vec :: new ( env) ;
2092+ args. push_back ( from_token. into_val ( env) ) ;
2093+ args. push_back ( out_token. clone ( ) . into_val ( env) ) ;
2094+ args. push_back ( payout. into_val ( env) ) ;
2095+ args. push_back ( recipient. into_val ( env) ) ;
2096+ let _swapped: i128 = env. invoke_contract ( out_token, & Symbol :: new ( env, "swap" ) , args) ;
2097+ } else if invoice. smart_route {
2098+ let from_token = invoice. tokens . get ( 0 ) . expect ( "no token" ) ;
2099+ let mut route_args: Vec < Val > = Vec :: new ( env) ;
2100+ route_args. push_back ( from_token. into_val ( env) ) ;
2101+ route_args. push_back ( payout. into_val ( env) ) ;
2102+ route_args. push_back ( recipient. clone ( ) . into_val ( env) ) ;
19512103 token_client. transfer ( & env. current_contract_address ( ) , & recipient, & payout) ;
2104+ } else {
2105+ let routed = Self :: execute_smart_route ( env, invoice, & recipient, payout) ;
2106+ if !routed {
2107+ token_client. transfer ( & env. current_contract_address ( ) , & recipient, & payout) ;
2108+ }
2109+ }
2110+ }
2111+
2112+ if total_tax > 0 {
2113+ if let Some ( ref auth) = invoice. tax_authority {
2114+ token_client. transfer ( & env. current_contract_address ( ) , auth, & total_tax) ;
19522115 }
19532116 }
2117+
2118+ if total_fee > 0 {
2119+ let treasury: Address = env
2120+ . storage ( )
2121+ . instance ( )
2122+ . get ( & treasury_key ( ) )
2123+ . expect ( "treasury not set" ) ;
2124+ token_client. transfer ( & env. current_contract_address ( ) , & treasury, & total_fee) ;
2125+ }
19542126 }
19552127
19562128 if total_tax > 0 {
@@ -2166,6 +2338,8 @@ impl SplitContract {
21662338 Vec :: new ( env) ,
21672339 Vec :: new ( env) ,
21682340 Vec :: new ( env) ,
2341+ None ,
2342+ None ,
21692343 ) ;
21702344 env. storage ( )
21712345 . persistent ( )
0 commit comments