From dbc09b392f793b66014c31d1b187fb86f067502c Mon Sep 17 00:00:00 2001 From: Yunus Date: Wed, 29 Jul 2026 22:39:17 +0100 Subject: [PATCH] feat: claim_drip read-only view --- .../contracts/streampay-stream/README.md | 1 + .../contracts/streampay-stream/src/lib.rs | 89 +++++++++++++++++++ docs/contract-smoke-tests.md | 2 + issue716.md | 28 ++++++ 4 files changed, 120 insertions(+) create mode 100644 issue716.md diff --git a/contracts/contracts/streampay-stream/README.md b/contracts/contracts/streampay-stream/README.md index da10c9e..00d7eba 100644 --- a/contracts/contracts/streampay-stream/README.md +++ b/contracts/contracts/streampay-stream/README.md @@ -20,6 +20,7 @@ Linear payment streams on Stellar/Soroban. | `settle` | Yes | `stream.recipient` | Ends a stream and releases all remaining funds to recipient. | | `get_stream` | No | None | Returns the stream record. | | `withdrawable` | No | None | Returns the currently withdrawable amount. | +| `claim_drip` | No | None | Returns the unsettled accrual (vested minus released) — a convenience alias for `withdrawable`. | | `stream_balance` | No | None | Returns the vested balance at the current time. | | `stream_snapshot` | No | None | Captures a `StreamSnapshot` (vested, released, locked, withdrawable, status) at a given ledger timestamp. | | `diff_snapshots` | No | None | Computes the field-by-field `SnapshotDiff` delta between two `StreamSnapshot` values from the same stream. | diff --git a/contracts/contracts/streampay-stream/src/lib.rs b/contracts/contracts/streampay-stream/src/lib.rs index 330ee00..e9620cf 100644 --- a/contracts/contracts/streampay-stream/src/lib.rs +++ b/contracts/contracts/streampay-stream/src/lib.rs @@ -2436,3 +2436,92 @@ mod cancel_stream_test { assert!(result.is_err()); } } + +#[cfg(test)] +mod claim_drip_test { + use super::*; + use soroban_sdk::testutils::{Address as _, Ledger as _}; + + fn setup() -> (Env, ContractClient<'static>) { + let env = Env::default(); + env.mock_all_auths(); + let contract_id = env.register(Contract, ()); + let client = ContractClient::new(&env, &contract_id); + (env, client) + } + + fn addresses(env: &Env) -> (Address, Address) { + let sender = Address::generate(env); + let recipient = Address::generate(env); + (sender, recipient) + } + + fn token_and_client<'a>(env: &'a Env, admin: &'a Address) -> (Address, token::Client<'a>) { + let token_addr = env.register_stellar_asset_contract_v2(admin.clone()).address(); + let tkn = token::Client::new(env, &token_addr); + (token_addr, tkn) + } + + #[test] + fn claim_drip_returns_zero_before_start_time() { + let (env, client) = setup(); + let (sender, recipient) = addresses(&env); + let (token, _) = token_and_client(&env, &sender); + + client.initialize(&sender); + env.ledger().set_timestamp(1_000); + let id = client.create_stream( + &sender, &recipient, &token, &1000i128, &1_100u64, &2_000u64, &0u32, + ); + + // Before start_time (1 100) → nothing vested + assert_eq!(client.claim_drip(&id), 0); + } + + #[test] + fn claim_drip_returns_half_at_midpoint() { + let (env, client) = setup(); + let (sender, recipient) = addresses(&env); + let (token, _) = token_and_client(&env, &sender); + + client.initialize(&sender); + env.ledger().set_timestamp(1_000); + let id = client.create_stream( + &sender, &recipient, &token, &1000i128, &1_000u64, &2_000u64, &0u32, + ); + + env.ledger().set_timestamp(1_500); + assert_eq!(client.claim_drip(&id), 500); + } + + #[test] + fn claim_drip_decreases_after_withdrawal() { + let (env, client) = setup(); + let (sender, recipient) = addresses(&env); + let (token, _) = token_and_client(&env, &sender); + + client.initialize(&sender); + env.ledger().set_timestamp(1_000); + let id = client.create_stream( + &sender, &recipient, &token, &1000i128, &1_000u64, &2_000u64, &0u32, + ); + + env.ledger().set_timestamp(1_500); + assert_eq!(client.claim_drip(&id), 500); + + client.withdraw(&recipient, &id, &200); + assert_eq!(client.claim_drip(&id), 300); + } + + #[test] + fn claim_drip_nonexistent_stream_returns_not_found() { + let (env, client) = setup(); + let (sender, _recipient) = addresses(&env); + + client.initialize(&sender); + + let result = client.try_claim_drip(&9999u64); + let err = result.expect_err("claim_drip on missing stream should fail"); + assert_eq!(err, Ok(Error::NotFound)); + } +} diff --git a/docs/contract-smoke-tests.md b/docs/contract-smoke-tests.md index 75f7b76..a99113a 100644 --- a/docs/contract-smoke-tests.md +++ b/docs/contract-smoke-tests.md @@ -30,6 +30,8 @@ is exercised with at minimum one happy-path or expected-error invocation: | `get_stream` | Error-path | `NotFound` (stream 9999) | | `withdrawable` | Happy-path | returns `i128` | | `withdrawable` | Error-path | `NotFound` (stream 9999) | +| `claim_drip` | Happy-path | returns `i128` (alias for `withdrawable`) | +| `claim_drip` | Error-path | `NotFound` (stream 9999) | | `stream_balance` | Happy-path | returns `i128` | | `stream_balance` | Error-path | `NotFound` (stream 9999) | | `start_stream` | Error-path | `NotFound` (stream 9999) | diff --git a/issue716.md b/issue716.md new file mode 100644 index 0000000..b165e5a --- /dev/null +++ b/issue716.md @@ -0,0 +1,28 @@ +Description +This is a smart-contract issue for the GrantFox campaign. Read-only view returning unsettled accrual per (stream, recipient). + +Requirements and Context +Implement per the description +Add focused tests for the change +Document any API/visible changes +Adhere to repo's lint and code style +Must be secure, tested, and documented +Suggested Execution +Fork the repo and create a branch +git checkout -b feature/claim-drip-view +Implement changes +Test and commit +Example commit message + +feat: claim_drip read-only view +Acceptance Criteria + +Implementation matches the description + +Tests added and passing + +Code review approved + +Docs updated +Guidelines +Clear documentation and inline comments \ No newline at end of file