Skip to content

feat: enforce invoice amount matches order amount on payouts#831

Merged
grunch merged 2 commits into
mainfrom
feat/enforce-invoice-amount-match
Jun 12, 2026
Merged

feat: enforce invoice amount matches order amount on payouts#831
grunch merged 2 commits into
mainfrom
feat/enforce-invoice-amount-match

Conversation

@grunch

@grunch grunch commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

Defense-in-depth so the bot never pays an invoice whose amount differs from the order amount. This closes a path where a malicious LNURL / Lightning Address endpoint could return an inflated invoice and get paid more than was held from the seller — draining the node.

Changes

  • ln/pay_request.ts
    • payRequest refuses to pay when the invoice encodes an amount that does not equal the expected amount, returning an AMOUNT_MISMATCH error.
    • payToBuyer treats AMOUNT_MISMATCH as a structural failure: it alerts the buyer and does not schedule a retry (a wrong invoice will never succeed).
  • lnurl/lnurl-pay.ts
    • resolvLightningAddress parses the returned invoice and rejects it (return false) unless it encodes exactly the requested msat amount, instead of trusting the server's min/maxSendable.
  • jobs/pending_payments.ts
    • Stops retrying a pending payment whose amount no longer matches the order amount (marked non-retryable).
    • Fixes early returncontinue so a single skipped/already-paid pending payment no longer aborts processing of the rest of the queue.
  • bot/scenes.ts
    • Both addInvoice wizards bail out with the unavailable_lightning_address notice when LNURL resolution returns no usable invoice, instead of dereferencing a missing pr.
  • bot/commands.ts
    • Corrects the unavailable-address guard to also handle a null/undefined resolution result (!laRes || !laRes.pr).

Validation

  • npx tsc --noEmit — passes
  • npm run lint — clean
  • npm run format / prettier check — clean
  • npm test — 126 passing

Notes

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved Lightning Address resolution to detect unavailable or incomplete responses and notify users instead of proceeding.
    • Continued processing of pending payments so one failed/skipped item no longer stops the whole job.
    • Added invoice amount verification to prevent paying mismatched invoices and surface clear errors.
    • Updated handling for expired or invalid invoices with clearer user notifications and no automatic retries.

Adds defense-in-depth so the bot never pays an invoice whose amount
differs from the order amount, closing a path where a malicious
LNURL/Lightning Address endpoint could return an inflated invoice and
be paid more than was held from the seller.

- ln/pay_request.ts: payRequest refuses to pay when the invoice encodes
  an amount that does not equal the expected amount, returning an
  AMOUNT_MISMATCH error. payToBuyer treats AMOUNT_MISMATCH as a
  structural failure: it alerts and does NOT schedule a retry.
- lnurl/lnurl-pay.ts: resolvLightningAddress parses the returned invoice
  and rejects it (returns false) unless it encodes exactly the requested
  msat amount, instead of trusting the server's min/maxSendable.
- jobs/pending_payments.ts: stop retrying a pending payment whose amount
  no longer matches the order amount (marks it as non-retryable). Also
  fixes early `return` -> `continue` so one skipped pending payment no
  longer aborts processing of the rest of the queue.
- bot/scenes.ts: in both addInvoice wizards, bail out with the
  unavailable_lightning_address notice when the LNURL resolution returns
  no usable invoice, instead of dereferencing a missing pr.
- bot/commands.ts: correct the unavailable-address guard to also handle
  a null/undefined resolution result (!laRes || !laRes.pr).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 89d0c801-e838-452b-9a4f-e0ba8402baf9

📥 Commits

Reviewing files that changed from the base of the PR and between bd22f4a and ae0ccd7.

📒 Files selected for processing (1)
  • jobs/pending_payments.ts
💤 Files with no reviewable changes (1)
  • jobs/pending_payments.ts

Walkthrough

Adds defense-in-depth invoice amount checks: handlers validate Lightning Address resolution, LNURL parsing verifies invoice amounts, payRequest rejects token mismatches, payToBuyer avoids retries for AMOUNT_MISMATCH, and the pending-payments job continues on individual failures while marking mismatches expired.

Changes

Invoice Amount Validation and Non-Retryable Failure Handling

