Validate LN swap amount when scanning/pasting invoice#560
Validate LN swap amount when scanning/pasting invoice#560gringokiwi wants to merge 3 commits intomasterfrom
Conversation
Deploying wallet-mutinynet with
|
| Latest commit: |
b5be455
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://bf97666d.arkade-wallet.pages.dev |
| Branch Preview URL: | https://james-misc.arkade-wallet.pages.dev |
WalkthroughReplaces the Branta SDK Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 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 |
Deploying tmp-boltz-upstream-mainnet-arkade-wallet with
|
| Latest commit: |
b5be455
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://4aefb9ad.tmp-boltz-upstream-mainnet-arkade-wallet.pages.dev |
| Branch Preview URL: | https://james-misc.tmp-boltz-upstream-mainnet-arkade-wallet.pages.dev |
There was a problem hiding this comment.
✅ Approved — clean, well-structured validation
Reviewed the full diff and the complete Form.tsx (973 lines) plus providers/limits.tsx for context.
What this does
Adds client-side validation for Lightning swap limits at four enforcement points:
- Button label — shows "Lightning swaps not enabled" / "Amount outside Lightning swap limits" (line ~444-449)
- Button disabled — prevents submit when LN validation fails (line ~696-697)
- Proceed useEffect — defense-in-depth before
createSubmarineSwap(line ~472-473) - handleContinue — gates both LNURL→LN fallback path (line ~596-597) and direct invoice path (line ~605-607)
Also fixes BrantaPayment type to use a local Partial<> wrapper instead of the SDK's Payment — pragmatic workaround flagged with TODO.
Correctness ✓
lnSwapsAllowed()andvalidLnSwap()read fromlimits.currentref — always latest value, no stale closure risksendInfo.invoicecorrectly added to label useEffect deps (line ~457) sinceisLightningSendderives from itsendInfo.satoshiscorrectly added to proceed useEffect deps (line ~489) since new validation reads it- Early
return handleError(...)inhandleContinuecorrectly preventssetProceed(true)from executing validLnSwap(0)returns false (line 145 of limits.tsx:if (!sats) return txtype !== TxType.swap), but this is unreachable for LN invoices sincegetInvoiceSatoshisenforces non-zero (line 247-248)
Not protocol-critical
This is UI-layer input validation only. No VTXO handling, signing, forfeit paths, or exit logic touched. Server-side swap creation would reject invalid amounts regardless — this is UX improvement.
One minor observation (non-blocking)
BrantaPayment type derivation (line ~55): Type is derived from V2BrantaClient['addPayment'] but the code calls getPaymentsByQRCode. If these return different shapes, Partial<> masks the mismatch. The TODO acknowledges this — just noting it for when the SDK types stabilize.
LGTM. Ship it 🚀
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/screens/Wallet/Send/Form.tsx (1)
434-454:⚠️ Potential issue | 🟠 MajorOnly gate Lightning swaps when the flow will actually use Lightning.
A BIP21 payload can include both
arkAddressandinvoice. The proceed effect already prefersarkAddress, but the new label, disabled state, andhandleContinuechecks block solely becauseinvoiceexists, so Ark-payable requests fail when LN swaps are disabled or out of range.🐛 Proposed fix
- const isLightningSend = Boolean(sendInfo.invoice) + const requiresLnSwap = Boolean(sendInfo.invoice && !sendInfo.arkAddress) setLabel( satoshis > liquidBalance ? 'Insufficient funds' @@ - : isLightningSend && !lnSwapsAllowed() + : requiresLnSwap && !lnSwapsAllowed() ? 'Lightning swaps not enabled' - : isLightningSend && !validLnSwap(satoshis) + : requiresLnSwap && !validLnSwap(satoshis) ? 'Amount outside Lightning swap limits' @@ - }, [sendInfo.satoshis, sendInfo.assets, sendInfo.invoice, liquidBalance, selectedAsset]) + }, [sendInfo.satoshis, sendInfo.assets, sendInfo.invoice, sendInfo.arkAddress, liquidBalance, selectedAsset])- if (sendInfo.invoice) { + if (sendInfo.invoice && !sendInfo.arkAddress) { if (!lnSwapsAllowed()) return handleError('Lightning swaps not enabled') if (!validLnSwap(satoshis)) return handleError('Amount outside Lightning swap limits') }- (invoice && !lnSwapsAllowed()) || - (invoice && !validLnSwap(satoshis)) || + (invoice && !arkAddress && !lnSwapsAllowed()) || + (invoice && !arkAddress && !validLnSwap(satoshis)) ||Also applies to: 606-609, 694-699
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/screens/Wallet/Send/Form.tsx` around lines 434 - 454, The label/disabled/continue logic is currently gating Lightning swaps whenever sendInfo.invoice exists even if the BIP21 payload includes an arkAddress; update the checks to only enforce lnSwapsAllowed() and validLnSwap(...) when the flow will actually use Lightning (i.e., invoice present AND no arkAddress). Concretely, change the conditions in setLabel (the ternary branch that uses isLightningSend), the corresponding disabled-state logic, and handleContinue so they require isLightningSend && !sendInfo.arkAddress (or the appropriate sendInfo address field) before calling lnSwapsAllowed or validLnSwap; keep proceed’s preference for arkAddress unchanged. Apply the same change to the other occurrences noted (the blocks around the other mentioned line ranges).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/screens/Wallet/Send/Form.tsx`:
- Around line 472-475: The code currently validates the pre-fee
sendInfo.satoshis but not the actual invoice amount after LN fee deductions;
change the checks so you first derive the real satoshi amount from the invoice
(e.g. const actualSats = amountForInvoice(sendInfo.invoice) ??
(sendInfo.satoshis ?? 0)) and then call validLnSwap(actualSats) and
lnSwapsAllowed() before createSubmarineSwap(sendInfo.invoice); apply the same
adjustment for the other occurrence referenced (the block around validLnSwap/use
of createSubmarineSwap at the later lines).
---
Outside diff comments:
In `@src/screens/Wallet/Send/Form.tsx`:
- Around line 434-454: The label/disabled/continue logic is currently gating
Lightning swaps whenever sendInfo.invoice exists even if the BIP21 payload
includes an arkAddress; update the checks to only enforce lnSwapsAllowed() and
validLnSwap(...) when the flow will actually use Lightning (i.e., invoice
present AND no arkAddress). Concretely, change the conditions in setLabel (the
ternary branch that uses isLightningSend), the corresponding disabled-state
logic, and handleContinue so they require isLightningSend &&
!sendInfo.arkAddress (or the appropriate sendInfo address field) before calling
lnSwapsAllowed or validLnSwap; keep proceed’s preference for arkAddress
unchanged. Apply the same change to the other occurrences noted (the blocks
around the other mentioned line ranges).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6fd01163-eda3-4433-bc42-50014700b8c1
📒 Files selected for processing (1)
src/screens/Wallet/Send/Form.tsx
| if (sendInfo.invoice) { | ||
| if (!lnSwapsAllowed()) return handleError('Lightning swaps not enabled') | ||
| if (!validLnSwap(sendInfo.satoshis ?? 0)) return handleError('Amount outside Lightning swap limits') | ||
| createSubmarineSwap(sendInfo.invoice) |
There was a problem hiding this comment.
Validate the actual Lightning invoice amount after fee deduction.
For LNURL fallback, amountForInvoice can be lower than satoshis when fees are deducted from the amount. The new limit checks validate satoshis, so a below-min actual swap can still proceed, or a valid post-fee swap can be rejected against the pre-fee amount.
🐛 Proposed fix
if (sendInfo.invoice) {
if (!lnSwapsAllowed()) return handleError('Lightning swaps not enabled')
- if (!validLnSwap(sendInfo.satoshis ?? 0)) return handleError('Amount outside Lightning swap limits')
+ const invoiceSatoshis = getInvoiceSatoshis(sendInfo.invoice)
+ if (!invoiceSatoshis || !validLnSwap(invoiceSatoshis)) {
+ return handleError('Amount outside Lightning swap limits')
+ }
createSubmarineSwap(sendInfo.invoice) // Fallback to Lightning invoice
if (!lnSwapsAllowed()) return handleError('Lightning swaps not enabled')
- if (!validLnSwap(satoshis)) return handleError('Amount outside Lightning swap limits')
const amountForInvoice = deductFromAmount ? satoshis - calcSubmarineSwapFee(satoshis) : satoshis
if (amountForInvoice < 1) return handleError('Amount too low to cover fees')
+ if (!validLnSwap(amountForInvoice)) return handleError('Amount outside Lightning swap limits')
const invoice = await fetchInvoice(sendInfo.lnUrl, amountForInvoice, '')Also applies to: 598-603
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/screens/Wallet/Send/Form.tsx` around lines 472 - 475, The code currently
validates the pre-fee sendInfo.satoshis but not the actual invoice amount after
LN fee deductions; change the checks so you first derive the real satoshi amount
from the invoice (e.g. const actualSats = amountForInvoice(sendInfo.invoice) ??
(sendInfo.satoshis ?? 0)) and then call validLnSwap(actualSats) and
lnSwapsAllowed() before createSubmarineSwap(sendInfo.invoice); apply the same
adjustment for the other occurrence referenced (the block around validLnSwap/use
of createSubmarineSwap at the later lines).
- Show "Minimum Lightning send is X sats" instead of generic "Amount outside Lightning swap limits" - Show error banner proactively when invoice is pasted (not just on submit) - Keep button text as "Continue" (disabled when limits not met) - Handle both min and max limit violations with specific messages
|
Added a commit to improve the error UX:
This gives users clear, actionable feedback about why they can't proceed. |
Deploying wallet-bitcoin with
|
| Latest commit: |
b5be455
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://bdc09e35.wallet-bitcoin.pages.dev |
| Branch Preview URL: | https://james-misc.wallet-bitcoin.pages.dev |
There was a problem hiding this comment.
Re-review — commit b5be455 (show specific LN swap limits in errors)
Previous approval on efa2bbc stands — the new commit is a UX improvement (specific min/max error messages instead of generic "Amount outside Lightning swap limits"). Two nits:
1. Missing error clearing in label effect (~line 449-462)
if (isLightningSend && satoshis > 0) {
if (!lnSwapsAllowed()) {
setError('Lightning sends are currently unavailable')
} else {
// ...
if (minSwap > 0 && satoshis < minSwap) {
setError(`Minimum Lightning send is ${minSwap} sats`)
} else if (maxSwap > 0 && satoshis > maxSwap) {
setError(`Maximum Lightning send is ${maxSwap} sats`)
}
// ← no else { setError('') } — stale error persists if limits change
}
}If the swap service reconnects (limits update) while the user has an invoice pasted, the error banner won't clear because no setError('') is called on the valid path. Low-impact since invoices are read-only and the user would likely re-paste, but worth adding else { setError('') } for robustness.
2. BIP21 arkAddress + invoice (echoing CodeRabbit)
CodeRabbit's point from the previous round is still valid and unaddressed: isLightningSend = Boolean(sendInfo.invoice) and the button disabled conditions (invoice && !lnSwapsAllowed()) / (invoice && !validLnSwap(satoshis)) fire even when arkAddress is present and would be the preferred path. This would incorrectly block Ark-payable BIP21 requests when LN swaps are disabled or out-of-range.
Neither is protocol-critical — server-side swap creation would reject regardless. Previous approval holds.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/screens/Wallet/Send/Form.tsx (1)
626-629:⚠️ Potential issue | 🟠 MajorLNURL fallback still validates the pre-fee amount.
When
deductFromAmountis true,amountForInvoice(line 630) differs fromsatoshis, but the new gating on lines 627-629 validatessatoshis. A below-min swap can slip through (pre-fee passes, post-fee fails) and a valid post-fee swap can be rejected against the pre-fee amount. Same root cause means the downstream re-check in the proceed effect (lines 498-502) also ends up validatingsendInfo.satoshisinstead of the actual invoice amount — fixing here (and optionally re-deriving from the fetched invoice in the proceed effect) resolves both.🐛 Suggested fix
} else { // Fallback to Lightning invoice if (!lnSwapsAllowed()) return handleError('Lightning sends are currently unavailable') - if (satoshis < minSwapAllowed()) return handleError(`Minimum Lightning send is ${minSwapAllowed()} sats`) - const maxSwap = maxSwapAllowed() - if (maxSwap > 0 && satoshis > maxSwap) return handleError(`Maximum Lightning send is ${maxSwap} sats`) const amountForInvoice = deductFromAmount ? satoshis - calcSubmarineSwapFee(satoshis) : satoshis if (amountForInvoice < 1) return handleError('Amount too low to cover fees') + if (amountForInvoice < minSwapAllowed()) + return handleError(`Minimum Lightning send is ${minSwapAllowed()} sats`) + const maxSwap = maxSwapAllowed() + if (maxSwap > 0 && amountForInvoice > maxSwap) + return handleError(`Maximum Lightning send is ${maxSwap} sats`) const invoice = await fetchInvoice(sendInfo.lnUrl, amountForInvoice, '')🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/screens/Wallet/Send/Form.tsx` around lines 626 - 629, The current gating uses the pre-fee variable satoshis but when deductFromAmount is true you must validate the actual invoice amount (amountForInvoice) instead; update the checks that call lnSwapsAllowed(), minSwapAllowed(), and maxSwapAllowed() to use amountForInvoice (or its derived value when deductFromAmount is true) instead of satoshis, and also update the downstream proceed effect to validate against the fetched invoice amount (not sendInfo.satoshis) so both the early gate and the proceed re-check use the real post-fee invoice amount.
🧹 Nitpick comments (1)
src/screens/Wallet/Send/Form.tsx (1)
265-275: Minor: inconsistent min/max guards across the four new checks.The parseRecipient block (268), proceed effect (500), and direct-invoice branch in
handleContinue(638) callsatoshis < minSwapAllowed()unconditionally, while the label effect (456) guards withminSwap > 0. Likewise, themaxSwap > 0sentinel relies on-1nbeing the "unlimited" marker fromlimits.tsx; a legitimate max of0would also be treated as unlimited. Consider a single helper (e.g. reusingvalidLnSwap(satoshis)for gating, with the contextual messages derived fromminSwapAllowed()/maxSwapAllowed()) so all call sites stay in lock-step and any future change to the sentinel convention only has to be made in one place.Also applies to: 498-502, 636-641
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/screens/Wallet/Send/Form.tsx` around lines 265 - 275, The four separate min/max checks are inconsistent; create or reuse a single helper (e.g. validLnSwap(satoshis)) that encapsulates the sentinel logic for minSwapAllowed() and maxSwapAllowed() (use the intended sentinel semantics from limits.tsx such as min>0 and max>-1/unlimited) and return a standardized result or error message; replace the ad-hoc checks in parseRecipient, the label effect, the proceed effect and the direct-invoice branch in handleContinue with calls to this helper so all call sites use the same guards and message generation derived from minSwapAllowed()/maxSwapAllowed().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/screens/Wallet/Send/Form.tsx`:
- Around line 626-629: The current gating uses the pre-fee variable satoshis but
when deductFromAmount is true you must validate the actual invoice amount
(amountForInvoice) instead; update the checks that call lnSwapsAllowed(),
minSwapAllowed(), and maxSwapAllowed() to use amountForInvoice (or its derived
value when deductFromAmount is true) instead of satoshis, and also update the
downstream proceed effect to validate against the fetched invoice amount (not
sendInfo.satoshis) so both the early gate and the proceed re-check use the real
post-fee invoice amount.
---
Nitpick comments:
In `@src/screens/Wallet/Send/Form.tsx`:
- Around line 265-275: The four separate min/max checks are inconsistent; create
or reuse a single helper (e.g. validLnSwap(satoshis)) that encapsulates the
sentinel logic for minSwapAllowed() and maxSwapAllowed() (use the intended
sentinel semantics from limits.tsx such as min>0 and max>-1/unlimited) and
return a standardized result or error message; replace the ad-hoc checks in
parseRecipient, the label effect, the proceed effect and the direct-invoice
branch in handleContinue with calls to this helper so all call sites use the
same guards and message generation derived from
minSwapAllowed()/maxSwapAllowed().
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: ff250c10-d3ff-46d0-9d41-0046eb8ccdb1
📒 Files selected for processing (1)
src/screens/Wallet/Send/Form.tsx
|
@gringokiwi check CRITICAL rabbit comment:
|
fix.mp4
also cleans up some type errors with the
brantaPaymenthookSummary by CodeRabbit
New Features
Bug Fixes