feat: stream checkpoints, recipient hook, withdrawal cooldown, dispute lock (#458, #459, #460, #461) - #537
Merged
Chuks-coderr merged 4 commits intoAug 31, 2026
Conversation
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.
|
@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! 🚀 |
Contributor
Author
|
resolved |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the four assigned issues on the
streamcontract:#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.hook_function(stream_id, recipient, amount)is invoked onhook_contractviatry_invoke_contract— best-effort: a failing or missing hook never blocks the withdrawal, it just emitsRecipientHookFailedinstead ofRecipientHookSuccess.clear_recipient_hook,get_recipient_hook.#460 — Per-stream withdrawal cooldown
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; blockspause_stream,cancel_stream,transfer_recipient, andupdate_stream_rateon 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-wideemergency_pauseare 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
Streamfields, sinceStreamwas already at the 40-field cap for#[contracttype]structs (see below).Pre-existing breakage fixed first (separate commit)
Before any of the above,
maindid not compile —cargo check -p sorostream-streamfailed with 28 errors (duplicateStreamErrorvariants/discriminants exceeding the 50-variant cap,Streamat 43 fields exceeding the 40-field cap,create_streamexceeding Soroban's 10-parameter limit, several call sites out of sync with function signatures,get_protocol_statsusing stdVecAPIs unavailable onsoroban_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_taxadmin function are now#[ignore]d with an explanatory reason (implementing that function is its own, unrelated feature).Testing
cargo check -p sorostream-stream— cleancargo test -p sorostream-stream --lib— 47 passed, 2 ignored (documented pre-existing gap), 0 failedcargo 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— succeeds12 new integration tests cover each feature's happy path, error path, and interaction with
withdraw().Notes on design choices
create_streamwasn'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 newcreate_stream_with_cooldownentry point that delegates tocreate_streaminternally, matching the existing pattern ofcreate_stream_scheduled/create_stream_with_milestones.Streamstruct fields were avoided everywhere (checkpoints, the hook, the cooldown, the dispute lock) sinceStreamis 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 throughScValin this soroban-sdk version when nested inside another#[contracttype]struct field — it compiles undercargo checkbut breakscargo test's client codegen. Hit this while freeing up field-count headroom in the first commit; documented inline where it matters (StreamQueryFilter.status).