feat(#217): add recurring payment scheduling via time-locked stream chains - #649
Open
Yinklekay wants to merge 1 commit into
Open
feat(#217): add recurring payment scheduling via time-locked stream chains#649Yinklekay wants to merge 1 commit into
Yinklekay wants to merge 1 commit into
Conversation
…d stream chains Adds an open_recurring_stream entry-point that automatically chains successive billing cycles on-chain, enabling true subscription/recurring payments without manual top-up or close-and-reopen. - RecurringStreamConfig (cycle_ledgers, amount_per_cycle, max_cycles, cycles_completed) stored under a new DataKey::RecurringStreamConfig(u32). - advance_recurring_stream: permissionless to invoke, marks a fully-claimed cycle complete and deducts the next deposit from the payer (payer auth required) until max_cycles is reached. - get_recurring_status: read-only cycles_completed / cycles_remaining view. - Emits recurring_cycle_start / recurring_cycle_end events. - Bumps STORAGE_LAYOUT_VERSION 3 -> 4 and sweeps the new key in bump_all_ttls. - Adds 7 integration tests covering the multi-cycle lifecycle. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
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 automated recurring/subscription payments for
FinchippayContractvia a newopen_recurring_streamentry-point that reuses the existing per-ledger streaming mechanics but automatically chains successive billing cycles on-chain. Payers no longer need to manually top up or close-and-reopen a stream every billing period — the contract deducts each cycle's deposit from the payer on demand (with the payer's consent).Closes #217 · References #51 · Addresses the ROADMAP item "Subscription/recurring payments via Soroban streams."
Background & Problem
The contract already supports
open_stream,top_up_stream, andclose_stream. Subscription use cases (e.g. monthly SaaS billing) require the payer to manually top up or close-and-reopen the stream at every billing cycle, defeating the purpose of programmatic payments. There is no on-chain notion of "cycle N of M".Solution
A recurring stream is an ordinary
Streamplus a storedRecurringStreamConfig:open_recurring_stream(token, payer, recipient, rate_per_ledger, deposit_per_cycle, cycle_ledgers, max_cycles)recurring_cycle_start. Returns the stream id.advance_recurring_stream(stream_id)amount_per_cyclefrom the payer. Anyone may invoke it; payer authorization is required only when funds move.get_recurring_status(stream_id)cycles_completedandcycles_remaining.Cycle semantics
cycles_completedcounts fully-claimed cycles (starts at0).deposited == claimed, preventing the payer being charged while unclaimed value remains outstanding.advance_recurring_streammarks the depleted cycle complete (cycles_completed += 1, emitsrecurring_cycle_end), then — ifcycles_completed < max_cycles— callspayer.require_auth(), transfersamount_per_cycle, top-upsstream.deposited(respectingMAX_STREAM_DEPOSIT), and emitsrecurring_cycle_start.cycles_completed == max_cycles, further advances panic with"max_cycles reached; no more advances".Files Changed
contracts/finchippay-contract/src/lib.rs— addedRecurringStreamConfigandRecurringStreamStatusstructs,DataKey::RecurringStreamConfig(u32)key, three entry-points, and bumpedSTORAGE_LAYOUT_VERSION3 → 4.contracts/finchippay-contract/src/streams.rs— implementedopen_recurring_stream,advance_recurring_stream,get_recurring_status.contracts/finchippay-contract/src/storage.rs— extended theTtlClass::Streamssweep to bumpRecurringStreamConfigentries.contracts/finchippay-contract/tests/integration.rs— added 7 integration tests.Security & Safety
open_recurring_streamrequirespayer.require_auth();advance_recurring_streamis permissionless to invoke but gates the deduction behindpayer.require_auth().require_not_paused.checked_add/checked_subwith explicit overflow/underflow panics.rate_per_ledger,deposit_per_cycle,cycle_ledgers, andmax_cyclesvalidated positive;MAX_STREAM_RATE/MAX_STREAM_DEPOSITrespected, including the cumulative cap on each advance.STORAGE_LAYOUT_VERSIONbumped per the documented policy (newDataKeyvariant).Acceptance Criteria
open_recurring_streamcreates a stream and stores the recurring configadvance_recurring_streamdeducts the next cycle's deposit from the payermax_cyclesis reached, no more advances are possibleget_recurring_statusreturnscycles_completedandcycles_remainingchecked_add/checked_subTests Added
test_open_recurring_stream— stream + config stored; initial status(0 completed, 3 remaining).test_advance_recurring_stream_deducts_next_cycle— drain cycle 1 → advance →depositeddoubled, payer debited.test_recurring_stream_stops_after_max_cycles— full 2-cycle lifecycle; further advance fails.test_advance_recurring_stream_requires_depleted_cycle— advancing an unclaimed stream fails.test_open_recurring_stream_emits_events—stream_open+recurring_cycle_start.test_advance_recurring_stream_emits_cycle_events—recurring_cycle_end+recurring_cycle_start.test_get_recurring_status_on_plain_stream_panics— status on a non-recurring stream fails.CI / Verification
Out of Scope