Skip to content

feat: prevent lightning invoice payments when no funds exist#1367

Open
Leeyah-123 wants to merge 2 commits intojamaljsr:masterfrom
Leeyah-123:fix/pay-invoice-zero-liquidity
Open

feat: prevent lightning invoice payments when no funds exist#1367
Leeyah-123 wants to merge 2 commits intojamaljsr:masterfrom
Leeyah-123:fix/pay-invoice-zero-liquidity

Conversation

@Leeyah-123
Copy link
Copy Markdown

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:

  • Pre-Payment Validation: Added logic in the Lightning model (lightning.ts) to verify if a node has either a confirmed wallet balance or available outbound channel liquidity before attempting to pay an invoice.
  • Improved UI Feedback: Updated the Pay Invoice Modal (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.
  • Helper Utilities: Extracted validation logic for inbound/outbound liquidity into cleanly separated utility functions (utils.ts).
  • Unit Tests: Added comprehensive test coverage in lightning.spec.ts to ensure the payment rejection works securely when liquidity is missing, and processes successfully when available.

Steps to Test

  1. Create a network with default settings.
  2. Create an invoice with a node, and try paying with another, unfunded node.

Screenshots

Screen.Recording.2026-03-27.at.15.49.14.mov

Prevent paying invoices from nodes that have neither wallet funds nor wallet liquidity.

re jamaljsr#1358
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 27, 2026

Greptile Summary

This 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 payInvoice model thunk and the Pay Invoice Modal UI.\n\nKey changes:\n- src/lib/lightning/utils.ts — new utility helpers getConfirmedLightningWalletBalance and getOutboundLightningLiquidity with safe parsing; clean and well-tested\n- src/store/models/lightning.tspayInvoice now calls getWalletBalance + getChannels upfront and throws NO_LIGHTNING_PAYMENT_FUNDS_ERROR if both confirmed balance and outbound channel liquidity are zero; the backend guard is sound\n- src/components/designer/lightning/actions/PayInvoiceModal.tsx — adds a hasNoPaymentFunds flag, a warning <Alert>, and a disabled OK button; however, the modal's useAsync only loads channels for litd nodes and never loads walletBalance for any node, so the proactive UI guard won't activate for LND/CLN/eclair nodes unless state was already populated by a prior event (e.g. mineListener)\n- The balanceChannels internal flow now triggers two extra API calls (getWalletBalance + getChannels) per rebalancing payment due to the unconditional guard inside payInvoice\n- The <Loader /> previously shown while litd asset info loaded was removed, which may briefly show an empty asset dropdown on litd networks

Confidence Score: 4/5

The backend guard in payInvoice is correct and prevents the NaN bug, but the proactive UI warning doesn't work for LND/CLN/eclair nodes on first open due to missing balance/channel fetching in the modal.

One P1 finding: hasNoPaymentFunds relies on state being pre-populated, but the modal's useAsync never fetches walletBalance (and skips non-litd nodes for channels entirely). The disabled button and alert — the core UI improvement of this PR — silently do nothing for LND nodes until after the first failed click. The model-level throw is correct and prevents the original NaN bug, but the stated UI improvement is incomplete.

src/components/designer/lightning/actions/PayInvoiceModal.tsx — the useAsync hook needs to load walletBalance and channels for the selected node regardless of implementation

Important Files Changed

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
Loading

Comments Outside Diff (1)

  1. src/store/models/lightning.ts, line 461-505 (link)

    P2 balanceChannels now triggers unintended API calls per payment

    balanceChannels already fetches fresh channels for every node (lines 471–477) before building the payment plan. However, it calls payInvoice in parallel for each rebalancing step (via Promise.all), and each payInvoice call now unconditionally fires getWalletBalance + getChannels again 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 _payInvoice thunk that skips the balance pre-check, or accept a skipFundsCheck option, so balanceChannels can call it directly without the redundant fetches.

Reviews (1): Last reviewed commit: "feat: prevent lightning invoice payments..." | Re-trigger Greptile

Comment on lines 58 to 66
@@ -60,9 +65,29 @@ const PayInvoiceModal: React.FC<Props> = ({ network }) => {
}
}, [network.nodes, visible]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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]);

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.

1 participant