Conversation
When a transaction is confirmed before the wallet sees it unconfirmed, only onchainTransactionConfirmed fires. The received sheet was wired solely to onchainTransactionReceived, so straight-to-confirmed receives showed no notification. Both handlers now share one check-and-show helper. Fixes synonymdev#455.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3df9c525b9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Routing both onchainTransactionReceived and onchainTransactionConfirmed through the shared presenter means two tasks can run for one txid. The seen-check and mark were not atomic across awaits, so both events could present the sheet. Reserve the txid synchronously on the MainActor before any await so only the first event presents it. Addresses the PR synonymdev#588 review.
|
Hint: As a general note, we expect PR authors to I know some teams might get some members flamey over this, not the case with us. |
|
Got failure from the e2e test. After CPFP boost and subsequent restoration of the wallet we got the notification of previously received transaction in UI, pls see recording: boost_-_Boost_boost_1_-_Can_do_CPFP-2026-06-10T21-22-29-359Z.mp4Attaching also all artifacts that include simulator logs. |
On a wallet restore the activity store is rebuilt without seenAt, so the initial on-chain sync replays onchainTransactionConfirmed for historical receives. The shared presenter then showed the "Received" sheet for an old transaction, which covered the activity list and timed out the CPFP restore e2e. Set a pendingRestoreActivitySeen flag on the restore success screen, have the presenter bail while it is set, and on the first post-restore on-chain syncCompleted mark all unseen activities as seen and clear the flag. New straight-to-confirmed receives still notify once the flag clears, so synonymdev#455 stays fixed. Addresses the PR synonymdev#588 review.
@piotr-iohk I did some more digging on this, and it definitely seems to be an issue. Root cause: on a wallet restore the activity store is rebuilt with no Fix: a short-lived suppression flag that mirrors the existing migration handling, scoped to seed restore.
After the flag clears, genuinely new straight-to-confirmed receives notify again, so #455 stays fixed. Verified on regtest:
The |
…ed-received-sheet
jvsena42
left a comment
There was a problem hiding this comment.
Code overall is good, no issues found in the tests
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Port the Android confirmed-only guards from synonymdev/bitkit-android#1299: a confirmed event opens the received sheet only when its block time is within one hour of the device clock and no migration is running, so the confirmations a full wallet scan replays for old txs stay silent. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ed-received-sheet # Conflicts: # journeys/README.md
|
QA reviewed on 33cf753. Agree with the open comment on the restore hold: a new mempool receive during seed restore returns before the txid is reserved, the cutoff sweep leaves it unseen, and clearing the hold does not present it. This head also makes a late confirmation of that same receive silent. shouldPresentConfirmedOnlyReceive skips a confirmed event whose block time is more than an hour from the device clock, so the later confirm is not a second chance. The one-hour window and the migration skip otherwise match merged bitkit-android#1299. The background notification journey is correctly not ported. Run Tests passed. No simulator run in this pass. The React Native migration case is still unchecked. |
|
@jvsena42 Android nightly on master This PR has the same one-hour window and the same restore hold. A receive confirmed within the last hour can leave that sheet up if the event is handled after the hold is cleared. Not reproduced on iOS. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@piotr-iohk thanks — fixed here in |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jvsena42
left a comment
There was a problem hiding this comment.
Since my last pass: 6c8c252 ports the restore journey and e86d0ae adds the hold log line it greps for. a84277a answers the three earlier threads. A restore failure now clears the hold, the restore tip is recorded only after markAllSeen succeeds, and receives arriving during the sweep are held and replayed. One LOW refinement is on the replay thread: held receives are dropped after a failed sweep. Not independently verified.
Checked and clean:
- Mempool-then-confirmed double sheet: blocked in-session by
receivedSheetInFlightTxidsand across relaunch by persistedseenAt. ldk-node writes the payment store before emitting the events, so the activity exists when it's marked seen. SyncCompleted{OnchainWallet}is emitted only after a successful sync that covers every monitored address type (ldk-node 0.7.0-rc.66), so the recorded tip only silences blocks the restore scanned.- Sends, self-transfers, channel/transfer txs and value-preserving RBF are filtered by
amountSats <= 0,shouldShowReceivedSheetandboostTxIds, the same as the mempool path. - Cutoff sweep: restored confirmed history gets the block timestamp, so it's swept; a mid-restore mempool receive keeps a later timestamp and stays unseen.
- The hold keys survive the backup's settings restore because
SettingsBackupConfigdoesn't list them, andAppReset.wipeclears them. - Kill mid-hold: the first onchain sync after relaunch sweeps with the original cutoff.
- The confirmed-only guard matches Android's
canShowConfirmedOnly: migration skip, 1h window, tip guard.
Ran manually ✅ Screen.Recording.2026-09-25.at.14.44.21.mov |
Fixes #455
Related: synonymdev/bitkit-android#797, Android twin synonymdev/bitkit-android#1299
Also fixes on iOS the restore case reported in synonymdev/bitkit-android#1342 (Android fix: synonymdev/bitkit-android#1343)
This PR shows the received sheet for on-chain deposits the wallet first sees already confirmed, without a prior mempool event.
Description
onchainTransactionConfirmedto the same received-sheet flow asonchainTransactionReceived, so a deposit mined before any wallet sync saw it in the mempool is no longer silent.await, and the persistedseenAtstill blocks a mempool-first tx from showing again when it confirms.AppViewModel.shouldPresentConfirmedOnlyReceive, matching Android'sMAX_CONFIRMED_ONLY_AGEand migration skip). A full wallet scan, as after a migration or when an address type starts being monitored, replays confirmations for old txs; those stay silent.RestoreWalletViewsetspendingRestoreActivitySeenSincebefore the node starts; the first on-chainsyncCompletedmarks activities that existed before the restore began as seen, then clears the flag. The flag is cleared only when that pass succeeds, and the cutoff keeps a payment arriving mid-restore unseen so it still notifies. Same rule as Android.restoreSyncedBlockHeight) and skips confirmed-only sheets at or below it, so a later rescan or a late-handled event for a pre-restore tx confirmed within the hour stays silent ([Bug]: historical on-chain receive sheet stays up after mnemonic restore bitkit-android#1342). Receives that arrive while the sweep runs are held and presented once it succeeds; whatever the restore scan itself emitted is dropped as history, since a payment arriving mid-scan looks the same as a replayed unconfirmed one. A failed restore clears the hold.markAllUnseenActivitiesAsSeenreturns whether the pass completed and takes an optionalstartedBeforecutoff; the post-migration caller keeps its full sweep.ReceiveOnchainAddress/ReceiveLightningAddressidentifiers toCopyAddressCard, matching the Android test tags, and ports theonchain-receivejourneys.Out of Scope
AppViewModel: a reorg that replaces a block at or below the restore tip with one paying the wallet a tx it never saw in the mempool shows no sheet;SyncCompletedreports only a height, so the replacement block cannot be told apart. A tx seen in the mempool first still notifies.AppViewModel: the one-hour window uses the device clock, so a device off by more than an hour never shows a confirmed-only receive. ldk-node emits the confirmed event once, so it cannot be recovered. Same limitation as Android; the mempool path is unaffected.AppViewModel: a confirmed-only deposit found more than one hour after its block (app closed while it confirmed) stays silent.confirmed-only-background-notification.xmlis not ported (recorded injourneys/README.md).Design
Reuses the existing received sheet with no visual change: Payment received event
Preview
bitkit-455-before-after.mp4
Regtest deposit mined immediately, so the tx goes straight to confirmed:
QA Notes
Journeys
confirmed-only-received-sheet.xml— a deposit mined before any sync shows the received sheet once, with only the confirmed event loggedmempool-then-confirmed-single-sheet.xml— a mempool-first deposit shows one sheet and no second one when it confirmsrestore-recent-receive-stays-silent.xml— a deposit confirmed within the hour stays silent after restoring its seed, and a deposit mined after the restore still shows the sheetManual Tests
Automated Checks
ConfirmedOnlyReceiveGuardTests.swift— confirmed-only receives show inside the one-hour window in both directions and skip outside it, during a migration, or at or below the restore tipRestoreActivitySeenSuppressionTests.swift— the restore hold outlives the marking pass, survives a failed pass, is bounded by the restore start, is skipped when no restore is pending, records the restore tip, drops receives from the restore scan, replays receives held during the sweep, and runs one sweep at a timeMarkAllUnseenActivitiesCutoffTests.swift— against a real DB, the sweep marks activity older than the cutoff and leaves newer activity unseen; without a cutoff it marks everything🤖 Generated with Claude Code