@@ -8,7 +8,7 @@ mod types;
88#[ cfg( test) ]
99mod test;
1010
11- use soroban_sdk:: { contract, contractimpl, symbol_short, token, Address , Env , Map , Symbol , Vec } ;
11+ use soroban_sdk:: { contract, contractimpl, symbol_short, token, Address , Bytes , Env , Map , Symbol , Vec } ;
1212use types:: {
1313 AuditEntry , CreateInvoiceParams , DisputeState , Invoice , InvoiceOptions , InvoicePayment ,
1414 InvoiceStats , InvoiceStatus , Payment , SplitRule , SubscriptionParams ,
@@ -43,7 +43,9 @@ fn load_invoice(env: &Env, id: u64) -> Invoice {
4343
4444fn save_invoice ( env : & Env , id : u64 , invoice : & Invoice ) {
4545 env. storage ( ) . persistent ( ) . set ( & invoice_key ( id) , invoice) ;
46- // Extend TTL to ~1 year (in ledgers at ~5s each: 365*24*3600/5 = 6_307_200)
46+ // Protocol 26 CAP-78: limited TTL extension host function.
47+ // Extend to ~1 year (6_307_200 ledgers at ~5s each).
48+ // Only bumps if current TTL < min_ttl (100_000 ledgers ~ 6 days).
4749 env. storage ( ) . persistent ( ) . extend_ttl ( & invoice_key ( id) , 100_000 , 6_307_200 ) ;
4850}
4951
@@ -382,20 +384,39 @@ impl SharpyContract {
382384 match invoice. split_rules . get ( i as u32 ) . unwrap ( ) {
383385 SplitRule :: Fixed ( fixed_amt) => fixed_amt,
384386 SplitRule :: Percentage ( bps) => {
385- ( invoice. funded as u128 * bps as u128 / 10_000u128 ) as i128
387+ // Protocol 26 CAP-82: use checked arithmetic to prevent overflow
388+ // funded * bps / 10_000 — all i128, safe for balances up to i128::MAX
389+ invoice. funded
390+ . checked_mul ( bps as i128 )
391+ . expect ( "percentage: overflow in funded * bps" )
392+ . checked_div ( 10_000 )
393+ . expect ( "percentage: division failed" )
386394 }
387395 SplitRule :: Tiered ( threshold, bps) => {
388396 if invoice. funded > threshold {
389- ( invoice. funded as u128 * bps as u128 / 10_000u128 ) as i128
397+ // Protocol 26 CAP-82: checked arithmetic for tiered splits
398+ invoice. funded
399+ . checked_mul ( bps as i128 )
400+ . expect ( "tiered: overflow in funded * bps" )
401+ . checked_div ( 10_000 )
402+ . expect ( "tiered: division failed" )
390403 } else {
391404 0
392405 }
393406 }
394407 }
395408 } else if i == n - 1 {
396- invoice. funded - distributed
409+ // Last recipient gets the remainder to avoid dust from rounding
410+ invoice. funded
411+ . checked_sub ( distributed)
412+ . expect ( "release: underflow computing remainder" )
397413 } else {
398- ( amount as u128 * invoice. funded as u128 / total as u128 ) as i128
414+ // Proportional split: amount * funded / total — checked throughout
415+ amount
416+ . checked_mul ( invoice. funded )
417+ . expect ( "proportional: overflow in amount * funded" )
418+ . checked_div ( total)
419+ . expect ( "proportional: division by zero total" )
399420 } ;
400421
401422 distributed += proportional;
@@ -519,7 +540,12 @@ impl SharpyContract {
519540 }
520541 }
521542 let completion_bps = if total > 0 {
522- ( invoice. funded as u128 * 10_000u128 / total as u128 ) as u32
543+ // Protocol 26 CAP-82: checked arithmetic for completion percentage
544+ invoice. funded
545+ . checked_mul ( 10_000 )
546+ . expect ( "stats: overflow in funded * 10_000" )
547+ . checked_div ( total)
548+ . expect ( "stats: division by zero" ) as u32
523549 } else {
524550 0
525551 } ;
@@ -535,4 +561,34 @@ impl SharpyContract {
535561 pub fn get_escrow_state ( env : Env , invoice_id : u64 ) -> Option < DisputeState > {
536562 env. storage ( ) . persistent ( ) . get ( & escrow_state_key ( invoice_id) )
537563 }
564+
565+ /// Extend the TTL of an invoice entry to ~1 year.
566+ /// Protocol 26 CAP-78: host function for limited TTL extension keeps long-lived
567+ /// and recurring invoices accessible without requiring a full state restore.
568+ pub fn bump_invoice_ttl ( env : Env , invoice_id : u64 ) {
569+ let _ = load_invoice ( & env, invoice_id) ;
570+ env. storage ( ) . persistent ( ) . extend_ttl ( & invoice_key ( invoice_id) , 100_000 , 6_307_200 ) ;
571+ }
572+
573+ /// Returns a SHA-256 fingerprint of the invoice's immutable fields.
574+ /// Protocol 25 CAP-75 / Protocol 26 crypto module: deterministic, tamper-evident
575+ /// content hash. The fingerprint commits to invoice_id, deadline, funded amount,
576+ /// and total — any modification produces a different hash.
577+ /// Use this for off-chain verification or receipt generation.
578+ pub fn get_invoice_fingerprint ( env : Env , invoice_id : u64 ) -> soroban_sdk:: BytesN < 32 > {
579+ let invoice = load_invoice ( & env, invoice_id) ;
580+ let total: i128 = invoice. amounts . iter ( ) . sum ( ) ;
581+
582+ // Build a flat byte buffer of key invoice fields for deterministic hashing
583+ let mut buf: [ u8 ; 40 ] = [ 0u8 ; 40 ] ; // 8 + 8 + 8 + 16 bytes
584+ buf[ 0 ..8 ] . copy_from_slice ( & invoice_id. to_be_bytes ( ) ) ;
585+ buf[ 8 ..16 ] . copy_from_slice ( & invoice. deadline . to_be_bytes ( ) ) ;
586+ buf[ 16 ..24 ] . copy_from_slice ( & ( invoice. recipients . len ( ) as u64 ) . to_be_bytes ( ) ) ;
587+ buf[ 24 ..40 ] . copy_from_slice ( & total. to_be_bytes ( ) ) ;
588+
589+ let data = Bytes :: from_array ( & env, & buf) ;
590+
591+ // SHA-256 via Protocol 25/26 crypto host function — returns Hash<32>
592+ env. crypto ( ) . sha256 ( & data) . into ( )
593+ }
538594}
0 commit comments