APPROVED FOR IMPLEMENTATION
The escrow contract supports two independent features:
- Escrow release: Immediate lump-sum payout when buyer releases escrowed funds
- Payment streams: Continuous token flow from sender to recipient at a configurable rate
Currently, when release() is called on an escrow, funds transfer directly to the farmer as a single atomic transfer. For subscription-style use cases (recurring harvest boxes, recurring services), a buyer may want escrowed funds to be paid out continuously over time rather than as a lump sum—especially if the farmer provides ongoing services.
The backend's subscriptions.js already models recurring payments off-chain. An on-chain equivalent would provide:
- Transparent audit trail on Stellar ledger
- Automated streaming without backend intervention
- Optionality at the transaction level (buyer can choose per-order)
A new public function alongside release():
pub fn release_to_stream(
env: Env,
order_id: u64,
platform_fee_bps: u32,
stream_rate_per_second: i128, // stroops/sec
stream_end_time: u64, // ledger timestamp
) -> Result<(), EscrowError>Behavior:
- Validate order exists, is Active, and caller is buyer (identical to
release()) - Deduct platform fee (identical to
release()) - Deduct cooperative royalty if applicable (identical to
release()) - Create a new
PaymentStreamin the stream.rs module with:- sender: this contract
- recipient: escrow.farmer
- rate_per_second: caller-provided rate
- deposit: farmer_amount (after fee/royalty)
- end_time: caller-provided timestamp
- Mark escrow as Released (identical to
release()) - Emit release event (identical to
release()) - Do NOT call reward token mint (streamed payouts complicate per-second reward timing)
Why this approach:
- Mirrors the contract's existing cross-contract call pattern (
try_invoke_contractin currentrelease()) - Reuses stream.rs's proven checkpoint/accrual logic
- Caller controls timing explicitly (rate + end_time), reducing trust on contract upgrades
- Supports immediate adoption: no off-chain coordination required
Add stream_params: Option<StreamParams> to the existing release() function.
Why rejected:
- Adds parameter explosion to an already complex function
- Mixes two distinct payoff models in one code path
- Harder to test and reason about
Document the design and ship only the spec.
Why rejected:
- Acceptance criteria expect a working implementation
- Stream infrastructure already exists; this is plumbing work
- Backend subscriptions route already models this; parity is valuable
release_to_stream()function inEscrowContract- Integration tests using
soroban.jstest helpers (see #975) - Docstring explaining streaming terms and reward behavior
- Reward token minting for streamed payouts (timing ambiguity)
- Stream cancellation from farmer side (escrow release is irreversible)
- Dynamic rate adjustment post-release (farmers may only decrease rates via stream API)
- Stream creation succeeds with valid rate and end_time
- Cooperative royalty correctly deducted before streaming
- Farmer can claim accrued streamed amounts mid-stream
- Dispute status blocks stream creation (identical to release)
- Deploy contracts, create escrow, release to stream, verify PaymentStream record exists
- Advance ledger time, verify accrued amount calculation
- Compare lump-sum vs. streamed payouts for identical inputs
No impact. This is a new entry point; existing release() remains unchanged.
-
Timestamp validation: Recommend that
stream_end_timemust be >env.ledger().timestamp()to prevent instant-end streams. This validation should happen before any state mutation. -
Reward tokens: Skipped because calculating per-second rewards would require on-contract reward logic (vs. off-contract tracking). Future enhancement can mint lump-sum rewards at stream end.
-
Fee deduction order: Platform fee is deducted first (fixed %), then cooperative royalty (% of remainder), matching existing
release(). This order is important for fair cooperative accounting.
Closes #973 (pending implementation)