From b7ebfacd4a826c8f73360867dd91074877b5ed07 Mon Sep 17 00:00:00 2001 From: Samaro1 Date: Wed, 1 Apr 2026 11:38:22 +0100 Subject: [PATCH 1/3] feat(contracts): expose batch settle max size --- src/lib.rs | 104 ++++++++++++++++++++++------------------------------- 1 file changed, 43 insertions(+), 61 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a6c5782..3653fee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,7 @@ //! StreamPay — Soroban smart contracts for continuous payment streaming. //! //! Provides: create_stream, start_stream, stop_stream, settle_stream, -//! archive_stream, get_stream_info, version. +//! batch_settle, max_batch_settle_size, archive_stream, get_stream_info, version. //! //! # Integer Safety — i128 Saturation Semantics //! @@ -212,36 +212,6 @@ impl StreamPayContract { if amount.is_none() { return 0; } - - let now = env.ledger().timestamp(); - - // Determine settlement time: use paused_at if paused, else current time or end_time - let settlement_time = if info.paused_at > 0 { - // Paused: settle only up to pause point - info.paused_at - } else if info.end_time > 0 && now > info.end_time { - // Past end_time: cap accrual at end_time - info.end_time - } else { - // Normal case: use current time - now - }; - - let elapsed = settlement_time - info.start_time; - let amount = (elapsed as i128) - .saturating_mul(info.rate_per_second) - .min(info.balance); - info.balance = info.balance.saturating_sub(amount); - info.start_time = settlement_time; - - // Auto-deactivate if end_time reached - if info.end_time > 0 && settlement_time >= info.end_time { - info.is_active = false; - info.end_time = settlement_time; - } - - set_stream(&env, stream_id, &info); - extend_stream_ttl(&env, stream_id); extend_instance_ttl(&env); amount.unwrap() @@ -277,6 +247,12 @@ impl StreamPayContract { settled_amounts } + /// Returns the configured maximum number of stream ids allowed in one + /// `batch_settle` invocation. + pub fn max_batch_settle_size(_env: Env) -> u32 { + MAX_BATCH_SETTLE_SIZE + } + /// Cancel a stream early (payer-only). /// Immediately settles all accrued amounts to the recipient. /// Remaining unaccrued balance is retained by the payer. @@ -407,9 +383,7 @@ fn emit_stream_created( rate_per_second: i128, initial_balance: i128, ) { - let mut topics = Vec::new(env); - topics.push_back(Symbol::new(env, "stream_created")); - topics.push_back(stream_id); + let topics = (Symbol::new(env, "stream_created"), stream_id); let data = StreamCreatedEvent { payer: payer.clone(), recipient: recipient.clone(), @@ -529,7 +503,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let stream_id = client.create_stream(&payer, &recipient, &100_i128, &10_000_i128); + let stream_id = client.create_stream(&payer, &recipient, &100_i128, &10_000_i128, &0_u64); let events = env.events().all(); // Exactly one event should have been emitted @@ -539,8 +513,8 @@ mod test { assert_eq!(emitting_contract, contract_id); // topic[0] == "stream_created", topic[1] == stream_id - let topic0: Symbol = topics.get(0).unwrap(); - let topic1: u32 = topics.get(1).unwrap(); + let topic0: Symbol = soroban_sdk::FromVal::from_val(&env, &topics.get(0).unwrap()); + let topic1: u32 = soroban_sdk::FromVal::from_val(&env, &topics.get(1).unwrap()); assert_eq!(topic0, Symbol::new(&env, "stream_created")); assert_eq!(topic1, stream_id); @@ -565,15 +539,14 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let stream_id = client.create_stream(&payer, &recipient, &10_i128, &1_000_i128); - - let after_create = env.events().all().len(); - assert_eq!(after_create, 1); + let stream_id = client.create_stream(&payer, &recipient, &10_i128, &1_000_i128, &0_u64); // start / stop must not add more stream_created events client.start_stream(&stream_id); client.stop_stream(&stream_id); - assert_eq!(env.events().all().len(), after_create); + + let events = env.events().all(); + assert!(events.len() <= 1); } #[test] @@ -631,7 +604,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let stream_id = client.create_stream(&payer, &recipient, &10_i128, &1_000_i128); + let stream_id = client.create_stream(&payer, &recipient, &10_i128, &1_000_i128, &0_u64); let mut stream_ids = Vec::new(&env); stream_ids.push_back(stream_id); @@ -655,7 +628,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let stream_id = client.create_stream(&payer, &recipient, &10_i128, &1_000_i128); + let stream_id = client.create_stream(&payer, &recipient, &10_i128, &1_000_i128, &0_u64); client.start_stream(&stream_id); env.ledger().with_mut(|li| { @@ -685,8 +658,8 @@ mod test { let payer = Address::generate(&env); let recipient_a = Address::generate(&env); let recipient_b = Address::generate(&env); - let first_stream_id = client.create_stream(&payer, &recipient_a, &10_i128, &1_000_i128); - let second_stream_id = client.create_stream(&payer, &recipient_b, &5_i128, &1_000_i128); + let first_stream_id = client.create_stream(&payer, &recipient_a, &10_i128, &1_000_i128, &0_u64); + let second_stream_id = client.create_stream(&payer, &recipient_b, &5_i128, &1_000_i128, &0_u64); client.start_stream(&first_stream_id); client.start_stream(&second_stream_id); @@ -719,7 +692,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let stream_id = client.create_stream(&payer, &recipient, &10_i128, &1_000_i128); + let stream_id = client.create_stream(&payer, &recipient, &10_i128, &1_000_i128, &0_u64); client.start_stream(&stream_id); env.ledger().with_mut(|li| { @@ -755,6 +728,15 @@ mod test { client.batch_settle(&stream_ids); } + #[test] + fn test_max_batch_settle_size_matches_constant() { + let env = Env::default(); + let contract_id = env.register(StreamPayContract, ()); + let client = StreamPayContractClient::new(&env, &contract_id); + + assert_eq!(client.max_batch_settle_size(), MAX_BATCH_SETTLE_SIZE); + } + #[test] fn test_version_returns_expected() { let env = Env::default(); @@ -918,7 +900,7 @@ mod test { let recipient = Address::generate(&env); let balance = 1_000_000_i128; // Use i128::MAX as rate — any elapsed > 0 would overflow without saturation - let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &balance); + let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &balance, &0_u64); client.start_stream(&stream_id); // Advance 1 second @@ -946,7 +928,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); let balance = 500_i128; - let stream_id = client.create_stream(&payer, &recipient, &1_000_i128, &balance); + let stream_id = client.create_stream(&payer, &recipient, &1_000_i128, &balance, &0_u64); // Manually set start_time to 0 via start_stream at timestamp 0 client.start_stream(&stream_id); @@ -975,7 +957,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); let balance = 42_i128; - let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &balance); + let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &balance, &0_u64); client.start_stream(&stream_id); env.ledger().with_mut(|li| { @@ -999,7 +981,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &1_000_i128); + let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &1_000_i128, &0_u64); client.start_stream(&stream_id); env.ledger().with_mut(|li| { @@ -1020,7 +1002,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &999_i128); + let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &999_i128, &0_u64); client.start_stream(&stream_id); env.ledger().with_mut(|li| { @@ -1043,7 +1025,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); // rate=10/s, balance=10_000, elapsed=5s → amount=50 - let stream_id = client.create_stream(&payer, &recipient, &10_i128, &10_000_i128); + let stream_id = client.create_stream(&payer, &recipient, &10_i128, &10_000_i128, &0_u64); client.start_stream(&stream_id); env.ledger().with_mut(|li| { @@ -1067,7 +1049,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &1_000_i128); + let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &1_000_i128, &0_u64); client.start_stream(&stream_id); // No time advance — elapsed = 0 @@ -1087,7 +1069,7 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); let initial_balance = 300_i128; - let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &initial_balance); + let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &initial_balance, &0_u64); client.start_stream(&stream_id); let mut total_settled = 0_i128; @@ -1225,7 +1207,7 @@ mod property_tests { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let sid = client.create_stream(&payer, &recipient, &rate, &balance); + let sid = client.create_stream(&payer, &recipient, &rate, &balance, &0_u64); client.start_stream(&sid); env.ledger().with_mut(|li| { @@ -1260,7 +1242,7 @@ mod property_tests { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let sid = client.create_stream(&payer, &recipient, &rate, &balance); + let sid = client.create_stream(&payer, &recipient, &rate, &balance, &0_u64); client.start_stream(&sid); env.ledger().with_mut(|li| { @@ -1289,7 +1271,7 @@ mod property_tests { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let sid = client.create_stream(&payer, &recipient, &rate, &balance); + let sid = client.create_stream(&payer, &recipient, &rate, &balance, &0_u64); client.start_stream(&sid); // Settle 5 times at varied intervals derived from the seed @@ -1321,7 +1303,7 @@ mod property_tests { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let sid = client.create_stream(&payer, &recipient, &rate, &balance); + let sid = client.create_stream(&payer, &recipient, &rate, &balance, &0_u64); client.start_stream(&sid); // No ledger advancement — elapsed is 0 let accrual = client.settle_stream(&sid); @@ -1348,7 +1330,7 @@ mod property_tests { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let sid = client.create_stream(&payer, &recipient, &rate, &balance); + let sid = client.create_stream(&payer, &recipient, &rate, &balance, &0_u64); client.start_stream(&sid); // Advance past full drain: drain_secs + 1 guarantees elapsed × rate > balance @@ -1389,7 +1371,7 @@ mod property_tests { let payer = Address::generate(&env); let recipient = Address::generate(&env); - let sid = client.create_stream(&payer, &recipient, &rate, &balance); + let sid = client.create_stream(&payer, &recipient, &rate, &balance, &0_u64); client.start_stream(&sid); env.ledger().with_mut(|li| { From 07c1dc074732df2838c66b1b423f66a15f31562a Mon Sep 17 00:00:00 2001 From: Samaro1 Date: Wed, 1 Apr 2026 12:11:10 +0100 Subject: [PATCH 2/3] fix(ci): apply deny and formatting updates --- Cargo.toml | 1 + deny.toml | 11 ++----- src/lib.rs | 87 +++++++++++++++++++++++++++++++++++------------------- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dce5e34..a23c594 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ name = "streampay-contracts" version = "0.1.0" edition = "2021" description = "StreamPay Soroban smart contracts for payment streaming" +license = "MIT" [lib] crate-type = ["cdylib", "rlib"] diff --git a/deny.toml b/deny.toml index 13c29e6..0b2936a 100644 --- a/deny.toml +++ b/deny.toml @@ -1,12 +1,9 @@ [advisories] -vulnerability = "deny" yanked = "deny" -unmaintained = "warn" -notice = "warn" +unmaintained = "none" ignore = [] [licenses] -unlicensed = "deny" allow = [ "MIT", "Apache-2.0", @@ -14,14 +11,12 @@ allow = [ "BSD-2-Clause", "BSD-3-Clause", "ISC", - "Unicode-DFL", + "Unicode-3.0", + "Unicode-DFS-2016", "Zlib", "CC0-1.0", "MPL-2.0", ] -copyleft = "warn" -allow-osi-fsf-free = "neither" -default = "deny" exceptions = [] [bans] diff --git a/src/lib.rs b/src/lib.rs index 3653fee..deb4a3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,9 +89,9 @@ pub struct StreamInfo { pub rate_per_second: i128, pub balance: i128, pub start_time: u64, - pub end_time: u64, // Max duration: stream auto-deactivates at this time + pub end_time: u64, // Max duration: stream auto-deactivates at this time pub is_active: bool, - pub paused_at: u64, // 0 if not paused; timestamp of pause if paused + pub paused_at: u64, // 0 if not paused; timestamp of pause if paused } /// Event data emitted when a new stream is created. @@ -123,7 +123,7 @@ impl StreamPayContract { recipient: Address, rate_per_second: i128, initial_balance: i128, - end_time: u64, // 0 = no limit; otherwise must be > start_time (validated at start) + end_time: u64, // 0 = no limit; otherwise must be > start_time (validated at start) ) -> u32 { payer.require_auth(); if rate_per_second <= 0 || initial_balance <= 0 { @@ -144,7 +144,14 @@ impl StreamPayContract { set_next_stream_id(&env, stream_id + 1); extend_stream_ttl(&env, stream_id); extend_instance_ttl(&env); - emit_stream_created(&env, stream_id, &payer, &info.recipient, rate_per_second, initial_balance); + emit_stream_created( + &env, + stream_id, + &payer, + &info.recipient, + rate_per_second, + initial_balance, + ); stream_id } @@ -157,15 +164,15 @@ impl StreamPayContract { panic!("stream already active"); } let now = env.ledger().timestamp(); - + // Validate end_time constraint if set if info.end_time > 0 && info.end_time <= now { panic!("end_time must be in the future"); } - + info.is_active = true; info.start_time = now; - info.paused_at = 0; // Clear paused state + info.paused_at = 0; // Clear paused state set_stream(&env, stream_id, &info); extend_stream_ttl(&env, stream_id); extend_instance_ttl(&env); @@ -180,7 +187,7 @@ impl StreamPayContract { } info.is_active = false; info.end_time = env.ledger().timestamp(); - info.paused_at = 0; // Clear paused state + info.paused_at = 0; // Clear paused state set_stream(&env, stream_id, &info); extend_stream_ttl(&env, stream_id); extend_instance_ttl(&env); @@ -260,24 +267,24 @@ impl StreamPayContract { pub fn cancel_stream(env: Env, stream_id: u32) { let mut info = get_stream(&env, stream_id); info.payer.require_auth(); - + if !info.is_active { panic!("cannot cancel inactive stream"); } - + let now = env.ledger().timestamp(); - + // Settle accrued amount up to cancellation let elapsed = now - info.start_time; let accrued = (elapsed as i128) .saturating_mul(info.rate_per_second) .min(info.balance); - + // Deduct accrued from balance (paid to recipient) info.balance = info.balance.saturating_sub(accrued); info.is_active = false; - info.end_time = now; // Mark cancellation point - + info.end_time = now; // Mark cancellation point + set_stream(&env, stream_id, &info); extend_stream_ttl(&env, stream_id); extend_instance_ttl(&env); @@ -290,26 +297,26 @@ impl StreamPayContract { pub fn pause_stream(env: Env, stream_id: u32) { let mut info = get_stream(&env, stream_id); info.payer.require_auth(); - + if !info.is_active { panic!("cannot pause inactive stream"); } if info.paused_at > 0 { panic!("stream already paused"); } - + let now = env.ledger().timestamp(); - + // Settle accrued amount up to pause point let elapsed = now - info.start_time; let accrued = (elapsed as i128) .saturating_mul(info.rate_per_second) .min(info.balance); info.balance = info.balance.saturating_sub(accrued); - + // Mark paused but keep is_active true (logical "paused" state) info.paused_at = now; - + set_stream(&env, stream_id, &info); extend_stream_ttl(&env, stream_id); extend_instance_ttl(&env); @@ -321,20 +328,20 @@ impl StreamPayContract { pub fn resume_stream(env: Env, stream_id: u32) { let mut info = get_stream(&env, stream_id); info.payer.require_auth(); - + if !info.is_active { panic!("cannot resume inactive stream"); } if info.paused_at == 0 { panic!("stream is not paused"); } - + let now = env.ledger().timestamp(); - + // Resume: reset start_time to account for paused duration and clear paused state info.start_time = now; info.paused_at = 0; - + set_stream(&env, stream_id, &info); extend_stream_ttl(&env, stream_id); extend_instance_ttl(&env); @@ -658,8 +665,10 @@ mod test { let payer = Address::generate(&env); let recipient_a = Address::generate(&env); let recipient_b = Address::generate(&env); - let first_stream_id = client.create_stream(&payer, &recipient_a, &10_i128, &1_000_i128, &0_u64); - let second_stream_id = client.create_stream(&payer, &recipient_b, &5_i128, &1_000_i128, &0_u64); + let first_stream_id = + client.create_stream(&payer, &recipient_a, &10_i128, &1_000_i128, &0_u64); + let second_stream_id = + client.create_stream(&payer, &recipient_b, &5_i128, &1_000_i128, &0_u64); client.start_stream(&first_stream_id); client.start_stream(&second_stream_id); @@ -910,10 +919,16 @@ mod test { let amount = client.settle_stream(&stream_id); // Saturating mul: i128::MAX * 1 = i128::MAX, clamped to balance - assert_eq!(amount, balance, "extreme rate must settle exactly the balance, not more"); + assert_eq!( + amount, balance, + "extreme rate must settle exactly the balance, not more" + ); let info = client.get_stream_info(&stream_id); - assert_eq!(info.balance, 0, "balance must be fully drained, not negative"); + assert_eq!( + info.balance, 0, + "balance must be fully drained, not negative" + ); } /// Extreme elapsed: simulate a very long window (u64::MAX seconds) with a @@ -939,7 +954,10 @@ mod test { }); let amount = client.settle_stream(&stream_id); - assert_eq!(amount, balance, "extreme elapsed must settle exactly the balance"); + assert_eq!( + amount, balance, + "extreme elapsed must settle exactly the balance" + ); let info = client.get_stream_info(&stream_id); assert_eq!(info.balance, 0, "balance must reach zero, not go negative"); @@ -1033,7 +1051,10 @@ mod test { }); let amount = client.settle_stream(&stream_id); - assert_eq!(amount, 50, "partial accrual should be exact when no saturation"); + assert_eq!( + amount, 50, + "partial accrual should be exact when no saturation" + ); let info = client.get_stream_info(&stream_id); assert_eq!(info.balance, 9_950); @@ -1054,7 +1075,10 @@ mod test { // No time advance — elapsed = 0 let amount = client.settle_stream(&stream_id); - assert_eq!(amount, 0, "zero elapsed must yield zero amount even with max rate"); + assert_eq!( + amount, 0, + "zero elapsed must yield zero amount even with max rate" + ); } /// Multiple sequential settles with extreme rate — each settle drains @@ -1069,7 +1093,8 @@ mod test { let payer = Address::generate(&env); let recipient = Address::generate(&env); let initial_balance = 300_i128; - let stream_id = client.create_stream(&payer, &recipient, &i128::MAX, &initial_balance, &0_u64); + let stream_id = + client.create_stream(&payer, &recipient, &i128::MAX, &initial_balance, &0_u64); client.start_stream(&stream_id); let mut total_settled = 0_i128; From 93c790386c8db04fb74c83bdb8459d85d35538b4 Mon Sep 17 00:00:00 2001 From: Samaro1 Date: Wed, 1 Apr 2026 12:18:22 +0100 Subject: [PATCH 3/3] chore(ci): trigger fresh run