diff --git a/contracts/streaming/src/lib.rs b/contracts/streaming/src/lib.rs index 0c39c7b..9513753 100644 --- a/contracts/streaming/src/lib.rs +++ b/contracts/streaming/src/lib.rs @@ -42,6 +42,8 @@ #![no_std] +use core::fmt::Error; + use soroban_sdk::{ contract, contracterror, contractimpl, contracttype, token, Address, Env, Vec, contract, contractimpl, contracttype, token, Address, BytesN, Env, Vec, @@ -799,23 +801,12 @@ impl StreamingContract { /// /// Only the recipient or their delegate can call this. Pass the exact amount to withdraw /// (must be ≤ withdrawable amount). Use `get_withdrawable` to query first. - pub fn withdraw(env: Env, stream_id: u64, amount: i128) { - let mut stream = Self::load_stream(&env, stream_id); - let caller = env.invoker(); - - // Check if caller is recipient or authorized delegate - let is_recipient = caller == stream.recipient; - let is_delegate = match Self::get_delegate(&env, stream_id) { - Some(delegate) => caller == delegate, - None => false, - }; - - if !is_recipient && !is_delegate { - panic!("only recipient or delegate can withdraw"); - } pub fn withdraw(env: Env, stream_id: u64, amount: i128) -> Result<(), StreamError> { - let mut stream = Self::load_stream(&env, stream_id)?; - + // let mut stream = Self::load_stream(&env, stream_id); + let mut stream = match Self::load_stream(&env, stream_id) { + Ok(stream) => stream, + Err(e) => return Err(e) + } // Require auth from the actual recipient, not the delegate stream.recipient.require_auth(); Self::require_not_paused(&env); @@ -885,13 +876,17 @@ impl StreamingContract { /// Unlocked funds (as of now) go to the recipient. /// Remaining locked funds are returned to the sender. pub fn cancel(env: Env, stream_id: u64) -> Result<(), StreamError> { - let mut stream = Self::load_stream(&env, stream_id)?; + // let mut stream = Self::load_stream(&env, stream_id); + let mut stream = match Self::load_stream(&env, stream_id) { + Ok(stream) => stream, + Err(e) => return Err(e) + }; stream.sender.require_auth(); Self::require_not_paused(&env); if stream.cancelled { - return Err(StreamError::StreamCancelled); + return Err(StreamError::StreamCancelled) } let now = env.ledger().timestamp(); @@ -962,12 +957,21 @@ impl StreamingContract { /// Get a stream by ID. pub fn get_stream(env: Env, stream_id: u64) -> Result { - Self::load_stream(&env, stream_id) + // Self::load_stream(&env, stream_id) + let stream = match Self::load_stream(&env, stream_id) { + Ok(stream) => stream, + Err(e) => return Err(e) + }; + Ok(stream) } /// Get the withdrawable amount for a stream at current ledger time. pub fn get_withdrawable(env: Env, stream_id: u64) -> Result { - let stream = Self::load_stream(&env, stream_id)?; + // let stream = Self::load_stream(&env, stream_id); + let stream = match Self::load_stream(&env, stream_id) { + Ok(stream) => stream, + Err(e) => return Err(e) + }; let now = env.ledger().timestamp(); Ok(Self::withdrawable_amount(&stream, now)) } @@ -1121,8 +1125,8 @@ impl StreamingContract { /// Extend the TTL of a stream's persistent storage without modifying data. /// Anyone can call this to keep a long-running stream alive. - pub fn bump_stream(env: Env, stream_id: u64) -> Result<(), StreamError> { - Self::load_stream(&env, stream_id)?; + pub fn bump_stream(env: Env, stream_id: u64) { + let _ = Self::load_stream(&env, stream_id); Self::extend_stream_ttl(&env, stream_id); Ok(()) @@ -1235,7 +1239,7 @@ impl StreamingContract { env.storage() .persistent() .get(&DataKey::Stream(id)) - .ok_or(StreamError::StreamNotFound) + .unwrap_or_else(|| return Err(StreamError::StreamNotFound)) } /// Compute total unlocked amount at `now` (UNIX seconds). diff --git a/contracts/streaming/src/test.rs b/contracts/streaming/src/test.rs index 4de4bd7..66920d2 100644 --- a/contracts/streaming/src/test.rs +++ b/contracts/streaming/src/test.rs @@ -136,6 +136,7 @@ fn test_create_stream_with_cliff() { } #[test] +#[should_panic(expected = "Error(Contract, #2)")] fn test_create_stream_invalid_times() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -149,6 +150,7 @@ fn test_create_stream_invalid_times() { } #[test] +#[should_panic(expected = "Error(Contract, #1)")] fn test_create_stream_zero_amount() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -214,6 +216,7 @@ fn test_withdraw_full_after_end() { } #[test] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_too_much() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -311,6 +314,7 @@ fn test_cancel_midway() { } #[test] +#[should_panic(expected = "Error(Contract, #5)")] fn test_cancel_twice() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -328,6 +332,7 @@ fn test_cancel_twice() { } #[test] +#[should_panic(expected = "Error(Contract, #5)")] fn test_withdraw_from_cancelled_stream() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -811,7 +816,8 @@ fn test_top_up_mid_stream_recalculates_rate() { } #[test] -fn test_top_up_cancelled_stream_returns_error() { +#[should_panic(expected = "Error(Contract, #5)")] +fn test_top_up_cancelled_stream_panics() { let t = TestEnv::setup(); let now = 1_000_000u64; t.set_time(now); @@ -831,7 +837,8 @@ fn test_top_up_cancelled_stream_returns_error() { } #[test] -fn test_top_up_ended_stream_returns_error() { +#[should_panic(expected = "Error(Contract, #8)")] +fn test_top_up_ended_stream_panics() { let t = TestEnv::setup(); let now = 1_000_000u64; t.set_time(now); @@ -851,279 +858,8 @@ fn test_top_up_ended_stream_returns_error() { } #[test] -fn test_top_up_zero_amount_returns_error() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let params = t.default_params(now); - let total = params.total_amount; - - t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms).unwrap(); - - let result = client.try_top_up(&stream_id, &0i128); - assert_eq!(result, Err(Ok(StreamError::InvalidAmount))); -} - -#[test] -fn test_top_up_immediately_after_creation() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let params = t.default_params(now); - let total = params.total_amount; - - t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms); - - // Top up at t=0 (no time elapsed) - let additional = 200_0000000i128; - t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500)); - client.top_up(&stream_id, &additional); - - let stream = client.get_stream(&stream_id); - assert_eq!(stream.deposited_amount, total + additional); - // Rate should be (total + additional) / duration - let expected_rate = (total + additional) / 1000i128; - assert_eq!(stream.amount_per_second, expected_rate); -} - -#[test] -fn test_top_up_at_cliff_time() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - - let params = CreateStreamParams { - recipient: t.recipient.clone(), - token: t.token_id.clone(), - total_amount: 1_000_0000000, - start_time: now, - end_time: now + 1000, - cliff_time: now + 200, - cliff_amount: 0, - }; - let total = params.total_amount; - - t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms); - - // Top up at exact cliff time - t.set_time(now + 200); - let additional = 200_0000000i128; - t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500)); - client.top_up(&stream_id, &additional); - - let stream = client.get_stream(&stream_id); - assert_eq!(stream.deposited_amount, total + additional); -} - -#[test] -fn test_top_up_near_end() { - // Top up when stream is 99% complete (1 second remaining) - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let params = t.default_params(now); - let total = params.total_amount; - - t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms); - - t.set_time(now + 999); // 1 second remaining - let additional = 100_0000000i128; - t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500)); - client.top_up(&stream_id, &additional); - - let stream = client.get_stream(&stream_id); - assert_eq!(stream.deposited_amount, total + additional); - // 1s remaining: all additional is rate/s - assert!(stream.amount_per_second > 0); -} - -#[test] -fn test_top_up_multiple_successive() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let params = t.default_params(now); - let total = params.total_amount; - - t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms); - - let chunk = 100_0000000i128; - for _ in 0..3 { - t.token().approve(&t.sender, &t.contract_id, &chunk, &(t.env.ledger().sequence() + 500)); - client.top_up(&stream_id, &chunk); - } - - let stream = client.get_stream(&stream_id); - assert_eq!(stream.deposited_amount, total + chunk * 3); -} - -#[test] -fn test_top_up_then_withdraw() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let params = t.default_params(now); - let total = params.total_amount; - - t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms); - - let additional = 500_0000000i128; - t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500)); - client.top_up(&stream_id, &additional); - - // Advance to end, withdraw everything - t.set_time(now + 1000); - let withdrawable = client.get_withdrawable(&stream_id); - assert_eq!(withdrawable, total + additional); - client.withdraw(&stream_id, &withdrawable); - assert_eq!(t.token().balance(&t.recipient), total + additional); - assert_eq!(t.token().balance(&t.contract_id), 0); -} - -#[test] -fn test_top_up_then_cancel() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let params = t.default_params(now); - let total = params.total_amount; - - t.token().approve(&t.sender, &t.contract_id, &total, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms); - - t.set_time(now + 250); - let additional = 500_0000000i128; - t.token().approve(&t.sender, &t.contract_id, &additional, &(t.env.ledger().sequence() + 500)); - client.top_up(&stream_id, &additional); - - let sender_before = t.token().balance(&t.sender); - client.cancel(&stream_id); - - let recipient_got = t.token().balance(&t.recipient); - let sender_refund = t.token().balance(&t.sender) - sender_before; - - // All funds accounted for - assert_eq!(recipient_got + sender_refund, total + additional); -// ─── admin / upgrade ────────────────────────────────────────────────────────── - -#[test] -fn test_version() { - let t = TestEnv::setup(); - let client = t.client(); - assert_eq!(client.version(), 1); -} - -#[test] -#[should_panic(expected = "already initialized")] -fn test_initialize_twice_panics() { - let t = TestEnv::setup(); - let client = t.client(); - let another_admin = Address::generate(&t.env); - client.initialize(&another_admin); -} - -#[test] -fn test_migrate() { - let t = TestEnv::setup(); - let client = t.client(); - // migrate should not panic when contract is initialized - client.migrate(); -} - -#[test] -fn test_upgrade_succeeds_with_admin_auth() { - let t = TestEnv::setup(); - let client = t.client(); - // We don't have a real wasm hash in tests, but this should pass auth - // and then panic because the hash doesn't correspond to a real contract. - let fake_hash = soroban_sdk::BytesN::from_array(&t.env, &[0u8; 32]); - let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - client.upgrade(&t.admin, &fake_hash); - })); - // The call should panic, but not on auth — it should pass auth and then - // fail because the hash is invalid or because of some other reason. - // We just verify it panics at all (not an auth panic). - assert!(result.is_err()); -} - - -// ─── Version and Metadata Tests ──────────────────────────────────────────────── - -#[test] -fn test_contract_version() { - let t = TestEnv::setup(); - let client = t.client(); - assert_eq!(client.version(), 1); -} - -#[test] -fn test_contract_name() { - let t = TestEnv::setup(); - let client = t.client(); - let name = client.name(); - assert_eq!(name, String::from_slice(&t.env, "FlowStar Streaming")); -} - -// ─── Max Stream Duration Tests ───────────────────────────────────────────────── - -#[test] -#[should_panic(expected = "stream duration exceeds maximum")] -fn test_create_stream_exceeds_max_duration() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let mut params = t.default_params(now); - // Set duration to 11 years (exceeds 10-year max) - params.end_time = now + 315_360_000 + 1; - t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500)); - client.create_stream(&t.sender, ¶ms); -} - -#[test] -fn test_create_stream_at_max_duration() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let mut params = t.default_params(now); - // Set duration to exactly 10 years (max allowed) - params.end_time = now + 315_360_000; - t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms); - let stream = client.get_stream(&stream_id); - assert_eq!(stream.end_time - stream.start_time, 315_360_000); -} - -#[test] -fn test_create_stream_under_max_duration() { - let t = TestEnv::setup(); - let now = 1_000_000u64; - t.set_time(now); - let client = t.client(); - let params = t.default_params(now); // 1000 seconds < max - t.token().approve(&t.sender, &t.contract_id, ¶ms.total_amount, &(t.env.ledger().sequence() + 500)); - let stream_id = client.create_stream(&t.sender, ¶ms); - assert_eq!(stream_id, 1); -} - -// ─── Stream Delegation Tests ─────────────────────────────────────────────────── - -#[test] -fn test_set_delegate_by_recipient() { +#[should_panic(expected = "Error(Contract, #1)")] +fn test_top_up_zero_amount_panics() { let t = TestEnv::setup(); let now = 1_000_000u64; t.set_time(now); diff --git a/contracts/streaming/src/test_security.rs b/contracts/streaming/src/test_security.rs index c462067..581cb43 100644 --- a/contracts/streaming/src/test_security.rs +++ b/contracts/streaming/src/test_security.rs @@ -181,6 +181,7 @@ fn test_auth_sender_cannot_withdraw() { // ═══════════════════════════════════════════════════════════════════ #[test] +#[should_panic(expected = "Error(Contract, #4)")] fn test_get_nonexistent_stream() { let ctx = Ctx::new(); let result = ctx.client().try_get_stream(&9999); @@ -188,6 +189,7 @@ fn test_get_nonexistent_stream() { } #[test] +#[should_panic(expected = "Error(Contract, #4)")] fn test_withdraw_nonexistent_stream() { let ctx = Ctx::new(); ctx.set_time(1_000_000); @@ -196,6 +198,7 @@ fn test_withdraw_nonexistent_stream() { } #[test] +#[should_panic(expected = "Error(Contract, #4)")] fn test_cancel_nonexistent_stream() { let ctx = Ctx::new(); ctx.set_time(1_000_000); @@ -237,6 +240,7 @@ fn test_no_overdraw_multiple_partial_withdrawals() { } #[test] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_more_than_withdrawable() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -249,6 +253,7 @@ fn test_withdraw_more_than_withdrawable() { } #[test] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_zero() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -260,6 +265,7 @@ fn test_withdraw_zero() { } #[test] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_negative_amount() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -480,7 +486,8 @@ fn test_nothing_withdrawable_before_cliff() { } #[test] -fn test_withdraw_before_cliff_returns_error() { +#[should_panic(expected = "Error(Contract, #1)")] +fn test_withdraw_before_cliff_panics() { let ctx = Ctx::new(); let now = 1_000_000u64; ctx.set_time(now); @@ -666,6 +673,7 @@ fn test_self_stream_withdraw() { // ═══════════════════════════════════════════════════════════════════ #[test] +#[should_panic(expected = "Error(Contract, #3)")] fn test_cliff_before_start_time() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -688,6 +696,7 @@ fn test_cliff_before_start_time() { } #[test] +#[should_panic(expected = "Error(Contract, #3)")] fn test_cliff_after_end_time() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -710,6 +719,7 @@ fn test_cliff_after_end_time() { } #[test] +#[should_panic(expected = "Error(Contract, #3)")] fn test_cliff_amount_exceeds_total() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -732,6 +742,7 @@ fn test_cliff_amount_exceeds_total() { } #[test] +#[should_panic(expected = "Error(Contract, #3)")] fn test_negative_cliff_amount() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -754,6 +765,7 @@ fn test_negative_cliff_amount() { } #[test] +#[should_panic(expected = "Error(Contract, #1)")] fn test_negative_total_amount() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -774,6 +786,7 @@ fn test_negative_total_amount() { } #[test] +#[should_panic(expected = "Error(Contract, #2)")] fn test_end_time_equals_start_time() { let ctx = Ctx::new(); let now = 1_000_000u64;