Skip to content

feat: implement idempotency key system for blockchain transaction endpoints#65

Merged
DeFiVC merged 5 commits into
ChainLearnOfficial:mainfrom
oomokaro1:feat/idempotency-key-system
Jul 18, 2026
Merged

feat: implement idempotency key system for blockchain transaction endpoints#65
DeFiVC merged 5 commits into
ChainLearnOfficial:mainfrom
oomokaro1:feat/idempotency-key-system

Conversation

@oomokaro1

Copy link
Copy Markdown
Contributor

Closes #7

Type of Change

  • New feature (non-breaking change that adds functionality)

Summary

Blockchain transactions are irreversible. The current API has no idempotency mechanism for endpoints that submit on-chain transactions (POST /api/rewards/claim and POST /api/credentials/mint). If a client's HTTP request times out after the on-chain transaction succeeds, they have no safe way to retry without risking double-minting tokens or NFTs. This PR adds a complete idempotency key system to prevent duplicate on-chain transactions.

Motivation / Context

Fixes #7 — The timeout scenario described in the issue creates a race condition where clients receive a 504 but the on-chain transaction has already succeeded. Retrying causes panics on the Stellar contract. This idempotency system caches responses and detects duplicate requests, returning the original result on retry.

Detailed Changes

A. Database Schema (src/database/schema.ts)

  • Added idempotencyKeys table with: key (PK), userId (FK), endpoint, requestHash, responseBody, responseStatus, txHash, createdAt, expiresAt
  • Added expiry index for efficient cleanup queries
  • Migration: src/database/migrations/0003_add_idempotency_keys.sql

B. Idempotency Middleware (src/middleware/idempotency.ts)

  • checkIdempotency(key, userId, endpoint, requestBody) — checks for existing key, returns cached response if request hash matches, throws ConflictError if key reused with different body, inserts new key otherwise
  • storeIdempotentResponse(key, status, body, txHash?) — updates key with response data after processing
  • cleanupIdempotencyKeys() — deletes expired keys (older than 24h)

C. Updated Route Handlers

  • POST /api/rewards/claim — now requires idempotencyKey (16-64 chars) in request body
  • POST /api/credentials/mint — now requires idempotencyKey (16-64 chars) in request body
  • Both controllers check idempotency before processing and store responses after completion
  • On error, the error response is also cached to prevent re-processing

D. Cleanup Job (src/jobs/cleanup-idempotency.ts)

  • Runs hourly to purge expired idempotency keys
  • Integrated into server startup/shutdown lifecycle

Testing

Unit Tests (tests/unit/services/idempotency.test.ts)

  • 8 tests covering all idempotency middleware functions
  • Tests: cached response return, new key insertion, conflict detection, response storage, cleanup

E2E Tests

  • tests/e2e/rewards.test.ts — 3 new tests: missing idempotency key rejection, short key rejection, unauthenticated with key
  • tests/e2e/credentials.test.ts — 2 new tests: missing key rejection, unauthenticated with key

Verification

  • npm run lint — 0 errors (73 pre-existing warnings)
  • npm run typecheck — clean
  • npm test — 128/129 pass (1 pre-existing timeout in quizzes.test.ts)

Tradeoffs

  • Client-generated keys: Idempotency keys are client-generated (like Stripe) rather than server-generated, giving clients control over retry semantics
  • 24h expiry: Keys expire after 24 hours, balancing storage cleanup with reasonable retry windows
  • Error caching: Error responses are cached alongside successes, preventing re-processing of known failures

Out of Scope

  • Client-side implementation (localStorage key management) — documented in issue but not part of this API change
  • Redis-based idempotency (used PostgreSQL for consistency with existing patterns)
  • Stellar tx hash lookup on retry with no stored result (edge case, contract panic serves as safety net)

Breaking Changes

Yes — The POST /api/rewards/claim and POST /api/credentials/mint endpoints now require an idempotencyKey field in the request body (16-64 character string). Existing clients must update to include this field.

Checklist

Self-Review

  • I have read the entire diff line by line as if a stranger wrote it
  • No debug code remains
  • No hardcoded secrets, tokens, API keys, or internal URLs
  • Naming is consistent with the existing codebase
  • Error handling is present and produces meaningful messages
  • Edge cases are addressed
  • No unused imports, dead code, or unnecessary dependencies

Testing

  • All existing tests pass locally (except pre-existing timeout)
  • New tests added for new logic
  • Edge cases and failure paths are tested

CI / Pipeline

  • All CI checks are passing (build, lint, test, type-check)

Security

  • User input is validated and sanitized at trust boundaries
  • No SQL injection, XSS, or injection vulnerabilities introduced

Add new table for tracking idempotency keys on blockchain transaction
endpoints. Includes foreign key to users, request hash for body
validation, response caching columns, and expiry index for cleanup.
Add checkIdempotency, storeIdempotentResponse, and cleanupIdempotencyKeys
functions. Prevents duplicate on-chain transactions by caching responses
and detecting reused keys with different request bodies.
Add idempotencyKey field to claim reward and mint credential request
schemas. Controllers now check idempotency before processing and store
responses after completion, enabling safe retry behavior for clients.
Cleanup runs hourly to purge idempotency keys older than 24 hours.
Integrated into server startup and graceful shutdown lifecycle.
Unit tests cover checkIdempotency, storeIdempotentResponse, and
cleanupIdempotencyKeys with mocked database. E2E tests verify
idempotency key validation on reward claim and credential mint endpoints.

@DeFiVC DeFiVC left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: Approve

Excellent implementation of the idempotency key system.

Architecture looks solid:

  • Clean separation: schema → middleware → controllers → cleanup job
  • Client-generated keys (Stripe pattern) with 16-64 char validation
  • checkIdempotency handles all three cases correctly: cached hit, new key, conflict on reuse
  • Error responses cached alongside successes — prevents re-processing known failures
  • Cleanup job with proper lifecycle integration (start/stop)

Testing is thorough:

  • 8 unit tests covering all middleware functions (cached response, new key, conflict, response storage, cleanup)
  • 5 E2E tests verifying validation on both /rewards/claim and /credentials/mint
  • CI green on both Lint & Typecheck and Test

PR quality:

  • 5 commits showing clear iterative development (schema → middleware → controllers → cleanup → tests)
  • Detailed description with tradeoffs, breaking changes, and self-review checklist
  • Clean scope — no unrelated changes

Approving.

@DeFiVC
DeFiVC merged commit 665ca7b into ChainLearnOfficial:main Jul 18, 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.

[Expert] Implement idempotency key system for all blockchain transaction endpoints

2 participants