Skip to content

Wallet receive: external transaction table and checks#3121

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

Wallet receive: external transaction table and checks#3121
huumn wants to merge 10 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.

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

High Risk
Touches payment-adjacent persistence, background reconciliation, and wallet provider integrations; incorrect status or timeline ordering could mislead users about funds, though checks are largely read-only verification paths.

Overview
Introduces external wallet transactions (starting with receive) as first-class records with reconciliation scheduling, GraphQL types, and a dedicated /wallets/transactions/[id] page that polls status and shows QR, diagnostics, and logs.

createWalletInvoice now persists a receive row (returns transaction id), enforces a cap on recent unpaid receives, and redirects the receive flow off the inline QR page. Wallet protocols gain optional checkInvoice (plus LNURL verify context where available); a minute cron/worker claims due rows and records SETTLED / FAILED / EXPIRED / UNKNOWN outcomes with structured unknown reasons.

Satistics and wallet activity no longer return only PayIns: a merged SQL timeline pages lightweight keys across PayIn send/receive branches and ExternalTransaction, hydrates both shapes, and exposes txs: [WalletActivityItem!]!. The activity table and PayIn detail pages are refactored to shared transaction UI, centralized viewer amount math, and stricter isSend caching (Apollo keyFields: ['id','isSend']).

Supporting changes include ExternalTransaction Prisma migration and wallet log linkage, shared wallet amount/preimage/error helpers, fetch timeout behavior, and assorted wallet-client hardening (NWC ambiguous failures, WebLN enable probe, CLN rune/docs for listinvoices).

Reviewed by Cursor Bugbot for commit 8f08ff8. 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 Outdated
Comment thread api/resolvers/payIn.js Outdated
@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
Comment thread wallets/server/external-transactions.js Outdated
@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)
    • changed a few weeks later to do a check-with-lease pattern instead
  • 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. We let #3129 display this.

@huumn
huumn marked this pull request as draft July 10, 2026 05:24
@huumn
huumn force-pushed the wallet-external-tx-check branch 3 times, most recently from 92c45c3 to 5059ca8 Compare July 16, 2026 18:28
@huumn
huumn marked this pull request as ready for review July 16, 2026 18:32
Comment thread wallets/server/external-transactions.js Outdated
Comment thread api/typeDefs/wallet.js
Comment thread api/resolvers/payIn.js
@huumn

huumn commented Jul 19, 2026

Copy link
Copy Markdown
Member Author

Apologies for more churn. I backported some fixups from #3125. The important stuff:

  • resumes persisting settledAt, actualFeeMsats, from wallet providers (not displayed anywhere - just threaded, normalized, and stored)
  • add RETENTION unknownReason for when dropBolt11 drops a non-terminal receive/send
  • centralizes/simplifies grace period and parameter normalization of observations
  • first-write-wins CAS for terminal statuses (vs doing lease-gated checks that only prevented rare and harmless races)

@huumn
huumn force-pushed the wallet-external-tx-check branch from 9cc6e5b to 8f08ff8 Compare July 21, 2026 08:01
@huumn

huumn commented Jul 21, 2026

Copy link
Copy Markdown
Member Author

Force pushed (should be last time):

@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 2 potential issues.

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 8f08ff8. Configure here.

invoiceExpiresAt
unknownReason
}
`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Receive diagnostic missing direction

Medium Severity

EXTERNAL_TRANSACTION_FIELDS never requests direction, but externalTransactionDiagnosticMessage only emits the receive-specific STATUS_UNAVAILABLE copy when direction === 'RECEIVE'. On this receive-only path that check always fails, so users get the generic reconciliation message instead of the intended expired-invoice guidance on the activity table and transaction page.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f08ff8. Configure here.

msats: walletAmountToMsatsOrUndefined({ sat: payment.receivedSat }),
actualFeeMsats: payment.fees,
settledAt: { milliseconds: payment.completedAt }
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Phoenixd fee unit mismatch

Low Severity

checkInvoice wraps receivedSat with { sat: ... } before normalization, but passes payment.fees as a bare number. walletAmountToMsats treats bare numbers as msats, so phoenixd’s sat-denominated fee is stored about 1000× too small whenever a non-zero liquidity fee is present.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8f08ff8. Configure here.

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