Skip to content

Recover stranded payjoin stack (pj/01-06) - #2504

Merged
i5hi merged 31 commits into
payjoin-upgradefrom
payjoin-salvage
Jul 24, 2026
Merged

Recover stranded payjoin stack (pj/01-06)#2504
i5hi merged 31 commits into
payjoin-upgradefrom
payjoin-salvage

Conversation

@i5hi

@i5hi i5hi commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

The Jul 21 stacked merges of #2478 #2479 #2480 #2481 landed in their parent branches (pj/00, pj/01) instead of payjoin-upgrade, so that work never reached the integration branch. This PR merges it in.

  • Merge pj/01-core-engine: core engine hardening, send stuck-flow fix, receive toggle + eligibility guard, transaction status display
  • Merge pj/06-csv-utc: CSV export UTC fix
  • Restore dismissed_announcements table (needed by feat(announcements): dismissible home announcement carousel #2484)
  • Conflicts in receive bloc/state resolved in favor of the reviewed stack versions

Verified: analyze clean, 765/765 tests pass, content containment of pj/00-05 proven via git.

ethicnology and others added 30 commits July 20, 2026 10:12
Building a plain (local) DateTime from a UTC end date's wall-clock
fields shifted the inclusive-day boundary by the device's UTC offset, so
the same export included or excluded edge transactions depending on the
machine's timezone. Round up in UTC when the input is UTC.
WatchPayjoinUsecase now emits both PayjoinReceiver and PayjoinSender
(scoped by ids); the send flow needs sender completion events a
receiver-only filter would swallow. ReceiveBloc keeps its receiver-only
behaviour by filtering the concrete type at its own call site.
Adds a sealed WalletFailure family and a single-transaction lookup
use-case returning Result<WalletTransaction?, WalletTransactionLookupFailure>
(Ok(null) = synced-and-absent, Err = infra failure), per the #1895
Result migration. Consumed by the payjoin engine and transaction
details.
PdkPayjoinDatasource gains stopPolling(id) and an idempotent dispose()
so a resolved session's directory poll can be cut (no more stale expiry
events) and tests get a clean teardown. The local datasource filters
include isAborted and fetchByTxId matches originalTxId too, and sender
poll logs/exceptions use a hashed logRef instead of the raw BIP21 URI.
The core of the fix. A unified backoff-poll engine arms two per-session
watchers — one for the real payjoin tx, one for the original (fallback)
tx — so a session always reaches a terminal state (completed or
aborted) instead of hanging. tryBroadcastOriginalTransaction is split
into a guarded public entry (refuses via canManuallyBroadcastOriginal,
emits on the stream) and the internal mechanism; expiry/proposal
processing re-fetch the fresh row and bail on terminal; resume moves out
of the constructor to resumePayjoinsOnStartup, called from AppLocator
once wallet/labels deps are registered. Wallet repos are injected as
lazy closures; the directory receiveTimeout is raised to 35s (above the
30s long-poll hold). Adds the PAYJOIN --> LABELS edge to FEATURES.md.
…2246)

