From 0e7931b20ad565f7fae13597a680ad53f1647beb Mon Sep 17 00:00:00 2001 From: Hakeem Kazeem Date: Fri, 26 Jun 2026 11:36:30 +0100 Subject: [PATCH] Replace panic! with Soroban contracterror enum --- contracts/streaming/src/lib.rs | 102 +++++++++++++++++------ contracts/streaming/src/test.rs | 16 ++-- contracts/streaming/src/test_security.rs | 26 +++--- 3 files changed, 96 insertions(+), 48 deletions(-) diff --git a/contracts/streaming/src/lib.rs b/contracts/streaming/src/lib.rs index d704cf1..e4c49ba 100644 --- a/contracts/streaming/src/lib.rs +++ b/contracts/streaming/src/lib.rs @@ -1,7 +1,9 @@ #![no_std] +use core::fmt::Error; + use soroban_sdk::{ - contract, contractimpl, contracttype, token, Address, Env, Vec, + Address, Env, Vec, contract, contracterror, contractimpl, contracttype, token }; // ─── Storage Keys ──────────────────────────────────────────────────────────── @@ -60,6 +62,19 @@ pub struct CreateStreamParams { // ─── Errors ────────────────────────────────────────────────────────────────── +#[contracterror] +#[derive(Copy, Clone, Debug)] +pub enum StreamError { + InvalidAmount = 1, + InvalidSchedule = 2, + InvalidCliff = 3, + StreamNotFound = 4, + StreamCancelled = 5, + Unauthorized = 6, + InvalidWithdrawAmount = 7, + StreamEnded = 8, +} + // ─── Events ─────────────────────────────────────────────────────────────────── #[soroban_sdk::contractevent] @@ -104,21 +119,24 @@ impl StreamingContract { /// `total_amount` of `token` via the token's `approve()` function. /// /// Returns the new stream's ID. - pub fn create_stream(env: Env, sender: Address, params: CreateStreamParams) -> u64 { + pub fn create_stream(env: Env, sender: Address, params: CreateStreamParams) -> Result { sender.require_auth(); // ── Validate params ────────────────────────────────────────────────── if params.total_amount <= 0 { - panic!("total_amount must be > 0"); + return Err(StreamError::InvalidAmount) } if params.end_time <= params.start_time { - panic!("end_time must be > start_time"); + return Err(StreamError::InvalidSchedule) + } if params.cliff_time < params.start_time || params.cliff_time > params.end_time { - panic!("cliff_time must be between start_time and end_time"); + return Err(StreamError::InvalidCliff) + } if params.cliff_amount < 0 || params.cliff_amount > params.total_amount { - panic!("cliff_amount must be between 0 and total_amount"); + return Err(StreamError::InvalidCliff) + } let duration = (params.end_time - params.start_time) as i128; @@ -168,7 +186,7 @@ impl StreamingContract { StreamCreatedEvent { stream_id: id, deposited_amount: stream.deposited_amount } .publish(&env); - id + Ok(id) } /// Top up an existing stream with additional funds. @@ -178,21 +196,28 @@ impl StreamingContract { /// /// The caller must have approved this contract to spend `additional_amount` /// of the stream's token before calling. - pub fn top_up(env: Env, stream_id: u64, additional_amount: i128) { - let mut stream = Self::load_stream(&env, stream_id); + pub fn top_up(env: Env, stream_id: u64, additional_amount: i128) -> Result<(), StreamError> { + let mut stream = match Self::load_stream(&env, stream_id) { + Ok(stream) => stream, + Err(e) => return Err(e) + }; + stream.sender.require_auth(); if stream.cancelled { - panic!("cannot top up a cancelled stream"); + return Err(StreamError::StreamCancelled) + } let now = env.ledger().timestamp(); if now >= stream.end_time { - panic!("cannot top up an ended stream"); + return Err(StreamError::StreamEnded) + } if additional_amount <= 0 { - panic!("additional_amount must be > 0"); + return Err(StreamError::InvalidAmount) + } // ── Send funds ─────────────────────────────────────────────────────── @@ -253,6 +278,8 @@ impl StreamingContract { new_amount_per_second, } .publish(&env); + + Ok(()) } // ── Write: Withdraw ────────────────────────────────────────────────────── @@ -261,20 +288,24 @@ impl StreamingContract { /// /// Only the recipient 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); + pub fn withdraw(env: Env, stream_id: u64, amount: i128) -> Result<(), StreamError> { + // 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.recipient.require_auth(); if stream.cancelled { - panic!("stream is cancelled"); + return Err(StreamError::StreamCancelled) } let now = env.ledger().timestamp(); let withdrawable = Self::withdrawable_amount(&stream, now); if amount <= 0 || amount > withdrawable { - panic!("invalid withdraw amount"); + return Err(StreamError::InvalidAmount) } stream.withdrawn_amount += amount; @@ -293,6 +324,8 @@ impl StreamingContract { ); WithdrawEvent { stream_id, amount }.publish(&env); + + Ok(()) } // ── Write: Cancel ──────────────────────────────────────────────────────── @@ -301,13 +334,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) { - let mut stream = Self::load_stream(&env, stream_id); + pub fn cancel(env: Env, stream_id: u64) -> Result<(), StreamError> { + // 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(); if stream.cancelled { - panic!("stream already cancelled"); + return Err(StreamError::StreamCancelled) } let now = env.ledger().timestamp(); @@ -349,20 +386,31 @@ impl StreamingContract { sender_refund: sender_gets_back, } .publish(&env); + + Ok(()) } // ── Read: Stream data ──────────────────────────────────────────────────── /// Get a stream by ID. - pub fn get_stream(env: Env, stream_id: u64) -> Stream { - Self::load_stream(&env, stream_id) + pub fn get_stream(env: Env, stream_id: u64) -> Result { + // 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) -> i128 { - let stream = Self::load_stream(&env, stream_id); + pub fn get_withdrawable(env: Env, stream_id: u64) -> Result { + // 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(); - Self::withdrawable_amount(&stream, now) + Ok(Self::withdrawable_amount(&stream, now)) } /// Get all stream IDs where `address` is the sender. @@ -386,17 +434,17 @@ 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) { - Self::load_stream(&env, stream_id); + let _ = Self::load_stream(&env, stream_id); Self::extend_stream_ttl(&env, stream_id); } // ── Internal helpers ───────────────────────────────────────────────────── - fn load_stream(env: &Env, id: u64) -> Stream { + fn load_stream(env: &Env, id: u64) -> Result { env.storage() .persistent() .get(&DataKey::Stream(id)) - .unwrap_or_else(|| panic!("stream not found")) + .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 f31d577..6f8dedb 100644 --- a/contracts/streaming/src/test.rs +++ b/contracts/streaming/src/test.rs @@ -130,7 +130,7 @@ fn test_create_stream_with_cliff() { } #[test] -#[should_panic(expected = "end_time must be > start_time")] +#[should_panic(expected = "Error(Contract, #2)")] fn test_create_stream_invalid_times() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -143,7 +143,7 @@ fn test_create_stream_invalid_times() { } #[test] -#[should_panic(expected = "total_amount must be > 0")] +#[should_panic(expected = "Error(Contract, #1)")] fn test_create_stream_zero_amount() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -208,7 +208,7 @@ fn test_withdraw_full_after_end() { } #[test] -#[should_panic(expected = "invalid withdraw amount")] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_too_much() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -292,7 +292,7 @@ fn test_cancel_midway() { } #[test] -#[should_panic(expected = "stream already cancelled")] +#[should_panic(expected = "Error(Contract, #5)")] fn test_cancel_twice() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -309,7 +309,7 @@ fn test_cancel_twice() { } #[test] -#[should_panic(expected = "stream is cancelled")] +#[should_panic(expected = "Error(Contract, #5)")] fn test_withdraw_from_cancelled_stream() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -454,7 +454,7 @@ fn test_top_up_mid_stream_recalculates_rate() { } #[test] -#[should_panic(expected = "cannot top up a cancelled stream")] +#[should_panic(expected = "Error(Contract, #5)")] fn test_top_up_cancelled_stream_panics() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -474,7 +474,7 @@ fn test_top_up_cancelled_stream_panics() { } #[test] -#[should_panic(expected = "cannot top up an ended stream")] +#[should_panic(expected = "Error(Contract, #8)")] fn test_top_up_ended_stream_panics() { let t = TestEnv::setup(); let now = 1_000_000u64; @@ -494,7 +494,7 @@ fn test_top_up_ended_stream_panics() { } #[test] -#[should_panic(expected = "additional_amount must be > 0")] +#[should_panic(expected = "Error(Contract, #1)")] fn test_top_up_zero_amount_panics() { let t = TestEnv::setup(); let now = 1_000_000u64; diff --git a/contracts/streaming/src/test_security.rs b/contracts/streaming/src/test_security.rs index 9566884..3400b06 100644 --- a/contracts/streaming/src/test_security.rs +++ b/contracts/streaming/src/test_security.rs @@ -161,14 +161,14 @@ fn test_auth_sender_cannot_withdraw() { // ═══════════════════════════════════════════════════════════════════ #[test] -#[should_panic(expected = "stream not found")] +#[should_panic(expected = "Error(Contract, #4)")] fn test_get_nonexistent_stream() { let ctx = Ctx::new(); ctx.client().get_stream(&9999); } #[test] -#[should_panic(expected = "stream not found")] +#[should_panic(expected = "Error(Contract, #4)")] fn test_withdraw_nonexistent_stream() { let ctx = Ctx::new(); ctx.set_time(1_000_000); @@ -176,7 +176,7 @@ fn test_withdraw_nonexistent_stream() { } #[test] -#[should_panic(expected = "stream not found")] +#[should_panic(expected = "Error(Contract, #4)")] fn test_cancel_nonexistent_stream() { let ctx = Ctx::new(); ctx.set_time(1_000_000); @@ -219,7 +219,7 @@ fn test_no_overdraw_multiple_partial_withdrawals() { } #[test] -#[should_panic(expected = "invalid withdraw amount")] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_more_than_withdrawable() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -231,7 +231,7 @@ fn test_withdraw_more_than_withdrawable() { } #[test] -#[should_panic(expected = "invalid withdraw amount")] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_zero() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -242,7 +242,7 @@ fn test_withdraw_zero() { } #[test] -#[should_panic(expected = "invalid withdraw amount")] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_negative_amount() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -480,7 +480,7 @@ fn test_nothing_withdrawable_before_cliff() { } #[test] -#[should_panic(expected = "invalid withdraw amount")] +#[should_panic(expected = "Error(Contract, #1)")] fn test_withdraw_before_cliff_panics() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -674,7 +674,7 @@ fn test_self_stream_withdraw() { // ═══════════════════════════════════════════════════════════════════ #[test] -#[should_panic(expected = "cliff_time must be between start_time and end_time")] +#[should_panic(expected = "Error(Contract, #3)")] fn test_cliff_before_start_time() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -696,7 +696,7 @@ fn test_cliff_before_start_time() { } #[test] -#[should_panic(expected = "cliff_time must be between start_time and end_time")] +#[should_panic(expected = "Error(Contract, #3)")] fn test_cliff_after_end_time() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -718,7 +718,7 @@ fn test_cliff_after_end_time() { } #[test] -#[should_panic(expected = "cliff_amount must be between 0 and total_amount")] +#[should_panic(expected = "Error(Contract, #3)")] fn test_cliff_amount_exceeds_total() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -740,7 +740,7 @@ fn test_cliff_amount_exceeds_total() { } #[test] -#[should_panic(expected = "cliff_amount must be between 0 and total_amount")] +#[should_panic(expected = "Error(Contract, #3)")] fn test_negative_cliff_amount() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -762,7 +762,7 @@ fn test_negative_cliff_amount() { } #[test] -#[should_panic(expected = "total_amount must be > 0")] +#[should_panic(expected = "Error(Contract, #1)")] fn test_negative_total_amount() { let ctx = Ctx::new(); let now = 1_000_000u64; @@ -782,7 +782,7 @@ fn test_negative_total_amount() { } #[test] -#[should_panic(expected = "end_time must be > start_time")] +#[should_panic(expected = "Error(Contract, #2)")] fn test_end_time_equals_start_time() { let ctx = Ctx::new(); let now = 1_000_000u64;