Skip to content

feat(security): implement idempotency key support for financial mutation endpoints #119

Description

@Josue19-08

🔖 Title

Implement idempotency key support for financial mutation endpoints


📄 Description

The four financial mutation endpoints have no idempotency protection:

  • `POST /loans/create`
  • `POST /loans/:id/pay`
  • `POST /liquidity/deposit`
  • `POST /liquidity/withdraw`

On network timeouts or client retries, the same request can be submitted twice before the blockchain confirms the first transaction. This can create duplicate loan records or apply the same payment twice. No `Idempotency-Key` header is documented, validated, or stored anywhere in the codebase.

The existing blockchain-level deduplication (unique constraints on `event_id` in the indexer) only protects after on-chain confirmation — it does not prevent two `PENDING` loan rows from being inserted before either is confirmed.


✅ Tasks to complete

  • Create `src/common/decorators/idempotency-key.decorator.ts` — extracts `Idempotency-Key` header from request; validates it is a UUID v4
  • Create `src/common/interceptors/idempotency.interceptor.ts` — on each request: generate cache key as `idempotency:{userId}:{endpoint}:{idempotency-key}`; if key exists in Redis, return cached response immediately without executing handler; otherwise execute handler and store response in Redis with a 24h TTL
  • Apply `@UseInterceptors(IdempotencyInterceptor)` and `@IdempotencyKey()` to `POST /loans/create` and `POST /loans/:id/pay` in `src/modules/loans/loans.controller.ts`
  • Apply the same to `POST /liquidity/deposit` and `POST /liquidity/withdraw` in `src/modules/liquidity/liquidity.controller.ts`
  • Add a DB-level guard in `src/modules/loans/loans.service.ts`: before inserting a new loan, check for an existing `PENDING` loan for the same user + same XDR hash to reject true duplicates even without a key
  • Document the `Idempotency-Key` header in all 4 endpoints via `@ApiHeader()` in Swagger
  • Add unit tests for `IdempotencyInterceptor` in `test/unit/common/interceptors/idempotency.interceptor.spec.ts`: first call executes and caches; second call with same key returns cache; different key executes fresh

📚 Documentation/context for AI

(This link never should removed)
https://github.com/TrustUp-app/TrustUp-API/tree/main/docs

Relevant files:

  • `src/modules/loans/loans.controller.ts`
  • `src/modules/loans/loans.service.ts`
  • `src/modules/liquidity/liquidity.controller.ts`
  • `src/common/interceptors/`
  • `src/common/decorators/`

🗒️ Additional notes

  • Redis is already configured via `CacheModule` with `cache-manager-redis-store` — reuse the same `CACHE_MANAGER` token
  • Cache key must include `userId` to prevent one user from replaying another user's idempotency key
  • If the handler throws, do NOT cache the error response — only cache successful (2xx) responses
  • Return a `X-Idempotent-Replayed: true` header on replayed responses so clients can detect them
  • Idempotency key should be optional (no header = no idempotency protection, for backwards compatibility)

Metadata

Metadata

Assignees

No one assigned

    Labels

    GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions