Skip to content

Validate LN swap amount when scanning/pasting invoice#560

Open
gringokiwi wants to merge 3 commits intomasterfrom
james-misc
Open

Validate LN swap amount when scanning/pasting invoice#560
gringokiwi wants to merge 3 commits intomasterfrom
james-misc

Conversation

@gringokiwi
Copy link
Copy Markdown
Contributor

@gringokiwi gringokiwi commented Apr 23, 2026

fix.mp4

also cleans up some type errors with the brantaPayment hook

Summary by CodeRabbit

  • New Features

    • Added Lightning swap validation across the send flow, including invoice eligibility and min/max limits.
    • Updated QR-code payment handling to support multiple payment entries.
  • Bug Fixes

    • Button state and proceed flows now block Lightning invoice sends when swaps are disabled or limits are exceeded.
    • Improved, user-facing error messages for Lightning send attempts that violate swap rules.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 23, 2026

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: b5be455
Status: ✅  Deploy successful!
Preview URL: https://bf97666d.arkade-wallet.pages.dev
Branch Preview URL: https://james-misc.arkade-wallet.pages.dev

View logs

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Walkthrough

Replaces the Branta SDK Payment type with a local BrantaPayment type and adds Lightning-swap gating across the send form: invoice parsing, button label/effect, proceed/continue flows, and disabled logic now enforce lnSwapsAllowed, minSwapAllowed, and maxSwapAllowed constraints.

Changes

Cohort / File(s) Summary
Lightning Swap Gating & Type Updates
src/screens/Wallet/Send/Form.tsx
Replaced Branta SDK Payment with local BrantaPayment and updated QR-code payment fetch to BrantaPayment[]. Added invoice parsing checks for lnSwapsAllowed, minSwapAllowed, and maxSwapAllowed; propagate user-facing errors. Updated effect dependencies to include sendInfo.invoice/satoshis, adjusted button-label effect, early-fail handleContinue/proceed paths, and extended send-button disabled logic to block disallowed Lightning sends.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • pietro909
  • bordalix
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: validating Lightning Network swap amounts when invoices are scanned or pasted, which aligns with the primary objective of the PR.
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 james-misc

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.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 23, 2026

Deploying tmp-boltz-upstream-mainnet-arkade-wallet with  Cloudflare Pages  Cloudflare Pages

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

View logs

Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

✅ 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:

  1. Button label — shows "Lightning swaps not enabled" / "Amount outside Lightning swap limits" (line ~444-449)
  2. Button disabled — prevents submit when LN validation fails (line ~696-697)
  3. Proceed useEffect — defense-in-depth before createSubmarineSwap (line ~472-473)
  4. 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() and validLnSwap() read from limits.current ref — always latest value, no stale closure risk
  • sendInfo.invoice correctly added to label useEffect deps (line ~457) since isLightningSend derives from it
  • sendInfo.satoshis correctly added to proceed useEffect deps (line ~489) since new validation reads it
  • Early return handleError(...) in handleContinue correctly prevents setProceed(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 since getInvoiceSatoshis enforces 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 🚀

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

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 | 🟠 Major

Only gate Lightning swaps when the flow will actually use Lightning.

A BIP21 payload can include both arkAddress and invoice. The proceed effect already prefers arkAddress, but the new label, disabled state, and handleContinue checks block solely because invoice exists, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6f08911 and efa2bbc.

📒 Files selected for processing (1)
  • src/screens/Wallet/Send/Form.tsx

Comment on lines 472 to 475
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)
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.

⚠️ Potential issue | 🟠 Major

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
@sahilc0
Copy link
Copy Markdown
Contributor

sahilc0 commented Apr 23, 2026

Added a commit to improve the error UX:

  • Button text stays "Continue" — disabled when limits aren't met, but text doesn't change (cleaner UX than showing error in button)
  • Error banner shows actual limits — "Minimum Lightning send is X sats" instead of generic "Amount outside Lightning swap limits"
  • Shows proactively — error appears immediately when invoice is pasted, not just on submit attempt

This gives users clear, actionable feedback about why they can't proceed.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: b5be455
Status: ✅  Deploy successful!
Preview URL: https://bdc09e35.wallet-bitcoin.pages.dev
Branch Preview URL: https://james-misc.wallet-bitcoin.pages.dev

View logs

Copy link
Copy Markdown

@arkanaai arkanaai Bot left a comment

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/screens/Wallet/Send/Form.tsx (1)

626-629: ⚠️ Potential issue | 🟠 Major

LNURL fallback still validates the pre-fee amount.

When deductFromAmount is true, amountForInvoice (line 630) differs from satoshis, but the new gating on lines 627-629 validates satoshis. 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 validating sendInfo.satoshis instead 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) call satoshis < minSwapAllowed() unconditionally, while the label effect (456) guards with minSwap > 0. Likewise, the maxSwap > 0 sentinel relies on -1n being the "unlimited" marker from limits.tsx; a legitimate max of 0 would also be treated as unlimited. Consider a single helper (e.g. reusing validLnSwap(satoshis) for gating, with the contextual messages derived from minSwapAllowed()/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

📥 Commits

Reviewing files that changed from the base of the PR and between efa2bbc and b5be455.

📒 Files selected for processing (1)
  • src/screens/Wallet/Send/Form.tsx

@bordalix
Copy link
Copy Markdown
Collaborator

@gringokiwi check CRITICAL rabbit comment:

  • if lightning is out of limits or disabled, ark payments are not allowed.

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.

3 participants