Skip to content

feat(#217): add recurring payment scheduling via time-locked stream chains - #649

Open
Yinklekay wants to merge 1 commit into
FinChippay:mainfrom
Yinklekay:feat/217-recurring-payment-streams
Open

feat(#217): add recurring payment scheduling via time-locked stream chains#649
Yinklekay wants to merge 1 commit into
FinChippay:mainfrom
Yinklekay:feat/217-recurring-payment-streams

Conversation

@Yinklekay

Copy link
Copy Markdown

Summary

Implements automated recurring/subscription payments for FinchippayContract via a new open_recurring_stream entry-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, and close_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 Stream plus a stored RecurringStreamConfig:

Entry-point Type Purpose
open_recurring_stream(token, payer, recipient, rate_per_ledger, deposit_per_cycle, cycle_ledgers, max_cycles) mutating Creates the stream, transfers the first cycle's deposit, stores the config, emits recurring_cycle_start. Returns the stream id.
advance_recurring_stream(stream_id) mutating Marks a fully-claimed cycle complete and, if cycles remain, deducts the next amount_per_cycle from the payer. Anyone may invoke it; payer authorization is required only when funds move.
get_recurring_status(stream_id) read-only Returns cycles_completed and cycles_remaining.

Cycle semantics

  • cycles_completed counts fully-claimed cycles (starts at 0).
  • A cycle is "depleted" only when deposited == claimed, preventing the payer being charged while unclaimed value remains outstanding.
  • advance_recurring_stream marks the depleted cycle complete (cycles_completed += 1, emits recurring_cycle_end), then — if cycles_completed < max_cycles — calls payer.require_auth(), transfers amount_per_cycle, top-ups stream.deposited (respecting MAX_STREAM_DEPOSIT), and emits recurring_cycle_start.
  • Once cycles_completed == max_cycles, further advances panic with "max_cycles reached; no more advances".

Files Changed

  • contracts/finchippay-contract/src/lib.rs — added RecurringStreamConfig and RecurringStreamStatus structs, DataKey::RecurringStreamConfig(u32) key, three entry-points, and bumped STORAGE_LAYOUT_VERSION 3 → 4.
  • contracts/finchippay-contract/src/streams.rs — implemented open_recurring_stream, advance_recurring_stream, get_recurring_status.
  • contracts/finchippay-contract/src/storage.rs — extended the TtlClass::Streams sweep to bump RecurringStreamConfig entries.
  • contracts/finchippay-contract/tests/integration.rs — added 7 integration tests.

Security & Safety

  • Authorizationopen_recurring_stream requires payer.require_auth(); advance_recurring_stream is permissionless to invoke but gates the deduction behind payer.require_auth().
  • Circuit breaker — both mutating entry-points call require_not_paused.
  • Checked arithmetic — all new math uses checked_add / checked_sub with explicit overflow/underflow panics.
  • Boundsrate_per_ledger, deposit_per_cycle, cycle_ledgers, and max_cycles validated positive; MAX_STREAM_RATE / MAX_STREAM_DEPOSIT respected, including the cumulative cap on each advance.
  • Atomicity — a panic reverts the whole transaction, so a partial advance can't leave funds locked-but-unrecorded.
  • Upgrade safetySTORAGE_LAYOUT_VERSION bumped per the documented policy (new DataKey variant).

Acceptance Criteria

  • open_recurring_stream creates a stream and stores the recurring config
  • After the first cycle is depleted, advance_recurring_stream deducts the next cycle's deposit from the payer
  • After max_cycles is reached, no more advances are possible
  • get_recurring_status returns cycles_completed and cycles_remaining
  • 7 new recurring-stream test cases (≥ 5 required)
  • All arithmetic uses checked_add / checked_sub

Tests Added

  1. test_open_recurring_stream — stream + config stored; initial status (0 completed, 3 remaining).
  2. test_advance_recurring_stream_deducts_next_cycle — drain cycle 1 → advance → deposited doubled, payer debited.
  3. test_recurring_stream_stops_after_max_cycles — full 2-cycle lifecycle; further advance fails.
  4. test_advance_recurring_stream_requires_depleted_cycle — advancing an unclaimed stream fails.
  5. test_open_recurring_stream_emits_eventsstream_open + recurring_cycle_start.
  6. test_advance_recurring_stream_emits_cycle_eventsrecurring_cycle_end + recurring_cycle_start.
  7. test_get_recurring_status_on_plain_stream_panics — status on a non-recurring stream fails.

CI / Verification

cd contracts/finchippay-contract
cargo check --target wasm32v1-none     # ✅ passed
cargo test                             # ✅ all passed (incl. 7 new tests)
cargo test --test integration          # ✅ passed
cargo build --target wasm32v1-none --release  # ✅ passed

Out of Scope

  • Cancelling individual future cycles
  • Variable-rate cycles

…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Issue #51 — Add recurring payment scheduling via time-locked stream chains

1 participant