feat: prevent lightning invoice payments when no funds exist#1367
feat: prevent lightning invoice payments when no funds exist#1367Leeyah-123 wants to merge 2 commits intojamaljsr:masterfrom
Conversation
Prevent paying invoices from nodes that have neither wallet funds nor wallet liquidity. re jamaljsr#1358
Greptile SummaryThis PR fixes a bug where paying a Lightning invoice from a node with 0 funds produced a "Sent NaN sats" toast by adding pre-payment balance validation in both the Confidence Score: 4/5The backend guard in One P1 finding: src/components/designer/lightning/actions/PayInvoiceModal.tsx — the
|
| Filename | Overview |
|---|---|
| src/components/designer/lightning/actions/PayInvoiceModal.tsx | Adds hasNoPaymentFunds guard and warning alert, but useAsync never loads walletBalance (and loads channels only for litd), so the proactive disabled-button/alert doesn't activate for LND/CLN/eclair nodes on first open; also removes the Loader shown during litd asset fetching |
| src/store/models/lightning.ts | Adds pre-payment validation in payInvoice that fetches fresh balance + channels before checking funds; correct for direct user payments but adds unintended overhead when called by balanceChannels |
| src/lib/lightning/utils.ts | New utility file with clean, well-guarded helpers for confirmed wallet balance and outbound channel liquidity; correctly handles undefined inputs and non-finite parse results |
| src/store/models/lightning.spec.ts | Adds two well-structured model-level tests covering the reject-when-no-funds and allow-when-outbound-liquidity paths; both mock correctly and assert payInvoice is not called when funds are absent |
| src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx | Adds a useful test for the no-funds error path and correctly sets up baseline mocks in beforeEach; the test validates the post-failure state (alert appears after click) rather than the proactive disabled state, which mirrors the actual behavior gap in the implementation |
Sequence Diagram
sequenceDiagram
participant User
participant PayInvoiceModal
participant LightningModel
participant LightningAPI
User->>PayInvoiceModal: Open modal (select node)
Note over PayInvoiceModal: useAsync fires<br/>(litd nodes only: getChannels)
Note over PayInvoiceModal: hasNoPaymentFunds = false<br/>if walletBalance/channels undefined
User->>PayInvoiceModal: Click "Pay Invoice"
PayInvoiceModal->>LightningModel: payInvoice(node, invoice)
LightningModel->>LightningAPI: getBalances(node) [NEW]
LightningAPI-->>LightningModel: walletBalance
LightningModel->>LightningAPI: getChannels(node) [NEW]
LightningAPI-->>LightningModel: channels
alt confirmedBalance <= 0 AND outboundLiquidity <= 0
LightningModel-->>PayInvoiceModal: throw NO_LIGHTNING_PAYMENT_FUNDS_ERROR
PayInvoiceModal->>User: Toast error + Alert warning shown
Note over PayInvoiceModal: State now populated<br/>hasNoPaymentFunds = true<br/>Button disabled on retry
else Has funds or outbound liquidity
LightningModel->>LightningAPI: payInvoice(node, invoice)
LightningAPI-->>LightningModel: receipt
LightningModel-->>PayInvoiceModal: receipt
PayInvoiceModal->>User: Sent X sats from node toast
end
Comments Outside Diff (1)
-
src/store/models/lightning.ts, line 461-505 (link)balanceChannelsnow triggers unintended API calls per paymentbalanceChannelsalready fetches fresh channels for every node (lines 471–477) before building the payment plan. However, it callspayInvoicein parallel for each rebalancing step (viaPromise.all), and eachpayInvoicecall now unconditionally firesgetWalletBalance+getChannelsagain before the payment.For a network with N channels being rebalanced this results in 2× extra API calls per payment, all happening concurrently. While not a correctness issue (the source node always has outbound channel balance during rebalancing, so the guard passes), this is unexpected side-effect overhead introduced by adding the guard unconditionally inside
payInvoice.Consider an internal
_payInvoicethunk that skips the balance pre-check, or accept askipFundsCheckoption, sobalanceChannelscan call it directly without the redundant fetches.
Reviews (1): Last reviewed commit: "feat: prevent lightning invoice payments..." | Re-trigger Greptile
| @@ -60,9 +65,29 @@ const PayInvoiceModal: React.FC<Props> = ({ network }) => { | |||
| } | |||
| }, [network.nodes, visible]); | |||
There was a problem hiding this comment.
Proactive warning doesn't work for LND/CLN/eclair nodes
The useAsync hook only fetches channels for litd nodes — it never calls getWalletBalance or getChannels for LND, c-lightning, or eclair nodes. Since hasNoPaymentFunds (line 84) requires both nodes[selectedName]?.walletBalance !== undefined and nodes[selectedName]?.channels !== undefined, the disabled button and alert won't activate proactively when a non-litd node with 0 funds is selected.
In practice the state is populated by mineListener during normal use, but immediately after network creation — or if mineListener hasn't fired yet — the guard is silently skipped and the user must click "Pay Invoice" once to trigger the model-level check, which then populates state and retroactively shows the warning.
The existing test (should display an error when the node has no funds...) confirms this: the button is not disabled before the click, and the alert only appears after the first failed attempt.
To make the proactive warning work reliably for all node types, add getWalletBalance and getChannels calls for the selected node inside the useAsync:
useAsync(async () => {
if (!visible) return;
if (selectedNode) {
await getWalletBalance(selectedNode);
await getChannels(selectedNode);
}
const litNodes = network.nodes.lightning.filter(n => n.implementation === 'litd');
for (const node of litNodes) {
await getInfo(node);
await getChannels(node);
await getAssetRoots(mapToTapd(node));
}
}, [network.nodes, visible, selectedNode]);
Prevent paying invoices from nodes that have neither wallet funds nor wallet liquidity.
re #1358
Description
This PR addresses a usability issue where the application previously allowed users to attempt paying Lightning invoices from nodes with 0 sats and no outbound liquidity, resulting in a "Sent NaN sats from ..." toast message.
Changes included:
lightning.ts) to verify if a node has either a confirmed wallet balance or available outbound channel liquidity before attempting to pay an invoice.PayInvoiceModal.tsx) to disable payment execution and display a helpful warning alert when the selected node lacks funds, guiding the user to either fund the node or open a channel with outbound liquidity first.utils.ts).lightning.spec.tsto ensure the payment rejection works securely when liquidity is missing, and processes successfully when available.Steps to Test
Screenshots
Screen.Recording.2026-03-27.at.15.49.14.mov