Layer / File(s) Summary
Lightning Address resolution validation at entry points
bot/commands.ts, bot/scenes.ts
Command and scene handlers now validate that resolvLightningAddress returns a result with a usable .pr; if missing, the bot replies with unavailable_lightning_address and returns early.
Invoice amount verification at LNURL callback response
lnurl/lnurl-pay.ts
resolvLightningAddress now imports parsePaymentRequest and parses res.pr to compute msat; if the parsed amount differs from the requested amountMsat or parsing fails, the resolution is rejected and an error logged.
Payment request amount validation and non-retryable error classification
ln/pay_request.ts
payRequest returns AMOUNT_MISMATCH when an encoded invoice token amount does not equal the expected order amount; payToBuyer treats this as non-retryable, not scheduling PendingPayment retries and notifying failure.
Pending payment job loop control and amount mismatch handling
jobs/pending_payments.ts
attemptPendingPayments now uses continue to skip single pending failures, adds an amount-consistency guard that flags pending records with AMOUNT_MISMATCH and marks invoices expired, and continues after expired-invoice results instead of returning.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

  • lnp2pBot/bot#830: Both PRs modify Lightning Address resolution and control-flow in bot/commands.ts and bot/scenes.ts.
  • lnp2pBot/bot#667: Overlaps in payment failure/retry handling and PendingPayment retry scheduling changes.

Suggested reviewers

  • Luquitasjeffrey
  • Catrya
  • mostronatorcoder

Poem

🐰 I checked each invoice, tokens and bits,

mtokens measured, no sly little fits,
LNURL parsed, the numbers aligned,
Mismatches flagged, retries left behind,
The job hops on—no early quits. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: enforce invoice amount matches order amount on payouts' directly and accurately summarizes the main change: adding validation to ensure invoice amounts match expected order amounts before processing payments.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/enforce-invoice-amount-match

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
jobs/pending_payments.ts (1)

53-65: 💤 Low value

Redundant pending.save() call.

The explicit await pending.save() at line 63 is redundant because the finally block at line 176 will always execute and save pending regardless of whether the loop iteration ends via continue, return, or normal flow. This isn't a bug (the operation is idempotent), but it's an unnecessary database write.

🔧 Suggested fix
       if (pending.amount !== order.amount) {
         pending.last_error = 'AMOUNT_MISMATCH';
         pending.is_invoice_expired = true; // prevents further retries
         logger.error(
           `attemptPendingPayments: amount mismatch for order ${order._id} ` +
             `(pending ${pending.amount} != order ${order.amount}); stopping retries`,
         );
-        await pending.save();
         continue;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jobs/pending_payments.ts` around lines 53 - 65, The amount-mismatch branch in
attemptPendingPayments redundantly calls await pending.save(); remove that
explicit save inside the if (pending.amount !== order.amount) block since the
finally block always persists pending; keep setting pending.last_error =
'AMOUNT_MISMATCH' and pending.is_invoice_expired = true and the logger.error
call, but delete the await pending.save() line to avoid the duplicate DB write.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@jobs/pending_payments.ts`:
- Around line 53-65: The amount-mismatch branch in attemptPendingPayments
redundantly calls await pending.save(); remove that explicit save inside the if
(pending.amount !== order.amount) block since the finally block always persists
pending; keep setting pending.last_error = 'AMOUNT_MISMATCH' and
pending.is_invoice_expired = true and the logger.error call, but delete the
await pending.save() line to avoid the duplicate DB write.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d4296b9e-3382-4cf9-b307-e146660a925f

📥 Commits

Reviewing files that changed from the base of the PR and between f3e6aa2 and bd22f4a.

📒 Files selected for processing (5)
  • bot/commands.ts
  • bot/scenes.ts
  • jobs/pending_payments.ts
  • ln/pay_request.ts
  • lnurl/lnurl-pay.ts

The finally block already persists pending on every iteration, so the
explicit save inside the amount-mismatch guard was a duplicate DB write.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@Luquitasjeffrey Luquitasjeffrey left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

tACK

@grunch grunch merged commit 084c0da into main Jun 12, 2026
7 checks passed
@grunch grunch deleted the feat/enforce-invoice-amount-match branch June 12, 2026 00:28
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.

2 participants