Skip to content

lightning: add lnurl pay send flow#4214

Merged
Beerosagos merged 1 commit into
BitBoxSwiss:staging-sparkfrom
Beerosagos:lnurl-pay
Jul 8, 2026
Merged

lightning: add lnurl pay send flow#4214
Beerosagos merged 1 commit into
BitBoxSwiss:staging-sparkfrom
Beerosagos:lnurl-pay

Conversation

@Beerosagos

Copy link
Copy Markdown
Collaborator

Add LNURL Pay support to the Lightning send flow while keeping rail selection inside the backend lightning package. Parse payment inputs into explicit BOLT11 and LNURL Pay variants, prepare and send each rail through the shared payment endpoints, and return localized error codes for invalid input and amounts.

Refactor the frontend review step into rail-specific BOLT11 and LNURL Pay components. Pass each component only the payload it needs, keep BOLT11 behavior aligned with the existing review flow, and keep LNURL Pay as an amount-entry flow within the server-provided min and max bounds.

Before asking for reviews, here is a check list of the most common things you might need to consider:

  • updating the Changelog
  • writing unit tests
  • checking if your changes affect other coins or tokens in unintended ways
  • testing on multiple environments (Qt, Android, ...)
  • having an AI review your changes

@Beerosagos

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

This PR extends the Lightning payment flow to support LNURL-Pay inputs alongside existing BOLT11 invoices. On the backend, ParsePaymentInput, PreparePayment, and SendPayment are refactored to accept unified request structures that dispatch to type-specific handlers for BOLT11 and LNURL-Pay, including new LNURL metadata parsing, amount bounds validation, and fee computation. On the frontend, API types are restructured into a discriminated TPaymentInput union, and the payment review UI is split into separate Bolt11ReviewStep and LNURLPayReviewStep components, with shared helper utilities for validation and error formatting. New localization strings and tests accompany these changes.

Sequence Diagram(s)

See diagrams embedded within the review stack artifact above.

Related PRs: None identified.

Suggested labels: backend, frontend, lightning, enhancement

Suggested reviewers: Not enough information available to determine reviewers.

🐇 A payment hops through LNURL's gate,
Bolt11 or address, both find their fate,
Fees are quoted, amounts checked twice,
Two review steps now offer good advice,
Send it off — the ledger stays straight.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
backend/lightning/payments.go (1)

471-509: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align the BOLT11 send path with the balance precheck
sendBolt11Payment still skips the same Balance()/checkPaymentBalance() guard used by prepareBolt11Payment and sendLNURLPay. If funds change after quoting, BOLT11 will only fail inside SendPayment; add the balance check here for parity.

🤖 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 `@backend/lightning/payments.go` around lines 471 - 509, The BOLT11 send flow
in sendBolt11Payment is missing the same Balance()/checkPaymentBalance()
precheck used by prepareBolt11Payment and sendLNURLPay, so add that guard after
CheckActive and before PrepareSendPayment to keep payment validation consistent.
Use the Lightning methods Balance, checkPaymentBalance, and sendBolt11Payment to
locate the change, and make sure the balance is verified against
request.AmountSat/ApprovedFeeSat before proceeding to the SDK send path.
frontends/web/src/routes/lightning/send/components/payment-input-details.tsx (1)

122-157: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add the confirm heading to the LNURL-Pay review step. send.tsx already renders the page title (lightning.send.title), but LNURLPayReviewStep still skips the confirmation heading that Bolt11PaymentDetails shows, so LNURL-Pay reviews read like an unfinished state.

🤖 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 `@frontends/web/src/routes/lightning/send/components/payment-input-details.tsx`
around lines 122 - 157, The LNURL-Pay review UI is missing the confirmation
heading shown in the Bolt11 flow, so add the same confirm title to
LNURLPayRecipientDetails or LNURLPayReviewStep using the existing translation
key lightning.send.confirm.title. Make the change in the payment-input-details
component so LNURL-Pay review matches Bolt11PaymentDetails and the send.tsx page
title remains separate from the confirmation heading.
🤖 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.

Inline comments:
In `@frontends/web/src/routes/lightning/send/components/bolt11-review-step.tsx`:
- Around line 38-247: The payment review logic in bolt11-review-step.tsx is
duplicated with LNURLPayReviewStep, including preparePayment, sendPayment, the
reset/debounce effects, and TSdkError handling. Extract this shared behavior
into a reusable hook or helper (for example, a payment review hook parameterized
by payment type and prepare/send functions) and have Bolt11PaymentReviewStep
consume it. Keep the invoice-specific UI and inputs in Bolt11PaymentReviewStep,
but move the common state/effect/error-branching flow into the shared
implementation to prevent further drift.
- Around line 112-130: The catch block in bolt11-review-step.tsx only guards
setIsSending(false) with mounted.current, but backToPaymentInput, setSendError,
and the preparePayment retry can still run after unmount. Add the same early
unmounted return pattern used in LNURLPayReviewStep before any side effects in
this catch path, and keep the TSdkError branches and errorMessage handling
inside the mounted-safe flow.

