Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 1 addition & 16 deletions contracts/streaming/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────────────────

Expand Down Expand Up @@ -663,7 +648,7 @@ impl StreamingContract {
pub fn create_streams_batch(
env: Env,
sender: Address,
streams: Vec<CreateStreamInput>,
streams: Vec<CreateStreamParams>,
) -> Result<Vec<u64>, StreamError> {
sender.require_auth();
Self::require_not_paused(&env)?;
Expand Down
123 changes: 97 additions & 26 deletions contracts/streaming/src/test_batch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -112,7 +112,7 @@ fn test_batch_create_happy_path() {
let total_approval = per_stream * 3;
t.approve(total_approval);

let mut inputs: Vec<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = 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));
Expand Down Expand Up @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(t.make_input(&r1, now));
inputs.push_back(t.make_input(&r2, now));

Expand All @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
for _ in 0..5 {
let r = Address::generate(&t.env);
inputs.push_back(t.make_input(&r, now));
Expand Down Expand Up @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = 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));
Expand Down Expand Up @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = 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,
Expand Down Expand Up @@ -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<CreateStreamInput> = Vec::new(&t.env);
inputs.push_back(CreateStreamInput {
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(CreateStreamParams {
recipient: r1,
token: t.token_id.clone(),
total_amount: per_stream,
Expand Down Expand Up @@ -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<CreateStreamInput> = Vec::new(&t.env);
inputs.push_back(CreateStreamInput {
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(CreateStreamParams {
recipient: r1,
token: t.token_id.clone(),
total_amount: per_stream,
Expand All @@ -350,8 +350,8 @@ fn test_batch_invalid_cliff_fails() {
let per_stream = 1_000_0000000i128;
t.approve(per_stream);

let mut inputs: Vec<CreateStreamInput> = Vec::new(&t.env);
inputs.push_back(CreateStreamInput {
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(CreateStreamParams {
recipient: r1,
token: t.token_id.clone(),
total_amount: per_stream,
Expand All @@ -374,8 +374,8 @@ fn test_batch_self_stream_fails() {
let per_stream = 1_000_0000000i128;
t.approve(per_stream);

let mut inputs: Vec<CreateStreamInput> = Vec::new(&t.env);
inputs.push_back(CreateStreamInput {
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(CreateStreamParams {
recipient: t.sender.clone(), // self-stream
token: t.token_id.clone(),
total_amount: per_stream,
Expand All @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = 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);
Expand Down Expand Up @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
for _ in 0..20 {
let r = Address::generate(&t.env);
inputs.push_back(t.make_input(&r, now));
Expand All @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
for _ in 0..21 {
let r = Address::generate(&t.env);
inputs.push_back(t.make_input(&r, now));
Expand All @@ -465,7 +465,7 @@ fn test_batch_empty_fails() {
let now = 1_000_000u64;
t.set_time(now);

let inputs: Vec<CreateStreamInput> = Vec::new(&t.env);
let inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
let result = t.client().try_create_streams_batch(&t.sender, &inputs);
assert_eq!(result, Err(Ok(StreamError::BatchEmpty)));
}
Expand All @@ -482,7 +482,7 @@ fn test_batch_single_stream_works() {
let amount = 1_000_0000000i128;
t.approve(amount);

let mut inputs: Vec<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(t.make_input(&r, now));

let client = t.client();
Expand All @@ -506,8 +506,8 @@ fn test_batch_supports_cliff() {
let cliff_amount = 100_0000000i128;
t.approve(amount);

let mut inputs: Vec<CreateStreamInput> = Vec::new(&t.env);
inputs.push_back(CreateStreamInput {
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(CreateStreamParams {
recipient: r.clone(),
token: t.token_id.clone(),
total_amount: amount,
Expand Down Expand Up @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(t.make_input(&r1, now));
inputs.push_back(t.make_input(&r2, now));

Expand Down Expand Up @@ -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<CreateStreamInput> = Vec::new(&t.env);
let mut inputs: Vec<CreateStreamParams> = Vec::new(&t.env);
inputs.push_back(t.make_input(&r1, now));
inputs.push_back(t.make_input(&r2, now));

Expand All @@ -597,3 +597,74 @@ 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<CreateStreamParams> = Vec::new(&t.env);
// Valid stream
inputs.push_back(t.make_input(&valid_recipient, now));
// Invalid stream with contract as recipient
inputs.push_back(CreateStreamParams {
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)));
}

// ─── 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<CreateStreamParams> = 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(CreateStreamParams {
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)));
}
1 change: 0 additions & 1 deletion contracts/streaming/src/test_features.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ use soroban_sdk::{
testutils::{Address as _, Ledger},
token::{Client as TokenClient, StellarAssetClient},
vec, Address, Env,
Address, Env,
};

// ─── Test helpers ─────────────────────────────────────────────────────────────
Expand Down
14 changes: 14 additions & 0 deletions contracts/streaming/src/test_security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}