Skip to content

fix(approvals): execute approved stream action exactly once - #1788

Open
samkelly-dev wants to merge 6 commits into
Streampay-Org:mainfrom
samkelly-dev:fix/approval-execution
Open

samkelly-dev wants to merge 6 commits into
Streampay-Org:mainfrom
samkelly-dev:fix/approval-execution

Conversation

@samkelly-dev

Copy link
Copy Markdown
Contributor

Closes #1729

Summary of Changes

Shared Stream Action Executor (app/lib/stream-executor.ts):

Implemented executeStreamAction(streamId, action, options) which handles authoritative stream transitions (start, pause, stop, settle, withdraw).
Standardized state guards across routes, side fields (pausedAt, settlementTxHash, withdrawal, nextAction), audit logging with metadata.approvalId, and error handling (atomic rollback on Soroban settlement failures).
Achieved 95.4% statement coverage (>90% requirement).

Refactored Direct Lifecycle Routes:

Refactored /pause, /start, /stop, /settle, and /withdraw to delegate post-authorization transition execution to executeStreamAction.

Approval Execution on Threshold (app/api/streams/[id]/approvals/[approvalId]/approve/route.ts):

Protected execution inside withLock(streamId).
Replaced all mock comments and mock request IDs with correlation context IDs.
When the threshold is met (thresholdMet: true), executes the action exactly once, setting status: "executed" and executedAt.
Returns 409 APPROVAL_ALREADY_EXECUTED if a subsequent or concurrent vote arrives on an executed approval.
Handles state drift: if the stream state changed and can no longer transition when threshold is reached, marks the approval as status: "failed", failureReason: "ILLEGAL_TRANSITION", and returns 409 ILLEGAL_TRANSITION.
Enforces TTL expiry: approvals older than TTL return 410 APPROVAL_EXPIRED.
On transient errors (e.g. 502 SETTLEMENT_FAILED), the approval remains approved (not executed) and retryable.

Approval State Diagram & Lifecycle Documentation:

Documented the approval lifecycle in docs/adr/001-org-stream-ownership.md.
Added /api/streams/{id}/approvals and /api/streams/{id}/approvals/{approvalId}/approve endpoints to openapi.json and regenerated types via npm run openapi:generate.

Test Coverage & Verification:

Added comprehensive route tests in app/api/streams/[id]/approvals/[approvalId]/approve/route.test.ts covering all 8 scenarios from the specification:
Threshold of 2, two votes for pause -> stream paused, approval executed.
Third vote after execution -> 409 APPROVAL_ALREADY_EXECUTED, no duplicate mutation.
Two concurrent final votes (Promise.all) -> exactly one execution, one audit record.
Stream stopped before final vote -> approval failed, 409 ILLEGAL_TRANSITION, stream unchanged.
Expired approval -> 410 APPROVAL_EXPIRED.
Settle rejection -> approval stays approved (not executed), 502, retryable.
Direct pause route behavior unchanged.
Response request_id matches correlation context.
Verified that grep -n "In a real implementation" app/api/streams returns nothing.
Verified npm test, npm run lint, npm run build, and npm run openapi:check all pass.

@drips-wave

drips-wave Bot commented Oct 19, 2026

Copy link
Copy Markdown

@samkelly-dev 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

@greatest0fallt1me

Copy link
Copy Markdown
Contributor

Held: app/api/streams/[id]/settle/route.ts does not compile (a catch without try, undefined updatedStream/settlement, and executeStreamAction used without an import). Please rebase onto the latest default branch, address this, and make sure CI passes, then it can be merged.

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.

Execute the approved lifecycle action when a stream approval reaches its multi-sig threshold

2 participants