In
`@frontends/web/src/routes/lightning/send/components/lnurl-pay-review-step.tsx`:
- Around line 36-41: The LNURL Pay amount state in lnurl-pay-review-step.tsx
always initializes customAmount as undefined, which misses the common
fixed-amount case. Update the component around the customAmount/useDebounce
state setup to prefill customAmount when minAmountSat equals maxAmountSat (the
fixed LNURL pay amount case), so the user doesn’t need to enter the only valid
value manually. Keep the behavior tied to the existing prepared-payment flow and
ensure any amount-setting logic in this component continues to use customAmount
consistently.

In `@frontends/web/src/routes/lightning/send/components/payment-review.ts`:
- Around line 6-11: TPreparedPayment currently mixes optional result fields with
an explicit isPreparing boolean, which conflicts with the loading-state
convention. Update the type in payment-review.ts to use a discriminated union
with mutually exclusive preparing/ready/error states instead of a separate
boolean, and adjust the shape around TPreparedPayment so callers can rely on
status-based narrowing. Keep the change centered on TPreparedPayment and the
related payment review state model so the two consumers can be updated
consistently later.

---

Outside diff comments:
In `@backend/lightning/payments.go`:
- Around line 471-509: The BOLT11 send flow in sendBolt11Payment is missing the
same Balance()/checkPaymentBalance() precheck used by prepareBolt11Payment and
sendLNURLPay, so add that guard after CheckActive and before PrepareSendPayment
to keep payment validation consistent. Use the Lightning methods Balance,
checkPaymentBalance, and sendBolt11Payment to locate the change, and make sure
the balance is verified against request.AmountSat/ApprovedFeeSat before
proceeding to the SDK send path.

In
`@frontends/web/src/routes/lightning/send/components/payment-input-details.tsx`:
- Around line 122-157: The LNURL-Pay review UI is missing the confirmation
heading shown in the Bolt11 flow, so add the same confirm title to
LNURLPayRecipientDetails or LNURLPayReviewStep using the existing translation
key lightning.send.confirm.title. Make the change in the payment-input-details
component so LNURL-Pay review matches Bolt11PaymentDetails and the send.tsx page
title remains separate from the confirmation heading.
🪄 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: ASSERTIVE

Plan: Pro Plus

Run ID: 78385121-c289-432e-b555-aee107e31b0c

📥 Commits

Reviewing files that changed from the base of the PR and between 26b4c88 and 997a17a.

📒 Files selected for processing (12)
  • backend/lightning/handlers.go
  • backend/lightning/payments.go
  • backend/lightning/payments_test.go
  • frontends/web/src/api/lightning.ts
  • frontends/web/src/locales/en/app.json
  • frontends/web/src/routes/lightning/send/components/bolt11-review-step.tsx
  • frontends/web/src/routes/lightning/send/components/lnurl-pay-review-step.tsx
  • frontends/web/src/routes/lightning/send/components/payment-input-details.tsx
  • frontends/web/src/routes/lightning/send/components/payment-review.ts
  • frontends/web/src/routes/lightning/send/components/review-step.tsx
  • frontends/web/src/routes/lightning/send/components/select-payment-input-step.tsx
  • frontends/web/src/routes/lightning/send/send.tsx

Comment thread frontends/web/src/routes/lightning/send/components/bolt11-review-step.tsx Outdated
Comment thread frontends/web/src/routes/lightning/send/components/bolt11-review-step.tsx Outdated
Comment thread frontends/web/src/routes/lightning/send/components/lnurl-pay-review-step.tsx Outdated
Comment thread frontends/web/src/routes/lightning/send/components/payment-review.ts Outdated
@Beerosagos Beerosagos marked this pull request as ready for review July 6, 2026 14:12
@Beerosagos Beerosagos requested a review from a team as a code owner July 6, 2026 14:12
@Beerosagos Beerosagos requested a review from bznein July 6, 2026 14:12

@bznein bznein 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.

I did a first quick pass on the backend, but I'll take some time to review more thoroughly

Comment thread backend/lightning/payments.go Outdated
Comment on lines +89 to +94
func msatToCeilSat(msat uint64) uint64 {
if msat%1000 == 0 {
return msat / 1000
}
return msat/1000 + 1
}

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.

[nit] not even sure if it's nicer to read or not, but

func msatToCeilSat(msat uint64) uint64 {
	return (msat + 999) / 1000
}

