Skip to content

Wallet receive: external transaction table and checks#3121

Open
huumn wants to merge 13 commits into
masterfrom
wallet-external-tx-check
Open

Wallet receive: external transaction table and checks#3121
huumn wants to merge 13 commits into
masterfrom
wallet-external-tx-check

Conversation

@huumn

@huumn huumn commented Jun 24, 2026

Copy link
Copy Markdown
Member

I'm happy with the shape of this now. I still need to:

  1. QA/review/verify every checkInvoice implementation
  2. QA/review error paths

This contains the core logic/relations for external transactions (ie payments that SN is not party to) + the receive side of external txs. Send side will be its own PR.

Vaguely:

  • creates a db table for external txs
  • polls server-side in a best effort manner for receive verification where wallets support them
  • extends scheduled bolt11 deletion to external txs
  • unions external txs with payins in /satistics and /wallets/[id]/activity
  • implements /wallets/[wid]/transactions/[id] for viewing external txs, sharing components/design with payins /transactions/[id]
    • on make invoice we navigate to this transaction page (we'll do this for send too)
  • adds checkInvoice to all wallets that have some means of checking invoice status
    • for wallets that don't support checkInvoice, or if an invoice expires and we don't have a definite settled/failed reported by the wallet, external tx statuses are marked as unknown and we attempt to report why it's unknown

Punted to other PRs:

  • send with send checks
  • external tx receive notifications
  • proxy on/off global setting with per receive proxy option

Note

Medium Risk
Touches wallet receive flows, activity pagination, and background settlement polling against external providers; receive-only in this PR but incorrect status or union ordering could mislead users about funds.

Overview
Adds ExternalTransaction persistence for wallet Lightning activity SN does not route as a PayIn, starting with receive: createWalletInvoice now creates a row (with optional verificationContext, e.g. LNURL verify) and returns transaction; the receive flow navigates to /wallets/transactions/[id] instead of showing a QR inline.

Settlement is polled server-side via new checkInvoice hooks on supported protocols (Blink, CLN REST, LNbits, LND gRPC, NWC, Phoenixd, LN Addr), with classifyExternalTransactionCheck mapping results to PENDING / SETTLED / FAILED / UNKNOWN (including expiry and permission/verification reasons). pg-boss jobs checkExternalTransaction and checkPendingExternalTransactions drive checks; reading a live receive can poke a debounced job.

Activity feeds change: satistics returns txs: [WalletActivityItem!]! (PayIn ∪ ExternalTransaction) using a sort-key union plus hydrateWalletActivity, with deterministic pagination tiebreaks. Wallet activity tables and Apollo cache policies follow txs.

UI shares TransactionDetail* layout between PayIn and external pages (QR for payable receives, status, invoice details, WalletLogs filtered by externalTransactionId). Scheduled bolt11 retention also clears sensitive fields on old external rows.

Reviewed by Cursor Bugbot for commit b54e160. Bugbot is set up for automated code reviews on this repo. Configure here.

@huumn huumn force-pushed the wallet-external-tx-check branch 3 times, most recently from b204a0c to bdd5c6b Compare June 25, 2026 21:14
@huumn huumn marked this pull request as ready for review June 25, 2026 21:28
Comment thread components/payIn/table/external.js
Comment thread api/resolvers/payIn.js
@huumn huumn force-pushed the wallet-external-tx-check branch from b03fca8 to 6a4c970 Compare June 27, 2026 17:58
Comment thread api/resolvers/wallet.js

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b54e160. Configure here.

if (transaction?.direction !== 'RECEIVE') return
if (externalTransactionFinal(transaction) && !externalTransactionResolvesLocally(transaction)) return
// debounce to roughly one check per page poll
if (new Date(transaction.updatedAt).getTime() > Date.now() - 10_000) return

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Poke debounce skips new checks

Medium Severity

pokeExternalTransactionCheck skips enqueueing a worker check whenever updatedAt is within the last ten seconds. A receive row is created with a fresh updatedAt, and the make-invoice flow immediately opens its transaction page, so the first reads that are meant to drive verification never queue a check until that window passes.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b54e160. Configure here.

@huumn

huumn commented Jul 3, 2026

Copy link
Copy Markdown
Member Author

The fixups I pushed are back ported from work on #3125:

  • timeoutSignal that allows child signals to respect aborts of parent signals
    • this makes abortable loops that loop over abortable wallet checks easier to reason about/synchronous-like
  • schema changes
  • improvements to the centralized tx status classifier
  • reduce invoice checking code by having external tx polling in the UI drive status rechecks (in addition to the slower, scheduled backstop/reaper)
  • reuse in the lnbits adapter
  • centralizing the where clause used to fetch txs that are actively checked

There's still some weirdness in how we surface metadata we store from checkInvoice (and checkPayment). I'll give it a full brain before merging.

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