chore: merge v0.42-dev into feat/per-wallet-filter-scan#698
Conversation
Co-authored-by: QuantumExplorer <11468583+QuantumExplorer@users.noreply.github.com>
* feat: make wallet events atomic
Reshape `WalletEvent` so each variant carries the records or context needed to persist a wallet update atomically off a single event, alongside the post-change balance.
The variant set is now:
- `TransactionReceived { wallet_id, record, balance }`. Fires when the wallet first sees an off-chain transaction.
- `TransactionStatusChanged { wallet_id, txid, context, balance }`. Fires when a known off-chain transaction has its state change. Currently fires only for InstantSend locks.
- `BlockUpdate { wallet_id, height, inserted, updated, matured, balance }`. Carries records bucketed by what happened to them in the block, plus the post-block balance.
- `SyncHeightUpdate { wallet_id, height }`. Marks a filter-batch checkpoint.
`TransactionRecord` carries `account_type` directly, identifying the owning account. `WalletInfoInterface` gains a `matured_coinbase_records` method that enumerates coinbase records crossing the maturity threshold during a height advance, populating `BlockUpdate.matured`.
The FFI groups the flattened account-discriminator fields into an `FFIAccountType` struct and renames the prior discriminant enum to `FFIAccountKind`.
* fix: record balance before bumping `block_processed_wallet_count`
Tests wait on `block_processed_wallet_count` and then read `last_confirmed`/`last_unconfirmed`. Bumping the counter before storing the balance snapshot left those reads racey. Reorder so the balance is recorded first.
Addresses CodeRabbit review comment on PR #696
#696 (comment)
* fix: place `IdentityTopUp.registration_index` in `index_secondary`
The `FFIAccountType` doc states `index_secondary` carries `registration_index` for `IdentityTopUp` and `index = 0` for variants without a meaningful primary index, matching the parallel encoding in `FFIAccountKind::from_account_type`. The `From<&AccountType>` impl wrote `registration_index` into `index` instead, breaking the documented FFI contract.
Addresses CodeRabbit review comment on PR #696
#696 (comment)
* fix: route confirmation backfills to `new_records`
`is_new` is wallet-wide (set on the first matching account, then breaks), so the per-account `else` branch can run for an account that did not previously hold the record. `confirm_transaction` backfills via `record_transaction` in that case, but the post-call record was always pushed onto `updated_records`, breaking the atomic `inserted`/`updated` contract consumed by `WalletEvent::BlockUpdate`. Capture `existed_before` per account and route to `new_records` when the record was just created.
Addresses CodeRabbit review comment on PR #696
#696 (comment)
* refactor(key-wallet-manager): extract `finalize_block_advance` helper
`process_block` and `update_last_processed_height` duplicated the entire balance-snapshot, prior-heights collection, matured-coinbase window, height advance, and per-wallet `BlockUpdate` emission. Extract the shared tail into a private `WalletManager::finalize_block_advance` helper that takes the inserted/updated maps. `update_last_processed_height` becomes a one-line call with empty maps; `process_block` keeps only its txdata loop before delegating.
* refactor: rename wallet events for clearer semantics
Rename `WalletEvent` variants and the matching FFI callbacks to past-participle names that say what happened, replacing vague "Update" suffixes:
- `TransactionReceived` -> `TransactionDetected`. "Received" implied incoming funds, but the event fires for any first-time off-chain sighting (incoming or outgoing).
- `TransactionStatusChanged` -> `TransactionInstantLocked`. The event only ever fires for an InstantSend lock applied to a known mempool tx, so name it for what it actually is. Drop the `status: TransactionContext` field and carry the `InstantLock` directly.
- `BlockUpdate` -> `BlockProcessed`. Mirrors `process_block` and matches the past-participle pattern.
- `SyncHeightUpdate` -> `SyncHeightAdvanced`. Conveys monotonic forward motion.
FFI rename mirrors the Rust side: the IS callback now takes `islock_data: *const u8` + `islock_len: usize` instead of an `FFITransactionContext`, removing a discriminant that was always `InstantSend`. The wallet-side `OnBlockProcessedCallback` becomes `OnWalletBlockProcessedCallback` to disambiguate from the existing sync-event type with the same name.
* fix: record balance before bumping IS-locked counter in test callback
Addresses CodeRabbit review comment on PR #696
#696 (review)
The instant_locked callback bumped `transaction_instant_send_locked_count` before calling `record_balance`. Tests that wait on the counter and then read `last_confirmed`/`last_unconfirmed` could observe the previous balance snapshot. Match the ordering used by the other callbacks: store the balance first, then bump the counter.
* fix: backfill missing transaction record in InstantSend path
Addresses CodeRabbit review comment on PR #696
#696 (review)
The IS-lock branch in `WalletTransactionChecker::check_core_transaction` only updated accounts that already held a `TransactionRecord` for the txid. When wallet-level `is_new` was `false` (because at least one account had the record) but another matched account did not, the latter was silently skipped: no record was created and `mark_utxos_instant_send` ran against an empty UTXO set on that account.
Mirror the confirmation path: when the affected account lacks the record, call `record_transaction` to register the record and its UTXOs, then mark them IS-locked. This ordering ensures the freshly registered UTXOs receive the IS-lock flag too. The backfilled record is pushed into `new_records` to match the existing convention from commit 659a6d5.
Add `test_instantsend_backfills_missing_record_in_other_account` covering the multi-account scenario.
Resolves conflicts between the per-wallet filter scan work (#694) and the atomic wallet events work (#696) from v0.42-dev. Conflict resolution: - `key-wallet-manager/src/lib.rs`: drop in-place event emission from `check_transaction_in_wallets`. Per #696, callers are responsible for emitting events after refreshing balances so events carry the post-change balance. The `CheckTransactionsResult.per_wallet_*` maps remain the per-wallet attribution channel. - `key-wallet-manager/src/process_block.rs`: - `process_block_for_wallets` (per-wallet API from #694) now collects `per_wallet_inserted` / `per_wallet_updated` and finalizes via `finalize_block_advance` (atomic events from #696) instead of the old `BalanceUpdated` snapshot/emit pair. - `finalize_block_advance` is parameterized on the wallet set so `process_block_for_wallets` and `update_wallet_last_processed_height` only advance and emit `BlockProcessed` for the listed wallets. Wallets already at or past `height` skip the height bump but still refresh their balance, preserving the rescan-without-going-backwards semantics from the feat branch. - `update_wallet_synced_height` now emits `SyncHeightAdvanced` for the one wallet whose height advanced (replacing the all-wallets `update_synced_height` from v0.42-dev). - `update_wallet_last_processed_height` delegates to `finalize_block_advance` over a single-wallet set so it emits a `BlockProcessed` if the balance moved, matching the v0.42-dev behavior of the deleted `update_last_processed_height`. - Test names updated for the new event types. - `key-wallet-manager/src/event_tests.rs`: take the rewritten v0.42-dev test file (designed against the new atomic events) and adapt the legacy-API call sites: `process_block(&block, h)` becomes `process_block_for_wallets(&block, h, &BTreeSet::from([wallet_id]))`, and `update_synced_height(h)` becomes `update_wallet_synced_height(&wallet_id, h)`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## feat/per-wallet-filter-scan #698 +/- ##
===============================================================
- Coverage 70.65% 70.52% -0.13%
===============================================================
Files 320 320
Lines 67692 67992 +300
===============================================================
+ Hits 47827 47952 +125
- Misses 19865 20040 +175
|
a8e381a
into
feat/per-wallet-filter-scan
* feat: per-wallet filter scan and runtime wallet catch-up Filter matching and block processing now operate per wallet, so a wallet added at runtime catches up without forcing the already-synced wallets to reprocess anything. - `WalletInterface` restructured around per-wallet ops: `process_block_for_wallets`, `wallets_behind`, `monitored_addresses_for`, `wallet_synced_height`, and monotonic per-wallet height updates. Aggregate heights are derived (min of `synced_height`, max of `last_processed_height`) rather than stored. - `FiltersManager::scan_batch` matches each behind wallet's addresses only against filter heights it hasn't yet covered; already-synced wallets are skipped entirely. Matched blocks flow through `BlocksNeeded` carrying the per-block wallet set so `BlocksManager` processes each block only against the wallets whose filters matched. `FiltersBatch` records the scanned-wallet set so commit advances only their `synced_height`. - `FiltersManager::tick` detects when a wallet's `synced_height` sits below the current `committed_height` (a runtime add behind scan progress), clears in-flight pipeline state, lowers `committed_height` to the new aggregate floor, and re-enters `start_download` on the next 100ms tick. Runs in `Syncing`, `Synced`, and `WaitForEvents`. Based on: - #689 * chore: merge v0.42-dev into feat/per-wallet-filter-scan (#698) * chore(dash-spv): refresh masternode seed files (#695) Co-authored-by: QuantumExplorer <11468583+QuantumExplorer@users.noreply.github.com> * feat: make wallet events atomic (#696) * feat: make wallet events atomic Reshape `WalletEvent` so each variant carries the records or context needed to persist a wallet update atomically off a single event, alongside the post-change balance. The variant set is now: - `TransactionReceived { wallet_id, record, balance }`. Fires when the wallet first sees an off-chain transaction. - `TransactionStatusChanged { wallet_id, txid, context, balance }`. Fires when a known off-chain transaction has its state change. Currently fires only for InstantSend locks. - `BlockUpdate { wallet_id, height, inserted, updated, matured, balance }`. Carries records bucketed by what happened to them in the block, plus the post-block balance. - `SyncHeightUpdate { wallet_id, height }`. Marks a filter-batch checkpoint. `TransactionRecord` carries `account_type` directly, identifying the owning account. `WalletInfoInterface` gains a `matured_coinbase_records` method that enumerates coinbase records crossing the maturity threshold during a height advance, populating `BlockUpdate.matured`. The FFI groups the flattened account-discriminator fields into an `FFIAccountType` struct and renames the prior discriminant enum to `FFIAccountKind`. * fix: record balance before bumping `block_processed_wallet_count` Tests wait on `block_processed_wallet_count` and then read `last_confirmed`/`last_unconfirmed`. Bumping the counter before storing the balance snapshot left those reads racey. Reorder so the balance is recorded first. Addresses CodeRabbit review comment on PR #696 #696 (comment) * fix: place `IdentityTopUp.registration_index` in `index_secondary` The `FFIAccountType` doc states `index_secondary` carries `registration_index` for `IdentityTopUp` and `index = 0` for variants without a meaningful primary index, matching the parallel encoding in `FFIAccountKind::from_account_type`. The `From<&AccountType>` impl wrote `registration_index` into `index` instead, breaking the documented FFI contract. Addresses CodeRabbit review comment on PR #696 #696 (comment) * fix: route confirmation backfills to `new_records` `is_new` is wallet-wide (set on the first matching account, then breaks), so the per-account `else` branch can run for an account that did not previously hold the record. `confirm_transaction` backfills via `record_transaction` in that case, but the post-call record was always pushed onto `updated_records`, breaking the atomic `inserted`/`updated` contract consumed by `WalletEvent::BlockUpdate`. Capture `existed_before` per account and route to `new_records` when the record was just created. Addresses CodeRabbit review comment on PR #696 #696 (comment) * refactor(key-wallet-manager): extract `finalize_block_advance` helper `process_block` and `update_last_processed_height` duplicated the entire balance-snapshot, prior-heights collection, matured-coinbase window, height advance, and per-wallet `BlockUpdate` emission. Extract the shared tail into a private `WalletManager::finalize_block_advance` helper that takes the inserted/updated maps. `update_last_processed_height` becomes a one-line call with empty maps; `process_block` keeps only its txdata loop before delegating. * refactor: rename wallet events for clearer semantics Rename `WalletEvent` variants and the matching FFI callbacks to past-participle names that say what happened, replacing vague "Update" suffixes: - `TransactionReceived` -> `TransactionDetected`. "Received" implied incoming funds, but the event fires for any first-time off-chain sighting (incoming or outgoing). - `TransactionStatusChanged` -> `TransactionInstantLocked`. The event only ever fires for an InstantSend lock applied to a known mempool tx, so name it for what it actually is. Drop the `status: TransactionContext` field and carry the `InstantLock` directly. - `BlockUpdate` -> `BlockProcessed`. Mirrors `process_block` and matches the past-participle pattern. - `SyncHeightUpdate` -> `SyncHeightAdvanced`. Conveys monotonic forward motion. FFI rename mirrors the Rust side: the IS callback now takes `islock_data: *const u8` + `islock_len: usize` instead of an `FFITransactionContext`, removing a discriminant that was always `InstantSend`. The wallet-side `OnBlockProcessedCallback` becomes `OnWalletBlockProcessedCallback` to disambiguate from the existing sync-event type with the same name. * fix: record balance before bumping IS-locked counter in test callback Addresses CodeRabbit review comment on PR #696 #696 (review) The instant_locked callback bumped `transaction_instant_send_locked_count` before calling `record_balance`. Tests that wait on the counter and then read `last_confirmed`/`last_unconfirmed` could observe the previous balance snapshot. Match the ordering used by the other callbacks: store the balance first, then bump the counter. * fix: backfill missing transaction record in InstantSend path Addresses CodeRabbit review comment on PR #696 #696 (review) The IS-lock branch in `WalletTransactionChecker::check_core_transaction` only updated accounts that already held a `TransactionRecord` for the txid. When wallet-level `is_new` was `false` (because at least one account had the record) but another matched account did not, the latter was silently skipped: no record was created and `mark_utxos_instant_send` ran against an empty UTXO set on that account. Mirror the confirmation path: when the affected account lacks the record, call `record_transaction` to register the record and its UTXOs, then mark them IS-locked. This ordering ensures the freshly registered UTXOs receive the IS-lock flag too. The backfilled record is pushed into `new_records` to match the existing convention from commit 659a6d5. Add `test_instantsend_backfills_missing_record_in_other_account` covering the multi-account scenario. --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: QuantumExplorer <11468583+QuantumExplorer@users.noreply.github.com> Co-authored-by: Kevin Rombach <35775977+xdustinface@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: QuantumExplorer <quantum@dash.org> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: QuantumExplorer <11468583+QuantumExplorer@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves the merge conflicts blocking #694 by merging the latest
v0.42-devintofeat/per-wallet-filter-scan. Once this lands on the feature branch, #694 will go fromCONFLICTINGto mergeable.Source of conflicts
v0.42-devadvanced ahead of the feature branch by #695 (masternode seed refresh, no overlap) and #696 (atomic wallet events). The wallet-events refactor changed the same files that #694 restructured around per-wallet processing:key-wallet-manager/src/lib.rscheck_transaction_in_wallets; #696 moved emission to callers so events can carry the post-balancekey-wallet-manager/src/process_block.rsprocess_block_for_wallets,wallets_behind,update_wallet_synced_height, …); #696 addedfinalize_block_advanceand replacedBalanceUpdated/TransactionReceived/TransactionStatusChangedwithTransactionDetected/TransactionInstantLocked/BlockProcessed/SyncHeightAdvancedkey-wallet-manager/src/event_tests.rsResolution
lib.rs— drop the in-place event emission insidecheck_transaction_in_wallets(per feat: make wallet events atomic #696, callers emit after refreshing balance). Keep feat: per-wallet filter scan and runtime wallet catch-up #694'snew_addressesBTreeMap<WalletId, _>so per-wallet attribution survives.process_block.rs—process_block_for_walletsnow collectsper_wallet_inserted/per_wallet_updatedper feat: make wallet events atomic #696 and callsfinalize_block_advance(height, wallets, …)to emitBlockProcessedonly for the listed wallets.finalize_block_advanceis parameterized on the wallet set, so the per-wallet API only advances/emits for those wallets. Wallets already at or pastheightskip the height bump but still refresh balance — preserving the rescan-without-going-backwards behavior introduced on the feat branch.update_wallet_synced_heightnow emitsSyncHeightAdvancedfor the wallet that advanced (the per-wallet analog of feat: make wallet events atomic #696'supdate_synced_height).update_wallet_last_processed_heightdelegates tofinalize_block_advanceover a single-wallet set, so it emitsBlockProcessedwhen the balance moved (matching what feat: make wallet events atomic #696's deletedupdate_last_processed_heightdid, but for one wallet at a time).test_process_block_emits_balance_updated→test_process_block_emits_block_processed; renamed/rewrotetest_update_synced_height_*→test_update_wallet_synced_height_*against the per-wallet API.event_tests.rs— start from feat: make wallet events atomic #696's rewritten file, then adapt API calls:manager.process_block(&block, h)→manager.process_block_for_wallets(&block, h, &BTreeSet::from([wallet_id]))manager.update_synced_height(h)→manager.update_wallet_synced_height(&wallet_id, h)Verification
cargo build --workspacecargo fmt --all --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspace --exclude dash-spv --exclude dash-spv-ffi --exclude integration_testcargo test -p key-wallet-manager(30/30 unit tests pass, including all event tests)cargo test -p dash-spv --lib(404/404 pass)Test plan
MERGEABLEand CI is green there tooWalletEvent::BlockProcessedevent in an SPV integration test (e.g.dashd_sync) — it should carry per-walletinserted/updated/matured/balancefor the right wallet and only fire for wallets that were behind🤖 Generated with Claude Code