would be functionally the same (provided msat + 999 doesn't overflow)

Comment thread backend/lightning/payments.go Outdated
Comment on lines +89 to +98
func msatToCeilSat(msat uint64) uint64 {
if msat%1000 == 0 {
return msat / 1000
}
return msat/1000 + 1
}

func msatToFloorSat(msat uint64) uint64 {
return msat / 1000
}

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.

overall, I think these two methods could be combined into a single one with an additional parameter, wdyt?

Comment on lines +115 to +121
for _, entry := range metadata {
if len(entry) >= 2 && entry[0] == "text/plain" {
description := entry[1]
return &description
}
}
return nil

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.

can we add some comments here please? It's not super clear what this does :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done 👍

@Beerosagos

Copy link
Copy Markdown
Collaborator Author

@bznein all done 👍

@bznein bznein 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.

backend untested LGTM

@Beerosagos Beerosagos requested a review from thisconnect July 8, 2026 09:23
@Beerosagos

Copy link
Copy Markdown
Collaborator Author

@thisconnect can you have a look at the frontend? 🙏

@thisconnect thisconnect 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.

LGTM with some questions and small nit

Comment thread frontends/web/src/api/lightning.ts
Comment thread frontends/web/src/locales/en/app.json
const amount = event.currentTarget.valueAsNumber;
setCustomAmount(Number.isNaN(amount) ? undefined : amount);
}}
value={customAmount === undefined ? '' : `${customAmount}`}

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.

nit

Suggested change
value={customAmount === undefined ? '' : `${customAmount}`}
value={customAmount}

Comment thread frontends/web/src/routes/lightning/send/components/review-step.tsx Outdated
type TPaymentReviewDetails = TLightningBolt11Invoice | TLightningLNURLPay;

const isBolt11Invoice = (paymentDetails: TPaymentReviewDetails): paymentDetails is TLightningBolt11Invoice => (
'invoice' in paymentDetails

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.

usePaymentReview is used in <Bolt11ReviewStep> and <LNURLPayReviewStep> so we should already know what type it is.

small nit: instead checking for 'invoice' in paymentDetails key, wouldn't it make sense to add type (type: TPaymentInputType.BOLT11 | TPaymentInputType.LNURL_PAY) to TLightningBolt11Invoice and TLightningLNURLPay or similar?

no blocker it's also ok as is.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point, added a type field inside TPaymentReviewDetails to improve it.

Comment thread frontends/web/src/routes/lightning/send/hooks/use-payment-review.ts Outdated
Comment thread frontends/web/src/routes/lightning/send/hooks/use-payment-review.ts Outdated
Comment thread frontends/web/src/routes/lightning/send/hooks/use-payment-review.ts Outdated
const amountSat = isBolt11Invoice(paymentDetails) && paymentDetails.amountSat !== undefined
? undefined
: currentAmountSat;
await preparePayment(amountSat);

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.

I am reading something like "in case of error: set amount to undefined in case of Bolt11Invoice, but to currentAmountSat in case of lnurl."

but I am not 100% why that is.

Could you add a comment why this is needed?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Bolt11Invoice can already define an amount: we don't need to pass it to the prepare call in that case. So we use currentAmountSat, which is the user-defined amount, for amountless Bolt11 invoices and LNURLPay (which is amountless by definition). Will add 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.

That all makes sense, I'm more wondering why this has to be done in the catch in case of postSendPayment error.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is an edge case: it is possible that the payment fee retrieved during the prepare phase is no longer valid when trying to send. In this case, the send call returns a PAYMENT_APPROVAL_REQUIRED error and we need to re-prepare the payment to display the new fee and confirm sending again.

@thisconnect thisconnect Jul 8, 2026

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.

Makes sense. Maybe small comment about this edge case that the fee may be expired could make this line more clear.

@Beerosagos Beerosagos force-pushed the lnurl-pay branch 2 times, most recently from 4c77953 to bdf5891 Compare July 8, 2026 15:36
@Beerosagos

Copy link
Copy Markdown
Collaborator Author

rebased

@Beerosagos

Copy link
Copy Markdown
Collaborator Author

@thisconnect PTAL 🙏

@thisconnect thisconnect 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.

very nice untested LGTM

const amountSat = isBolt11Invoice(paymentDetails) && paymentDetails.amountSat !== undefined
? undefined
: currentAmountSat;
await preparePayment(amountSat);

@thisconnect thisconnect Jul 8, 2026

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.

Makes sense. Maybe small comment about this edge case that the fee may be expired could make this line more clear.

Add LNURL Pay support to the Lightning send flow while keeping rail
selection inside the backend lightning package. Parse payment inputs
into explicit BOLT11 and LNURL Pay variants, prepare and send each
rail through the shared payment endpoints, and return localized error
codes for invalid input and amounts.

Refactor the frontend review step into rail-specific BOLT11 and LNURL
Pay components. Pass each component only the payload it needs, keep
BOLT11 behavior aligned with the existing review flow, and keep LNURL
Pay as an amount-entry flow within the server-provided min and max
bounds.
@Beerosagos Beerosagos merged commit c4b8810 into BitBoxSwiss:staging-spark Jul 8, 2026
8 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.

3 participants