From 268a6f3fcefbe47432c6fffda90f2ecd153d922b Mon Sep 17 00:00:00 2001 From: ChainBid Developer Date: Tue, 28 Jul 2026 02:14:22 +0100 Subject: [PATCH 1/3] fix: validate fillAmount format before BigInt parsing Add e2e test asserting non-numeric fillAmount returns 400 with validation error, not an ungraceful 500 from BigInt SyntaxError. --- test/intents.e2e-spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/intents.e2e-spec.ts b/test/intents.e2e-spec.ts index 00c272b..a0d8efd 100644 --- a/test/intents.e2e-spec.ts +++ b/test/intents.e2e-spec.ts @@ -114,6 +114,21 @@ describe("IntentsController (e2e)", () => { }); }); + it("POST /api/v1/intents/:id/fill with non-numeric fillAmount returns 400", async () => { + const created = await createIntent({ user: "GFILLAMOUNT123456" }); + await request(app.getHttpServer()) + .post(`/api/v1/intents/${created.intentId}/accept`) + .send({ solver: "SOLVER_ALPHA" }) + .expect(201); + + const res = await request(app.getHttpServer()) + .post(`/api/v1/intents/${created.intentId}/fill`) + .send({ solver: "SOLVER_ALPHA", fillAmount: "abc", txHash: "e2e-hash" }) + .expect(400); + expect(res.body.error).toBe("Validation failed"); + expect(Array.isArray(res.body.details)).toBe(true); + }); + it("accept with an unknown/inactive solver is forbidden", async () => { const created = await createIntent({ user: "GUNKNOWNSOLVER12345" }); await request(app.getHttpServer()) From 93acdcb198262e1d1e456f57654fa841d64a3f06 Mon Sep 17 00:00:00 2001 From: ChainBid Developer Date: Tue, 28 Jul 2026 02:15:41 +0100 Subject: [PATCH 2/3] docs: add detailed PR description files for issues #64 and #65 --- PR_DESCRIPTION_64.md | 47 ++++++++++++++++++++++++++++++++++++++++++++ PR_DESCRIPTION_65.md | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 PR_DESCRIPTION_64.md create mode 100644 PR_DESCRIPTION_65.md diff --git a/PR_DESCRIPTION_64.md b/PR_DESCRIPTION_64.md new file mode 100644 index 0000000..abca768 --- /dev/null +++ b/PR_DESCRIPTION_64.md @@ -0,0 +1,47 @@ +# PR Description: Fix Pagination NaN Bug in IntentsController.list() + +## Issue Reference + +- Closes #64 + +## Summary + +`IntentsController.list()` was using `parseInt()` on raw query parameters (`limitRaw`, `offsetRaw`) without any validation. When a non-numeric value was provided (e.g., `?limit=abc`), `parseInt()` returned `NaN`, which silently corrupted the pagination slice (`intents.slice(offset, offset + limit)`) instead of returning a clear 400 error. + +## Changes + +### New File: `src/intents/dto/list-intents.dto.ts` + +Introduced a typed DTO for list query parameters using `class-validator` decorators consistent with the rest of the codebase: + +- `limit`: `@IsInt()`, `@Min(1)`, `@Max(100)` — ensures the value is a valid integer between 1 and 100 +- `offset`: `@IsInt()`, `@Min(0)` — ensures the value is a valid non-negative integer +- `state`, `user`, `chain`: `@IsOptional()`, `@IsString()` — optional string filters + +### Modified File: `src/intents/intents.controller.ts` + +- Changed `list()` method signature from individual `@Query()` parameters to a single `@Query() dto: ListIntentsDto` +- Replaced `Math.min(parseInt(limitRaw, 10), 100)` and `parseInt(offsetRaw, 10)` with `dto.limit` and `dto.offset` +- The global `ValidationPipe` (configured in `main.ts` with `whitelist: true, transform: true`) automatically validates the DTO and returns a 400 response with detailed error information when validation fails + +### Modified File: `test/intents.e2e-spec.ts` + +Added two new e2e tests: + +1. **`GET /api/v1/intents with non-numeric limit returns 400`** — Asserts that `?limit=abc` returns HTTP 400 with `error: "Validation failed"` and an array of `details` +2. **`GET /api/v1/intents with non-numeric offset returns 400`** — Asserts that `?offset=xyz` returns HTTP 400 with `error: "Validation failed"` and an array of `details` + +## Verification + +- `npm run lint` passes +- `npm run typecheck` passes +- `npm test` passes +- `npm run test:e2e` passes (new e2e tests for non-numeric limit/offset) + +## Acceptance Criteria + +- [x] Limit/offset moved into a proper DTO with `@IsInt()`/`@Min()`/`@Max()` validators +- [x] Non-numeric `limit` query param returns 400, not a silently broken page +- [x] Non-numeric `offset` query param returns 400, not a silently broken page +- [x] E2E tests added and passing +- [x] PR description includes "Closes #64" \ No newline at end of file diff --git a/PR_DESCRIPTION_65.md b/PR_DESCRIPTION_65.md new file mode 100644 index 0000000..fa1f2ac --- /dev/null +++ b/PR_DESCRIPTION_65.md @@ -0,0 +1,41 @@ +# PR Description: Guard BigInt(dto.fillAmount) Against Malformed Input + +## Issue Reference + +- Closes #65 + +## Summary + +`IntentsController.fill()` calls `BigInt(dto.fillAmount)` directly. While `FillIntentDto` already has `@Matches(/^\d+$/)` on `fillAmount` (matching the pattern used on `CreateIntentDto.srcAmount`), there was no e2e test asserting that a non-numeric `fillAmount` returns a clean 400 validation error instead of an ungraceful 500 `SyntaxError` from `BigInt()`. + +## Changes + +### Modified File: `test/intents.e2e-spec.ts` + +Added a new e2e test: + +**`POST /api/v1/intents/:id/fill with non-numeric fillAmount returns 400`** + +- Creates an intent and accepts it with a valid solver +- Submits a fill request with `fillAmount: "abc"` (non-numeric) +- Asserts HTTP 400 response with `error: "Validation failed"` and an array of `details` +- Confirms the response is a clean validation error, not an ungraceful 500 from `BigInt("abc")` throwing a `SyntaxError` + +## Background + +The `FillIntentDto` already includes the `@Matches(/^\d+$/)` decorator on `fillAmount`, which ensures only digit-only strings reach the `BigInt()` constructor. The `ValidationPipe` (configured globally in `main.ts` with `whitelist: true, transform: true`) intercepts invalid input before it reaches the controller method and returns a structured 400 response. + +This test closes the gap by verifying the end-to-end behavior: a malformed `fillAmount` query body is rejected at the validation layer with a clear 400 status. + +## Verification + +- `npm run lint` passes +- `npm run typecheck` passes +- `npm test` passes +- `npm run test:e2e` passes (new e2e test for non-numeric fillAmount) + +## Acceptance Criteria + +- [x] `FillIntentDto.fillAmount` has `@Matches(/^\d+$/)` pattern matching `CreateIntentDto.srcAmount` +- [x] E2E test added asserting non-numeric `fillAmount` returns 400 with validation error, not 500 +- [x] PR description includes "Closes #65" \ No newline at end of file From e1c2392f7f8c91171706deea0a0707e2d08f9c4d Mon Sep 17 00:00:00 2001 From: ChainBid Developer Date: Tue, 28 Jul 2026 02:25:26 +0100 Subject: [PATCH 3/3] docs: update PR description files with PR URLs --- PR_DESCRIPTION_64.md | 5 +++++ PR_DESCRIPTION_65.md | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/PR_DESCRIPTION_64.md b/PR_DESCRIPTION_64.md index abca768..6923410 100644 --- a/PR_DESCRIPTION_64.md +++ b/PR_DESCRIPTION_64.md @@ -4,6 +4,11 @@ - Closes #64 +## PR Links + +- PR #158: https://github.com/stellar-vortex-protocol/vortex-backend/pull/158 +- Branch: `fix/pagination-nan-validation` + ## Summary `IntentsController.list()` was using `parseInt()` on raw query parameters (`limitRaw`, `offsetRaw`) without any validation. When a non-numeric value was provided (e.g., `?limit=abc`), `parseInt()` returned `NaN`, which silently corrupted the pagination slice (`intents.slice(offset, offset + limit)`) instead of returning a clear 400 error. diff --git a/PR_DESCRIPTION_65.md b/PR_DESCRIPTION_65.md index fa1f2ac..cdd41ff 100644 --- a/PR_DESCRIPTION_65.md +++ b/PR_DESCRIPTION_65.md @@ -4,6 +4,11 @@ - Closes #65 +## PR Links + +- PR #159: https://github.com/stellar-vortex-protocol/vortex-backend/pull/159 +- Branch: `fix/fill-amount-validation` + ## Summary `IntentsController.fill()` calls `BigInt(dto.fillAmount)` directly. While `FillIntentDto` already has `@Matches(/^\d+$/)` on `fillAmount` (matching the pattern used on `CreateIntentDto.srcAmount`), there was no e2e test asserting that a non-numeric `fillAmount` returns a clean 400 validation error instead of an ungraceful 500 `SyntaxError` from `BigInt()`.