From b45eb9baf231f99be44f8c0f35a045e900d5a4da Mon Sep 17 00:00:00 2001 From: soma-enyi Date: Sun, 30 Aug 2026 19:19:43 +0100 Subject: [PATCH 1/4] test: add contract test for bump_stream() on nonexistent stream (issue #658) --- contracts/streaming/src/test_security.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/contracts/streaming/src/test_security.rs b/contracts/streaming/src/test_security.rs index e322061..b8bec50 100644 --- a/contracts/streaming/src/test_security.rs +++ b/contracts/streaming/src/test_security.rs @@ -1435,3 +1435,17 @@ fn test_unlocked_amount_overflow_returns_error() { assert_eq!(ctx.token().balance(&ctx.recipient), total_amount); assert_eq!(ctx.token().balance(&ctx.contract_id), 0); } + +/// Test bump_stream() with a nonexistent stream ID returns StreamNotFound. +/// Issue #658: Add contract test for bump_stream() on a nonexistent stream ID. +#[test] +fn test_bump_stream_nonexistent_stream_returns_stream_not_found() { + let ctx = Ctx::new(); + let now = 1_000_000u64; + ctx.set_time(now); + + // Attempt to bump a stream ID that was never created + let nonexistent_id = 99999u64; + let result = ctx.client().try_bump_stream(&nonexistent_id); + assert_eq!(result, Err(Ok(StreamError::StreamNotFound))); +} From f17815528905b3bac8725bc13f520212a9dbab58 Mon Sep 17 00:00:00 2001 From: soma-enyi Date: Sun, 30 Aug 2026 19:20:39 +0100 Subject: [PATCH 2/4] test: add contract test for batch creation that rejects contract as recipient (issue #659) --- contracts/streaming/src/test_batch.rs | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/contracts/streaming/src/test_batch.rs b/contracts/streaming/src/test_batch.rs index 2b9ef7f..891bb8b 100644 --- a/contracts/streaming/src/test_batch.rs +++ b/contracts/streaming/src/test_batch.rs @@ -597,3 +597,37 @@ fn test_batch_streams_are_independently_cancellable() { assert!(s1.cancelled); assert!(!s2.cancelled); } + +// ─── Batch rejects when recipient is the contract ───────────────────────────── + +/// Test create_streams_batch() rejects a batch entry whose recipient is the contract itself. +/// Issue #659: Add contract test for batch creation with contract as recipient. +#[test] +fn test_batch_rejects_contract_as_recipient() { + let t = TestEnv::setup(); + let now = 1_000_000u64; + t.set_time(now); + + // Create a batch where one entry has the contract as recipient + let valid_recipient = Address::generate(&t.env); + let total_per_stream = 1_000_0000000i128; + t.approve(total_per_stream * 2); + + let mut inputs: Vec = Vec::new(&t.env); + // Valid stream + inputs.push_back(t.make_input(&valid_recipient, now)); + // Invalid stream with contract as recipient + inputs.push_back(CreateStreamInput { + recipient: t.contract_id.clone(), + token: t.token_id.clone(), + total_amount: total_per_stream, + start_time: now, + end_time: now + 1000, + cliff_time: now, + cliff_amount: 0, + }); + + let client = t.client(); + let result = client.try_create_streams_batch(&t.sender, &inputs); + assert_eq!(result, Err(Ok(StreamError::InvalidRecipient))); +} From f7d5ca15ff6cbc6da81b1e75055569c8e2edde2a Mon Sep 17 00:00:00 2001 From: soma-enyi Date: Sun, 30 Aug 2026 19:20:59 +0100 Subject: [PATCH 3/4] test: add contract test for batch creation that rejects dust/zero-rate stream (issue #660) --- contracts/streaming/src/test_batch.rs | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/contracts/streaming/src/test_batch.rs b/contracts/streaming/src/test_batch.rs index 891bb8b..0d09bc8 100644 --- a/contracts/streaming/src/test_batch.rs +++ b/contracts/streaming/src/test_batch.rs @@ -631,3 +631,40 @@ fn test_batch_rejects_contract_as_recipient() { let result = client.try_create_streams_batch(&t.sender, &inputs); assert_eq!(result, Err(Ok(StreamError::InvalidRecipient))); } + +// ─── Batch rejects dust/zero-rate streams ──────────────────────────────────── + +/// Test create_streams_batch() rejects a batch entry with a dust/zero-rate stream. +/// Issue #660: Add contract test for batch creation that rejects dust/zero-rate stream. +#[test] +fn test_batch_rejects_dust_stream() { + let t = TestEnv::setup(); + let now = 1_000_000u64; + t.set_time(now); + + // Create a batch where one entry is a dust stream (amount too small for duration) + let valid_recipient = Address::generate(&t.env); + let dust_recipient = Address::generate(&t.env); + let valid_amount = 1_000_0000000i128; + let dust_amount = 100i128; // Very small amount over a long duration + + t.approve(valid_amount + dust_amount); + + let mut inputs: Vec = Vec::new(&t.env); + // Valid stream + inputs.push_back(t.make_input(&valid_recipient, now)); + // Invalid dust stream: 100 stroops over 1 year = 0 per second + inputs.push_back(CreateStreamInput { + recipient: dust_recipient, + token: t.token_id.clone(), + total_amount: dust_amount, + start_time: now, + end_time: now + 31_536_000, // 1 year (31,536,000 seconds) + cliff_time: now, + cliff_amount: 0, + }); + + let client = t.client(); + let result = client.try_create_streams_batch(&t.sender, &inputs); + assert_eq!(result, Err(Ok(StreamError::RateIsZero))); +} From f2d8e60f523e7c9b9604c6f536b3f6c0055de92d Mon Sep 17 00:00:00 2001 From: soma-enyi Date: Sun, 30 Aug 2026 19:26:27 +0100 Subject: [PATCH 4/4] refactor: consolidate CreateStreamInput and CreateStreamParams structs (issue #661) --- contracts/streaming/src/lib.rs | 17 +------ contracts/streaming/src/test_batch.rs | 60 ++++++++++++------------ contracts/streaming/src/test_features.rs | 1 - 3 files changed, 31 insertions(+), 47 deletions(-) diff --git a/contracts/streaming/src/lib.rs b/contracts/streaming/src/lib.rs index bc3393f..776ca71 100644 --- a/contracts/streaming/src/lib.rs +++ b/contracts/streaming/src/lib.rs @@ -175,21 +175,6 @@ pub struct CreateStreamParams { pub cliff_amount: i128, } -/// Input parameters for a single stream in a batch creation call. -/// -/// Mirrors [`CreateStreamParams`] but is a distinct type so that it can be -/// evolved independently without affecting the single-stream API surface. -#[contracttype] -#[derive(Clone, Debug)] -pub struct CreateStreamInput { - pub recipient: Address, - pub token: Address, - pub total_amount: i128, - pub start_time: u64, - pub end_time: u64, - pub cliff_time: u64, - pub cliff_amount: i128, -} // ─── Errors ────────────────────────────────────────────────────────────────── @@ -663,7 +648,7 @@ impl StreamingContract { pub fn create_streams_batch( env: Env, sender: Address, - streams: Vec, + streams: Vec, ) -> Result, StreamError> { sender.require_auth(); Self::require_not_paused(&env)?; diff --git a/contracts/streaming/src/test_batch.rs b/contracts/streaming/src/test_batch.rs index 0d09bc8..97ab0a9 100644 --- a/contracts/streaming/src/test_batch.rs +++ b/contracts/streaming/src/test_batch.rs @@ -72,9 +72,9 @@ impl TestEnv { self.env.ledger().with_mut(|l| l.timestamp = timestamp); } - /// Build a valid [`CreateStreamInput`] pointing at `recipient`. - fn make_input(&self, recipient: &Address, now: u64) -> CreateStreamInput { - CreateStreamInput { + /// Build a valid [`CreateStreamParams`] pointing at `recipient`. + fn make_input(&self, recipient: &Address, now: u64) -> CreateStreamParams { + CreateStreamParams { recipient: recipient.clone(), token: self.token_id.clone(), total_amount: 1_000_0000000, // 1000 tokens @@ -112,7 +112,7 @@ fn test_batch_create_happy_path() { let total_approval = per_stream * 3; t.approve(total_approval); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); inputs.push_back(t.make_input(&r1, now)); inputs.push_back(t.make_input(&r2, now)); inputs.push_back(t.make_input(&r3, now)); @@ -172,7 +172,7 @@ fn test_batch_create_returns_ids_in_order() { let r2 = Address::generate(&t.env); t.approve(single_amount * 2); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); inputs.push_back(t.make_input(&r1, now)); inputs.push_back(t.make_input(&r2, now)); @@ -193,7 +193,7 @@ fn test_batch_sender_index_updated() { let per_stream = 1_000_0000000i128; t.approve(per_stream * 5); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); for _ in 0..5 { let r = Address::generate(&t.env); inputs.push_back(t.make_input(&r, now)); @@ -224,7 +224,7 @@ fn test_batch_recipient_indexes_updated() { let per_stream = 1_000_0000000i128; t.approve(per_stream * 3); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); inputs.push_back(t.make_input(&r_shared, now)); inputs.push_back(t.make_input(&r_shared, now)); inputs.push_back(t.make_input(&r_other, now)); @@ -259,10 +259,10 @@ fn test_batch_partial_failure_rejected_atomically() { let per_stream = 1_000_0000000i128; t.approve(per_stream * 2); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); inputs.push_back(t.make_input(&r1, now)); // valid // Invalid: zero amount - inputs.push_back(CreateStreamInput { + inputs.push_back(CreateStreamParams { recipient: r2.clone(), token: t.token_id.clone(), total_amount: 0, @@ -294,8 +294,8 @@ fn test_batch_accepts_past_start_and_cliff_times() { let per_stream = 1_000_0000000i128; t.approve(per_stream); - let mut inputs: Vec = Vec::new(&t.env); - inputs.push_back(CreateStreamInput { + let mut inputs: Vec = Vec::new(&t.env); + inputs.push_back(CreateStreamParams { recipient: r1, token: t.token_id.clone(), total_amount: per_stream, @@ -325,8 +325,8 @@ fn test_batch_invalid_time_range_fails() { let per_stream = 1_000_0000000i128; t.approve(per_stream); - let mut inputs: Vec = Vec::new(&t.env); - inputs.push_back(CreateStreamInput { + let mut inputs: Vec = Vec::new(&t.env); + inputs.push_back(CreateStreamParams { recipient: r1, token: t.token_id.clone(), total_amount: per_stream, @@ -350,8 +350,8 @@ fn test_batch_invalid_cliff_fails() { let per_stream = 1_000_0000000i128; t.approve(per_stream); - let mut inputs: Vec = Vec::new(&t.env); - inputs.push_back(CreateStreamInput { + let mut inputs: Vec = Vec::new(&t.env); + inputs.push_back(CreateStreamParams { recipient: r1, token: t.token_id.clone(), total_amount: per_stream, @@ -374,8 +374,8 @@ fn test_batch_self_stream_fails() { let per_stream = 1_000_0000000i128; t.approve(per_stream); - let mut inputs: Vec = Vec::new(&t.env); - inputs.push_back(CreateStreamInput { + let mut inputs: Vec = Vec::new(&t.env); + inputs.push_back(CreateStreamParams { recipient: t.sender.clone(), // self-stream token: t.token_id.clone(), total_amount: per_stream, @@ -400,7 +400,7 @@ fn test_batch_rejects_past_start_time() { let per_stream = 1_000_0000000i128; t.approve(per_stream * 2); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); inputs.push_back(t.make_input(&r1, now)); // Second stream backdates its start_time into the past. let mut bad = t.make_input(&r2, now); @@ -429,7 +429,7 @@ fn test_batch_max_size_exactly_20_succeeds() { let per_stream = 1_000_0000000i128; t.approve(per_stream * 20); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); for _ in 0..20 { let r = Address::generate(&t.env); inputs.push_back(t.make_input(&r, now)); @@ -449,7 +449,7 @@ fn test_batch_over_max_size_fails() { let per_stream = 1_000_0000000i128; t.approve(per_stream * 21); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); for _ in 0..21 { let r = Address::generate(&t.env); inputs.push_back(t.make_input(&r, now)); @@ -465,7 +465,7 @@ fn test_batch_empty_fails() { let now = 1_000_000u64; t.set_time(now); - let inputs: Vec = Vec::new(&t.env); + let inputs: Vec = Vec::new(&t.env); let result = t.client().try_create_streams_batch(&t.sender, &inputs); assert_eq!(result, Err(Ok(StreamError::BatchEmpty))); } @@ -482,7 +482,7 @@ fn test_batch_single_stream_works() { let amount = 1_000_0000000i128; t.approve(amount); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); inputs.push_back(t.make_input(&r, now)); let client = t.client(); @@ -506,8 +506,8 @@ fn test_batch_supports_cliff() { let cliff_amount = 100_0000000i128; t.approve(amount); - let mut inputs: Vec = Vec::new(&t.env); - inputs.push_back(CreateStreamInput { + let mut inputs: Vec = Vec::new(&t.env); + inputs.push_back(CreateStreamParams { recipient: r.clone(), token: t.token_id.clone(), total_amount: amount, @@ -547,7 +547,7 @@ fn test_batch_streams_are_independently_withdrawable() { let per_stream = 1_000_0000000i128; t.approve(per_stream * 2); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); inputs.push_back(t.make_input(&r1, now)); inputs.push_back(t.make_input(&r2, now)); @@ -582,7 +582,7 @@ fn test_batch_streams_are_independently_cancellable() { let per_stream = 1_000_0000000i128; t.approve(per_stream * 2); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); inputs.push_back(t.make_input(&r1, now)); inputs.push_back(t.make_input(&r2, now)); @@ -613,11 +613,11 @@ fn test_batch_rejects_contract_as_recipient() { let total_per_stream = 1_000_0000000i128; t.approve(total_per_stream * 2); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); // Valid stream inputs.push_back(t.make_input(&valid_recipient, now)); // Invalid stream with contract as recipient - inputs.push_back(CreateStreamInput { + inputs.push_back(CreateStreamParams { recipient: t.contract_id.clone(), token: t.token_id.clone(), total_amount: total_per_stream, @@ -650,11 +650,11 @@ fn test_batch_rejects_dust_stream() { t.approve(valid_amount + dust_amount); - let mut inputs: Vec = Vec::new(&t.env); + let mut inputs: Vec = Vec::new(&t.env); // Valid stream inputs.push_back(t.make_input(&valid_recipient, now)); // Invalid dust stream: 100 stroops over 1 year = 0 per second - inputs.push_back(CreateStreamInput { + inputs.push_back(CreateStreamParams { recipient: dust_recipient, token: t.token_id.clone(), total_amount: dust_amount, diff --git a/contracts/streaming/src/test_features.rs b/contracts/streaming/src/test_features.rs index cbc43b6..242ea15 100644 --- a/contracts/streaming/src/test_features.rs +++ b/contracts/streaming/src/test_features.rs @@ -7,7 +7,6 @@ use soroban_sdk::{ testutils::{Address as _, Ledger}, token::{Client as TokenClient, StellarAssetClient}, vec, Address, Env, - Address, Env, }; // ─── Test helpers ─────────────────────────────────────────────────────────────