Skip to content

feat(escrow): add idempotency layer for payment operations #218 - #235

Merged
SudiptaPaul-31 merged 1 commit into
Lumina-eX:mainfrom
doncross03:fix-issue-218-idempotency-layer
Sep 27, 2026
Merged

SudiptaPaul-31 merged 1 commit into
Lumina-eX:mainfrom
doncross03:fix-issue-218-idempotency-layer

Conversation

@doncross03

Copy link
Copy Markdown
Contributor

Overview

This PR introduces an idempotency layer for escrow payment operations so blockchain/payment requests are processed exactly once. It prevents duplicate escrow creation, double payment releases and inconsistent financial records caused by retries or concurrent requests.

Related Issue

Closes #218

Changes

🔐 Idempotency core

  • [ADD] lib/db/migrations/010_idempotency_records.sql
    • idempotency_records table with idempotencyKey, operationType, requestHash, requestPayload, responsePayload, responseStatus, status, createdAt, expiresAt.
    • Unique (idempotency_key, operation_type) constraint + expiry index for TTL cleanup.
    • Partial unique index on escrow_transaction_logs(transaction_hash) for deposit / milestone_release / refund.
  • [ADD] lib/idempotency/
    • validation.ts — key extraction (Idempotency-Key header or idempotencyKey body), format validation and order-independent SHA-256 payload hashing.
    • repository.ts — atomic INSERT … ON CONFLICT claim, plus complete / markFailed / remove / purgeExpired.
    • service.ts — claim → execute → store/replay lifecycle with typed storage errors.
    • middleware.ts — withIdempotency() route wrapper.
    • errors.ts — machine-readable codes: IDEMPOTENCY_KEY_REQUIRED, IDEMPOTENCY_KEY_INVALID, IDEMPOTENCY_KEY_REUSED, IDEMPOTENCY_IN_PROGRESS, IDEMPOTENCY_STORAGE_ERROR.

🔗 Route integration

  • [MODIFY] app/api/escrow/create/route.ts, app/api/escrow/fund/route.ts, app/api/escrow/release/route.ts, app/api/escrow/refund/route.ts
    • Require a valid idempotency key and replay stored responses. The wrapper is composed inside the auth/RBAC middleware so unauthenticated requests never claim a key.

🧹 Retention & configuration

  • [ADD] scripts/idempotency-cleanup.ts + pnpm idempotency:cleanup
  • [MODIFY] env.example — documents IDEMPOTENCY_TTL_HOURS (default 24).
  • [MODIFY] package.json — adds the cleanup command.

🧪 Tests

  • [ADD] __tests__/idempotency/validation.test.ts, service.test.ts, middleware.test.ts, repository.test.ts

Verification Results

pnpm exec vitest run __tests__/idempotency
✅ 4 files, 36/36 passed

pnpm exec eslint lib/idempotency scripts/idempotency-cleanup.ts app/api/escrow __tests__/idempotency
✅ 0 errors

pnpm build
✅ compiled successfully (Next.js + TypeScript type check)

Acceptance Criteria

Acceptance Criteria Status
Works across retries — repeated requests with the same key return the original result ✅ Successful responses are stored and replayed; Idempotency-Replayed: true header on replay
Handles concurrent requests safely via DB constraints / locks ✅ Single atomic INSERT … ON CONFLICT claim on the unique (key, operation) constraint; duplicates receive 409 IDEMPOTENCY_IN_PROGRESS with Retry-After
Expired idempotency records are cleaned up after a configurable TTL ✅ IDEMPOTENCY_TTL_HOURS (default 24, clamped 1–720) + pnpm idempotency:cleanup
Atomicity — payment operation and idempotency record creation ✅ Key is claimed in one atomic statement before any work and the result persisted in a single statement (the external on-chain call cannot share the DB transaction, so the industry-standard claim → execute → store pattern is used)
Clear error codes for missing/invalid idempotency keys ✅ IDEMPOTENCY_KEY_REQUIRED / IDEMPOTENCY_KEY_INVALID (400), IDEMPOTENCY_KEY_REUSED / IDEMPOTENCY_IN_PROGRESS (409)
Prevent duplicate escrow/payment records at the database level ✅ Unique (idempotency_key, operation_type) + partial unique index on payment transaction hashes

Notes for reviewers

  • Run pnpm migrate to apply 010_idempotency_records.sql.
  • The new partial unique index on escrow_transaction_logs will fail to create if the target database already contains duplicate payment hashes — worth checking before deploy.
  • Failed (non-2xx) attempts release the claim so clients can safely retry with the same key; only 2xx responses are cached.

Ensure blockchain/payment requests are processed exactly once so retries or concurrent requests cannot create duplicate escrow contracts, double releases or inconsistent financial records.

- Add idempotency_records migration with a unique (idempotency_key, operation_type) constraint, TTL index and a partial unique index on escrow_transaction_logs payment hashes.
- Add lib/idempotency (validation, hashing, repository, service, withIdempotency route middleware) exposing clear IDEMPOTENCY_* error codes.
- Wire idempotency into /api/escrow create, fund, release and refund routes.
- Cache successful responses and replay them; release the claim on failure so retries work; reject concurrent duplicates with 409.
- Add configurable TTL (IDEMPOTENCY_TTL_HOURS) and an idempotency:cleanup script.
- Add unit tests covering validation, service, middleware and repository.
@drips-wave

drips-wave Bot commented Sep 26, 2026

Copy link
Copy Markdown

@doncross03 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

@SudiptaPaul-31
SudiptaPaul-31 merged commit 42934b4 into Lumina-eX:main Sep 27, 2026
2 checks passed
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.

[Feature]: Idempotency Layer for Payment Operations

2 participants