SendCubit._watchPayjoin now owns resolving the flow: a completed/aborted
sender session moves to success with the on-chain txid, syncs the wallet
and stores the user label on the final txid; an expired session returns
to confirm with a broadcast-failure error and clears the provisional
txId/payjoinSender so a retry starts clean. Payjoin is attempted only
when willAttemptPayjoin holds (global setting on, locally-signing
wallet, non-self BIP21 with a pj= param), fail-closed by default.
The bitcoin confirm review shows a 'payjoin will be attempted' card; the
success screen says when a send fell back to a plain transaction and
navigates to the broadcast tx by txid (never the sender BIP21 URI); the
coordinating screen shows a fallback countdown, gated on
canManuallyBroadcastOriginal and only when the fallback is imminent
(<=1h), and localizes its title. Adds the SEND --> TX_HISTORY edge.
Gates payjoin session creation on the global setting and carries
payjoinMinAmountSat in state (with isPayjoinBelowMinimum); drops the
per-address payjoin toggle in favour of that setting. Adds
isPayjoinFlowOwningNavigation so the shell's generic tx-received
listener defers to the payjoin screen, and lets the in-progress screen
be entered on any post-started status. Guards the manual original-tx
broadcast behind canManuallyBroadcastOriginal, adds isClosed guards to
the watchers, splits the new-address error like the start path, and
fixes the lightning in-progress navigation to use the route name.
Adds the RECEIVE --> TX_HISTORY edge.
Rewrites the payjoin-in-progress screen to distinguish below-minimum,
generic fallback, real payjoin and expired outcomes with their own copy,
auto-navigates to transaction details on a real completion, and shows a
fallback countdown (imminence-gated) plus a View Details / receive
normally action driven by canManuallyBroadcastOriginal.
White in both light and dark mode, mirroring onError next to error. Lets
a green success surface use theme-correct white text instead of reaching
for a mismatched onSecondary/onError token.
Adds an optional action widget slot alongside the existing
onAction/actionIcon plain-icon-button trailing, for a screen that needs
something richer than a single icon (e.g. the receive screen's payjoin
toggle chip). Positioned like the plain icon button — no extra offset
stacked on top of its own padding — so it stays aligned with the title.
Adds a green/red toggle chip (ReceivePayjoinToggleButton) to the
receive TopBar, wired to the global setting — only for a Bitcoin
receive with a payjoin-capable wallet (funded, locally-signing), so it
never shows on Liquid/Lightning or non-eligible wallets. Tapping flips
the setting; long-pressing opens the payjoin settings screen. The
BlocProvider now wraps the whole ReceiveScaffold (not just its body) so
the TopBar can host it, gated behind a narrow selector so switching
receive networks still does not rebuild the scaffold.

ReceiveBloc now watches payjoinEnabledChangeStream live, creating or
clearing the session the moment the setting changes anywhere in the
app — no need to leave and re-enter the receive screen. Session
creation (and the toggle itself) is also gated on the wallet having a
confirmed balance (isPayjoinToggleable/_isPayjoinEligible): a payjoin
proposal needs at least one UTXO, so there is nothing to toggle for an
empty wallet, and isPayjoinLoading resolves immediately instead of
waiting forever for a session that will never be created.

Also suppresses the pj= endpoint from the QR (without tearing down the
session) when the requested amount is below the configured anti-probing
minimum, since the receiver would decline it anyway — with an
explanatory hint, and it reappears the instant the amount is raised or
cleared. Adds the RECEIVE --> SETTINGS edge (payjoin settings
long-press).
Adds Transaction.displayPayjoinStatus: when the broadcast transaction is
visible its txid is authoritative (== payjoin txid -> completed, ==
original txid -> aborted), else the session status. The status label and
details table consume it, and the table's status row becomes an
exhaustive switch (no more raw status.name leaking to the user).
TransactionDetailsCubit reloads on payjoin events (not just syncs),
resolves a resolved session's broadcast txid (local then a forced
Result-based lookup) and re-enters by that txid so the screen lands on
the real transaction instead of a session placeholder. The manual
original-tx broadcast and its button gate on canManuallyBroadcastOriginal,
with tightened subscription hygiene (cancel-before-replace, isClosed
guards).
Adds a paged carousel of dismissible announcements on the wallet home,
shown between the balance header and the wallet list. Ships two
compile-time announcements: a payjoin-privacy nudge (once the wallet has
transaction history and payjoin is off) and an autoswap-active notice.

Each card is tappable to its target (payjoin/autoswap settings) and
dismissible via an explicit x with a Read/Dismiss confirmation; the
Read option opens the same target as tapping. Dismissals persist in the
dismissed_announcements table with a permanent/snooze policy per
announcement. The carousel adapts its height to the text scale and
collapses to nothing when no announcement is visible.

Built on bull_ui (BullInfoCard + PageView) and wired via a thin cubit
that re-evaluates on payjoin-setting changes and wallet-sync completion.
fix(send): resolve stuck sender flow on payjoin fallback (#2246)
feat(receive): payjoin toggle badge in TopBar with eligibility guard
feat(transactions): display payjoin status and land on broadcast tx
feat(announcements): dismissible home announcement carousel
@i5hi
i5hi merged commit eff002d into payjoin-upgrade Jul 24, 2026
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