Skip to content

feat(streams): client-side amount validation, list() union, forceCancel + transferRecipient wrappers - #465

Merged
Jaydbrown merged 1 commit into
conduit-protocol:mainfrom
TemiMustapha:fix/streams-module-validations-and-wrappers
Aug 26, 2026
Merged

feat(streams): client-side amount validation, list() union, forceCancel + transferRecipient wrappers#465
Jaydbrown merged 1 commit into
conduit-protocol:mainfrom
TemiMustapha:fix/streams-module-validations-and-wrappers

Conversation

@TemiMustapha

@TemiMustapha TemiMustapha commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Four StreamsModule enhancements from the Stellar Wave program, each previously either silently wrong, unguarded, or only a prose TODO:

  1. Enhancement: withdraw()/topUp() don't validate amount > 0n client-side, unlike create()'s validation #451withdraw() / topUp() now reject amount <= 0n client-side, before any RPC round-trip, matching the fail-fast validation create() already applies (src/builder.ts philosophy: "mandatory client-side validation prevents invalid payloads from reaching the smart contract"). Zero/negative amounts previously went straight into the contract call and only failed after a full simulate+reject cycle surfacing StreamErrorCode.InvalidAmount.

  2. Enhancement: list() silently ignores recipient when both sender and recipient are provided #452list() no longer silently drops recipient when both sender and recipient are provided. The if (sender) { ... } else if (recipient) { ... } chain returned sender-only results with no indication anything was ignored. Both filters are now queried concurrently and merged into a de-duplicated union (streams where the address is either sender or recipient). Documented in docs/api.md and README.md.

  3. Enhancement: StreamsModule has no wrapper for force_cancel(), and StreamErrorCode.PauseThresholdNotMet is unreachable through the SDK #453 — New StreamsModule.forceCancel(streamId) wrapping the contract's DripStream::force_cancel(). Recipients can now force-cancel a stream paused beyond the 30-day threshold; it settles atomically like cancel() (earned tokens → recipient, remainder → sender). This also makes StreamErrorCode.PauseThresholdNotMet reachable through the SDK for the first time — previously nothing in streams.ts invoked force_cancel, so that error code could never be thrown via a public SDK call path.

  4. Enhancement: StreamsModule has no wrapper for transfer_recipient() #454 — New StreamsModule.transferRecipient(streamId, newRecipient) wrapping the contract's DripStream::transfer_recipient(). The current recipient can reassign the recipient role (including accrued withdrawable balance) without dropping to raw buildContractCallTx/invokeContract calls. Validates the new recipient address client-side.

Changes

src/streams.ts

  • withdraw(): guard amount !== undefined && amount <= 0n → throws 'Invalid amount: must be greater than zero'. The omitted-amount path (defaults to full withdrawable balance) is unaffected — a zero balance there still correctly surfaces NothingToWithdraw.
  • topUp(): guard amount <= 0n → throws before any RPC call (also covers topUpStream() and per-item batchWithdraw() failures).
  • list(): when both filters are present, fetch streamsBySender + streamsByRecipient concurrently and return [...new Set([...senderIds, ...recipientIds])].
  • forceCancel(streamId): _invokeforce_cancel (no args).
  • transferRecipient(streamId, newRecipient): validates a non-empty recipient, _invoketransfer_recipient with the new Address ScVal.
  • Updated JSDoc for list().

Tests (src/tests/streams.test.ts, src/tests/streams-success.test.ts)

  • Zero/negative amount rejection for withdraw, topUp, topUpStream, and per-item batchWithdraw failure reporting.
  • Omitted-amount withdraw() still defaults to the withdrawable balance.
  • list() with both filters: queries both factory methods with the same offset/limit, and returns the de-duplicated union.
  • forceCancel() / transferRecipient(): keypair guard, success paths returning the tx hash, transfer_recipient invoked with the new recipient address, and PauseThresholdNotMet / NotPaused mapped to ConduitError codes 13 / 10.
  • transferRecipient() empty / whitespace recipient rejection.

Docs

Verification

  • npx tsc --noEmit — clean
  • npx vitest run — 740 passed, 2 skipped (58 files)

Closes #451, Closes #452, Closes #453, Closes #454

…el + transferRecipient wrappers

- withdraw()/topUp() now reject amount <= 0n client-side before any RPC
  round-trip, matching create()'s fail-fast validation (conduit-protocol#451)
- list() returns the de-duplicated union when both sender and recipient
  filters are given instead of silently dropping recipient (conduit-protocol#452)
- add StreamsModule.forceCancel() wrapping DripStream::force_cancel,
  making StreamErrorCode.PauseThresholdNotMet reachable through the SDK (conduit-protocol#453)
- add StreamsModule.transferRecipient() wrapping DripStream::transfer_recipient (conduit-protocol#454)
- update docs (api.md, architecture.md, README, CHANGELOG) and add unit tests

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@drips-wave

drips-wave Bot commented Aug 25, 2026

Copy link
Copy Markdown

@TemiMustapha Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Jaydbrown

Copy link
Copy Markdown
Contributor

Thanks for the contribution here — squash-merging this now. Any follow-ups we'll track in a fresh issue. 🚀

@Jaydbrown
Jaydbrown merged commit 6d4d7b4 into conduit-protocol:main Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment