feat: enforce invoice amount matches order amount on payouts#831
Conversation
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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
💤 Files with no reviewable changes (1)
WalkthroughAdds 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. ChangesInvoice Amount Validation and Non-Retryable Failure Handling
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
jobs/pending_payments.ts (1)
53-65: 💤 Low valueRedundant
pending.save()call.The explicit
await pending.save()at line 63 is redundant because thefinallyblock at line 176 will always execute and savependingregardless of whether the loop iteration ends viacontinue,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
📒 Files selected for processing (5)
bot/commands.tsbot/scenes.tsjobs/pending_payments.tsln/pay_request.tslnurl/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>
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.tspayRequestrefuses to pay when the invoice encodes an amount that does not equal the expected amount, returning anAMOUNT_MISMATCHerror.payToBuyertreatsAMOUNT_MISMATCHas a structural failure: it alerts the buyer and does not schedule a retry (a wrong invoice will never succeed).lnurl/lnurl-pay.tsresolvLightningAddressparses the returned invoice and rejects it (return false) unless it encodes exactly the requested msat amount, instead of trusting the server'smin/maxSendable.jobs/pending_payments.tsreturn→continueso a single skipped/already-paid pending payment no longer aborts processing of the rest of the queue.bot/scenes.tsaddInvoicewizards bail out with theunavailable_lightning_addressnotice when LNURL resolution returns no usable invoice, instead of dereferencing a missingpr.bot/commands.ts!laRes || !laRes.pr).Validation
npx tsc --noEmit— passesnpm run lint— cleannpm run format/ prettier check — cleannpm test— 126 passingNotes
DISABLE_LN_ADDRESSkill-switch): that flag lets operators turn the feature off entirely, while this PR hardens the path that stays on.unavailable_lightning_addressandinvoicePaymentFailedMessagestrings — no new locale keys required.🤖 Generated with Claude Code
Summary by CodeRabbit