Skip to content

Handle malformed wallet withdrawal bodies#185

Merged
ralyodio merged 1 commit into
profullstack:masterfrom
morganschp:fix-wallet-withdraw-invalid-json
May 23, 2026
Merged

Handle malformed wallet withdrawal bodies#185
ralyodio merged 1 commit into
profullstack:masterfrom
morganschp:fix-wallet-withdraw-invalid-json

Conversation

@morganschp
Copy link
Copy Markdown
Contributor

Summary

  • return 400 Invalid request body for malformed or non-object wallet withdraw JSON
  • validate destination type before wallet lookup/payment logic
  • add route regression coverage proving malformed bodies stop before withdrawal lookup

Fixes #184

uGig task

Submitted for the active uGig repo testing task: https://ugig.net/gigs/4741218f-a723-46bb-82cb-6516120331ae

No payout details included; those can be provided after acceptance.

Tests

  • ./node_modules/.bin/vitest run src/app/api/wallet/withdraw/route.test.ts
  • ./node_modules/.bin/eslint src/app/api/wallet/withdraw/route.ts src/app/api/wallet/withdraw/route.test.ts
  • ./node_modules/.bin/tsc --noEmit
  • git diff --check -- src/app/api/wallet/withdraw/route.ts src/app/api/wallet/withdraw/route.test.ts

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 23, 2026

Greptile Summary

This PR hardens the wallet withdrawal endpoint against malformed request bodies by extracting a parseWithdrawRequestBody helper that wraps request.json() in a try-catch and rejects non-object payloads with a 400 before any database or wallet logic runs. It also adds an explicit typeof guard for destination to prevent non-string values from reaching the Lightning payment path.

  • Malformed or non-object JSON now returns { error: "Invalid request body" } (400) immediately after auth, before any Supabase or LNbits calls.
  • typeof amount_sats !== "number" and typeof destination !== "string" guards are inserted ahead of the DB rate-limit and wallet-lookup logic, with regression tests confirming mockFrom and mockGetUserLnWallet are never reached on invalid inputs.

Confidence Score: 5/5

Safe to merge — all new validation exits before any database or wallet calls and the existing happy-path is untouched.

The changes are narrowly scoped: a new parsing helper and three extra type guards added ahead of all side-effectful operations. Each guard is independently tested and the existing tests continue to pass unchanged. No logic downstream of the guards is modified.

No files require special attention.

Important Files Changed

Filename Overview
src/app/api/wallet/withdraw/route.ts Adds parseWithdrawRequestBody helper to catch malformed/non-object JSON, plus explicit typeof guards for amount_sats and destination before database calls are made.
src/app/api/wallet/withdraw/route.test.ts Adds makeRawRequest helper and three new test cases covering malformed JSON, non-object bodies, and non-string destinations; all correctly assert DB/wallet mocks are not reached.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[POST /api/wallet/withdraw] --> B[getAuthContext]
    B -- null --> C[401 Unauthorized]
    B -- auth --> D[parseWithdrawRequestBody]
    D -- parse error / non-object --> E[400 Invalid request body]
    D -- body object --> F{!amount_sats OR !destination}
    F -- true --> G[400 required fields]
    F -- false --> H{typeof amount_sats !== number OR !isInteger}
    H -- true --> I[400 Amount must be whole number]
    H -- false --> J{typeof destination !== string}
    J -- true --> K[400 Destination must be a string]
    J -- false --> L{amount_sats range check}
    L -- out of range --> M[400 Amount out of range]
    L -- ok --> N[Supabase rate-limit check]
    N --> O[Daily limit check]
    O --> P[getUserLnWallet]
    P -- null --> Q[400 No Lightning wallet]
    P -- wallet --> R[getLnBalance]
    R -- insufficient --> S[400 Insufficient balance]
    R -- ok --> T[payInvoice / LNURL resolve]
    T -- error --> U[502 Payment failed]
    T -- ok --> V[syncBalanceCache + insert tx]
    V --> W[200 ok]
Loading

Reviews (3): Last reviewed commit: "Handle malformed wallet withdrawal bodie..." | Re-trigger Greptile

Comment on lines 43 to +51
if (!amount_sats || !destination) {
return NextResponse.json({ error: "amount_sats and destination are required" }, { status: 400 });
}
if (!Number.isInteger(amount_sats)) {
if (typeof amount_sats !== "number" || !Number.isInteger(amount_sats)) {
return NextResponse.json({ error: "Amount must be a whole number" }, { status: 400 });
}
if (typeof destination !== "string") {
return NextResponse.json({ error: "Destination must be a string" }, { status: 400 });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Falsy destination short-circuits new type check

The !destination guard on line 43 is falsy, so a destination: 0 payload returns "amount_sats and destination are required" before the new typeof destination !== "string" check on line 49 is ever reached. Concretely, { amount_sats: 100, destination: 0 } produces the generic "required" message rather than the more informative "Destination must be a string". Using an explicit null/undefined check (destination == null) instead of the falsy !destination would let all non-null, non-string destinations reach the type-specific error path the PR introduces.

@ralyodio ralyodio closed this May 23, 2026
@ralyodio ralyodio reopened this May 23, 2026
@ralyodio ralyodio closed this May 23, 2026
@ralyodio ralyodio reopened this May 23, 2026
@ralyodio ralyodio merged commit 65fa0cd into profullstack:master May 23, 2026
4 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.

bug: wallet withdraw API returns 500 on malformed JSON

2 participants