Skip to content

feat: stream checkpoints, recipient hook, withdrawal cooldown, dispute lock (#458, #459, #460, #461) - #537

Merged
Chuks-coderr merged 4 commits into
SoroStream:mainfrom
frienzy514-png:feat/458-459-460-461-stream-contract-features
Aug 31, 2026
Merged

feat: stream checkpoints, recipient hook, withdrawal cooldown, dispute lock (#458, #459, #460, #461)#537
Chuks-coderr merged 4 commits into
SoroStream:mainfrom
frienzy514-png:feat/458-459-460-461-stream-contract-features

Conversation

@frienzy514-png

@frienzy514-png frienzy514-png commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements the four assigned issues on the stream contract:

#458 — Stream checkpoint storage

  • checkpoint_stream(stream_id) — permissionless; records (ledger, timestamp, cumulative_withdrawn) if the configured interval (default 100 ledgers) has elapsed since the last checkpoint.
  • withdraw() also opportunistically checkpoints once due, so normal usage rarely needs an explicit call.
  • get_checkpoint_count, get_checkpoint(id, index), get_checkpoint_interval / set_checkpoint_interval (admin).

#459 — Recipient notification hook

  • set_recipient_hook(sender, stream_id, hook_contract, hook_function) — sender-only, only before any withdrawal has occurred.
  • On the stream's first withdrawal, hook_function(stream_id, recipient, amount) is invoked on hook_contract via try_invoke_contract — best-effort: a failing or missing hook never blocks the withdrawal, it just emits RecipientHookFailed instead of RecipientHookSuccess.
  • clear_recipient_hook, get_recipient_hook.

#460 — Per-stream withdrawal cooldown

  • New create_stream_with_cooldown(...) entry point (see note below on why it's separate) sets a minimum number of seconds between consecutive withdrawals by the recipient.
  • withdraw() enforces whichever of the per-stream cooldown and the existing protocol-wide cooldown is stricter.
  • get_stream_withdrawal_cooldown.

#461 — Dispute lock

  • lock_stream_for_dispute(stream_id, lock_duration_ledgers) — admin-only; blocks pause_stream, cancel_stream, transfer_recipient, and update_stream_rate on that stream until the ledger window elapses. No unlock call is needed — enforcement just compares against the current ledger sequence, so it expires automatically.
  • clear_dispute_lock (admin, lifts it early), is_dispute_locked.
  • withdraw() and the contract-wide emergency_pause are intentionally unaffected.

All four features are additive and opt-in — no existing entry point's default behavior changes. New state lives in dedicated storage keys rather than new Stream fields, since Stream was already at the 40-field cap for #[contracttype] structs (see below).

Pre-existing breakage fixed first (separate commit)

Before any of the above, main did not compile — cargo check -p sorostream-stream failed with 28 errors (duplicate StreamError variants/discriminants exceeding the 50-variant cap, Stream at 43 fields exceeding the 40-field cap, create_stream exceeding Soroban's 10-parameter limit, several call sites out of sync with function signatures, get_protocol_stats using std Vec APIs unavailable on soroban_sdk::Vec, and more), and the test suite couldn't compile at all on top of that. This is unrelated to #458–461 — it's baked into the repo's sole initial commit — but had to be fixed for anything here to be buildable or testable. See the first commit's message for the full list; it's isolated from the feature commit so it's easy to review separately.

Two tests that assumed an unimplemented set_creation_tax admin function are now #[ignore]d with an explanatory reason (implementing that function is its own, unrelated feature).

Testing

  • cargo check -p sorostream-stream — clean
  • cargo test -p sorostream-stream --lib47 passed, 2 ignored (documented pre-existing gap), 0 failed
  • cargo clippy -p sorostream-stream --lib --tests — 0 errors (a few pre-existing, unrelated warnings remain)
  • cargo build -p sorostream-stream --target wasm32-unknown-unknown --release — succeeds

12 new integration tests cover each feature's happy path, error path, and interaction with withdraw().

Notes on design choices

  • create_stream wasn't extended for Implement configurable withdrawal cooldown period between consecutive withdrawals #460. It was already at Soroban's hard 10-parameter-per-function limit (fixing that pre-existing overflow is part of the first commit). Rather than reshuffle its signature further, the cooldown ships as a new create_stream_with_cooldown entry point that delegates to create_stream internally, matching the existing pattern of create_stream_scheduled / create_stream_with_milestones.
  • New Stream struct fields were avoided everywhere (checkpoints, the hook, the cooldown, the dispute lock) since Stream is already at the 40-field cap. Also worth knowing for future work on this contract: Option<T> for a custom #[contracttype] (or a tuple) does not round-trip through ScVal in this soroban-sdk version when nested inside another #[contracttype] struct field — it compiles under cargo check but breaks cargo test's client codegen. Hit this while freeing up field-count headroom in the first commit; documented inline where it matters (StreamQueryFilter.status).

The stream contract's only commit shipped in a non-compiling state:
- errors.rs had duplicate StreamError variant names/discriminants and
  exceeded the 50-variant Soroban XDR cap for contracterror enums.
- Stream had 43 fields, exceeding the 40-field cap for #[contracttype]
  structs (Option<CustomType>/Option<tuple> struct fields also don't
  round-trip through ScVal in this soroban-sdk version, so the fix
  removes 3 always-inert fields — holdback_amount/holdback_claimed/
  is_dual_stream, none of which any public entry point ever set to a
  non-default value — rather than merging live fields into an Option).
- interface.rs had a duplicate create_stream parameter and a missing
  ProtocolStats import.
- create_stream exceeded the 10-parameter contract-function limit;
  trimmed renew_count/allow_recipient_termination (defaulted, matching
  what the existing test suite already assumed).
- Several call sites were out of sync with function signatures
  (stream_partial_cancelled, Self::create_stream, batch_create_stream),
  a few storage helpers were missing from lib.rs's import list, and
  get_protocol_stats used std Vec APIs unavailable on soroban_sdk::Vec.
- StreamQueryFilter.status: Option<StreamStatus> hit the same
  ScVal-Option limitation; changed to a documented Option<u32> code.

None of this touches issues SoroStream#458-461; it's a prerequisite so the crate
(and its test suite) builds at all. Two integration tests that assumed
an unimplemented set_creation_tax admin function are now #[ignore]d
with an explanatory reason rather than deleted.

All 37 previously-uncompilable tests now pass (2 ignored for the
documented gap above).
…e lock

Implements four independent stream-contract features (all opt-in, none
change existing entry-point behavior by default):

- SoroStream#458 Stream checkpoint storage: periodically records
  (ledger, timestamp, cumulative_withdrawn) snapshots for a stream.
  withdraw() opportunistically checkpoints once the configurable
  interval (default 100 ledgers) has elapsed; checkpoint_stream() lets
  anyone force one permissionlessly. New: checkpoint_stream,
  get_checkpoint_count, get_checkpoint, get/set_checkpoint_interval.

- SoroStream#459 Recipient notification hook: a sender-registered
  (contract, function) pair invoked with (stream_id, recipient, amount)
  the first time a stream is withdrawn from. Best-effort — a failing or
  missing hook contract never blocks the withdrawal, only emits
  RecipientHookFailed. New: set_recipient_hook, clear_recipient_hook,
  get_recipient_hook.

- SoroStream#460 Per-stream withdrawal cooldown: a new create_stream_with_cooldown
  entry point (create_stream itself is already at Soroban's 10-parameter
  function limit) sets a minimum ledger-time gap between withdrawals by
  the recipient. withdraw() enforces whichever of the per-stream and
  existing protocol-wide cooldown is stricter. New:
  create_stream_with_cooldown, get_stream_withdrawal_cooldown.

- SoroStream#461 Dispute lock: an admin-only, time-bounded lock that blocks
  pause_stream, cancel_stream, transfer_recipient, and
  update_stream_rate on a stream for a configurable ledger window,
  expiring automatically (no unlock call needed) since enforcement just
  compares against the current ledger sequence. withdraw() and the
  contract-wide emergency_pause are unaffected. New:
  lock_stream_for_dispute, clear_dispute_lock, is_dispute_locked.

All four are additive: new storage keys (not new Stream struct fields,
since Stream is already at the 40-field cap), new error variants
(DisputeLockActive, InvalidRecipientHook, CheckpointIntervalNotElapsed —
using the 3 slots freed by removing dead variants in the prior commit),
and new events. 12 new integration tests cover each feature's happy
path, error path, and interaction with withdraw(); full suite (47
tests) and clippy pass, and the wasm32 release build succeeds.
@drips-wave

drips-wave Bot commented Aug 30, 2026

Copy link
Copy Markdown

@frienzy514-png 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

@frienzy514-png

Copy link
Copy Markdown
Contributor Author

resolved

@Chuks-coderr
Chuks-coderr merged commit 3deae39 into SoroStream:main Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment