Skip to content

refactor(wallet-events): atomic events carrying records + balance#680

Open
QuantumExplorer wants to merge 3 commits intov0.42-devfrom
claude/priceless-mcclintock-f933d4
Open

refactor(wallet-events): atomic events carrying records + balance#680
QuantumExplorer wants to merge 3 commits intov0.42-devfrom
claude/priceless-mcclintock-f933d4

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented Apr 23, 2026

Summary

  • Replaces TransactionReceived / TransactionStatusChanged / BalanceUpdated on WalletEvent with three self-contained variants: MempoolTransactionReceived, TransactionInstantSendLocked, BlockProcessChange. Each carries the transaction record(s) involved plus the wallet's post-event balance so consumers can persist transactions + balance atomically off a single event.
  • BlockProcessChange bundles all records updated by a block into a single per-wallet event; an empty transactions_updated carries balance-only drift (e.g. coinbase maturing as synced height advances).
  • FFI surface updated: three new FFIWalletEventCallbacks each receiving an FFIBalance*; the block callback also receives an FFITransactionRecord array + count, and the IS-lock callback receives consensus-serialized InstantLock bytes.

Event routing

Entry point Event
Mempool arrival (new) MempoolTransactionReceived (record.context=Mempool)
Mempool arrival with IS lock (new) MempoolTransactionReceived (record.context=InstantSend(..))
IS lock on already-known tx TransactionInstantSendLocked
Block containing wallet txs BlockProcessChange (one per affected wallet, all records bundled)
Synced height advance with balance drift only BlockProcessChange (empty records)
Chain lock on already-confirmed tx / duplicate mempool / stale IS no event

Implementation notes

  • TransactionCheckResult now exposes affected_records: Vec<TransactionRecord> (populated for both new records and state-modifying confirmations / IS-locks on existing records).
  • check_transaction_in_all_wallets emits context-routed events; a new check_transaction_in_all_wallets_silent lets process_block batch per-wallet records into a single consolidated event.
  • Removed emit_balance_changes / standalone BalanceUpdated emission — balance is now embedded in every event.

Test plan

  • cargo test --workspace — 1925 passed, 0 failed, 47 ignored
  • cargo clippy -p key-wallet-manager -p dash-spv-ffi --tests --all-features — clean
  • All existing event-flow tests rewritten against the new variants (mempool→confirmed, mempool→IS→confirmed, IS dedup, mixed IS paths, chain-locked block first-seen, dry-run suppression, etc.)
  • dashd_sync integration tests — not run here; the FFI tracker was migrated to the new callback shape but assertions around transaction_received_count / balance_updated_count now count the new event surface (documented in comments)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • Refactoring
    • Redesigned wallet event system to provide richer transaction context and balance information. Events for mempool transactions, instant-send locks, and block processing now carry updated balance data inline, eliminating separate balance update notifications.

Replace `TransactionReceived` / `TransactionStatusChanged` / `BalanceUpdated`
with three self-contained variants — `MempoolTransactionReceived`,
`TransactionInstantSendLocked`, `BlockProcessChange` — each carrying the
transaction record(s) it pertains to plus the wallet's post-event balance.
Consumers can now persist transactions and balance atomically off a single
event.

- `BlockProcessChange` bundles all records updated by a block into a single
  per-wallet event; an empty `transactions_updated` signals a balance-only
  drift (e.g. coinbase maturing as synced height advanced).
- `TransactionCheckResult` now exposes `affected_records` (populated for
  both new records and state-modifying confirmations / IS-locks).
- `check_transaction_in_all_wallets` routes events by context; a silent
  variant lets `process_block` batch per-wallet records into one event.
- FFI surface updated (three new callbacks on `FFIWalletEventCallbacks`
  carrying `FFIBalance*` and record arrays); `ffi_cli` and dashd_sync
  tests migrated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

The pull request refactors the wallet event system to embed WalletCoreBalance and transaction details directly into events, replacing three separate callback/event types with three context-specific variants (MempoolTransactionReceived, TransactionInstantSendLocked, BlockProcessChange) across the FFI, wallet manager, and transaction checking layers.

Changes

Cohort / File(s) Summary
FFI Callback Refactoring
dash-spv-ffi/src/bin/ffi_cli.rs, dash-spv-ffi/src/callbacks.rs, dash-spv-ffi/tests/dashd_sync/callbacks.rs
Replaces three callbacks (on_transaction_received, on_transaction_status_changed, on_balance_updated) with three new ones (on_mempool_transaction_received, on_transaction_instant_send_locked, on_block_process_change). Changes parameter passing from explicit fields to FFIBalance pointers and transaction/islock record data. Updates test callback wiring and helper to persist balance atomics and track transaction counts per record.
Core Event System Redesign
key-wallet-manager/src/events.rs, key-wallet-manager/src/lib.rs
Restructures WalletEvent to embed WalletCoreBalance and transaction/block details into each variant. Removes TransactionReceived, TransactionStatusChanged, BalanceUpdated; adds MempoolTransactionReceived, TransactionInstantSendLocked, BlockProcessChange with boxed records/locks and post-change balance. Adds wallet_id() and balance() methods. Refactors check/emission logic with silent variants and context-routed event emission.
Event-Driven Logic Updates
key-wallet-manager/src/process_block.rs, key-wallet-manager/src/accessors.rs, key-wallet-manager/src/event_tests.rs, key-wallet-manager/src/test_helpers.rs
Updates block/mempool/instant-send processing to emit new event variants carrying full transaction records and balances. Removes emit_balance_changes helper. Reworks event assertions to validate embedded balances and transaction context via new variant constructors and per-context expectations.
Transaction Record Tracking
key-wallet/src/transaction_checking/account_checker.rs, key-wallet/src/transaction_checking/wallet_checker.rs
Replaces new_records (tuples with account index) with flat affected_records list, eliminating account pairing and consolidating newly recorded, confirmed, and InstantSend-locked records into a single field.
Architecture & Test Documentation
dash-spv/ARCHITECTURE.md, dash-spv/tests/dashd_sync/helpers.rs, dash-spv/tests/dashd_sync/tests_mempool.rs
Documents new event semantics (mempool/instant-send/block events carry balances inline; removes BalanceUpdated mechanism). Updates test helpers and assertion messages to match MempoolTransactionReceived naming and new event flow expectations.

Sequence Diagram

sequenceDiagram
    participant User as Transaction<br/>Lifecycle
    participant Manager as WalletManager
    participant Checker as TransactionChecker
    participant Events as Event Emitter
    
    User->>Manager: process_mempool_transaction(record)
    activate Manager
    Manager->>Checker: check_transaction_in_all_wallets_silent(record)
    activate Checker
    Checker->>Checker: affected_records = [record]
    Checker-->>Manager: CheckTransactionsResult { records_per_wallet }
    deactivate Checker
    
    Manager->>Events: emit_context_routed_events<br/>(Mempool context)
    activate Events
    Events-->>Manager: WalletEvent::MempoolTransactionReceived<br/>{ wallet_id, record, balance }
    deactivate Events
    deactivate Manager
    
    User->>Manager: process_instant_send_lock(islock)
    activate Manager
    Manager->>Checker: update existing record
    Checker->>Checker: affected_records = [updated_record]
    Manager->>Events: emit TransactionInstantSendLocked<br/>{ wallet_id, txid, instant_send_lock, balance }
    deactivate Manager
    
    User->>Manager: process_block(height, txs)
    activate Manager
    Manager->>Checker: check_transaction_in_all_wallets_silent(txs)
    Checker->>Checker: affected_records = [confirmed_records...]
    Manager->>Events: emit_context_routed_events<br/>(InBlock context)
    activate Events
    Events-->>Manager: WalletEvent::BlockProcessChange<br/>{ wallet_id, height, transactions_updated, balance }
    deactivate Events
    deactivate Manager
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • feat: surface mempool transactions in wallet UI dash-evo-tool#780: Requires updating the UI event handler (spawn_sync_event_handler) and wallet model wiring to accommodate the new WalletEvent variants (MempoolTransactionReceived, TransactionInstantSendLocked, BlockProcessChange) and their embedded balance/transaction payloads.

Possibly related PRs

Suggested labels

ready-for-review

Poem

🐰 Events now bloom with balance in tow,
Mempool, InstantSend, blocks all aglow—
No more scattered whispers, one voice clear,
Each transaction's journey, the balance laid near!
A hop, skip, and spike through the wallet's own year 🌱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor(wallet-events): atomic events carrying records + balance' clearly and concisely summarizes the main change: replacing multiple separate wallet events with unified, atomic events that carry both transaction records and balance information.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/priceless-mcclintock-f933d4

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 69.68326% with 67 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.18%. Comparing base (62d7af7) to head (aa99ca4).
⚠️ Report is 4 commits behind head on v0.42-dev.

Files with missing lines Patch % Lines
dash-spv-ffi/src/bin/ffi_cli.rs 0.00% 30 Missing ⚠️
key-wallet-manager/src/events.rs 0.00% 21 Missing ⚠️
dash-spv-ffi/src/callbacks.rs 71.42% 8 Missing ⚠️
key-wallet-manager/src/process_block.rs 71.42% 8 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           v0.42-dev     #680      +/-   ##
=============================================
- Coverage      70.40%   70.18%   -0.22%     
=============================================
  Files            247      319      +72     
  Lines          48029    66802   +18773     
=============================================
+ Hits           33816    46888   +13072     
- Misses         14213    19914    +5701     
Flag Coverage Δ
core 75.81% <ø> (-0.01%) ⬇️
ffi 45.02% <34.48%> (+1.69%) ⬆️
rpc 20.00% <ø> (ø)
spv 86.41% <ø> (+0.03%) ⬆️
wallet 68.85% <82.20%> (?)
Files with missing lines Coverage Δ
key-wallet-manager/src/accessors.rs 57.72% <ø> (ø)
key-wallet-manager/src/lib.rs 67.49% <100.00%> (ø)
key-wallet-manager/src/test_helpers.rs 100.00% <100.00%> (ø)
...wallet/src/transaction_checking/account_checker.rs 66.51% <100.00%> (ø)
...-wallet/src/transaction_checking/wallet_checker.rs 99.28% <ø> (ø)
dash-spv-ffi/src/callbacks.rs 73.70% <71.42%> (-6.91%) ⬇️
key-wallet-manager/src/process_block.rs 84.51% <71.42%> (ø)
key-wallet-manager/src/events.rs 0.00% <0.00%> (ø)
dash-spv-ffi/src/bin/ffi_cli.rs 0.00% <0.00%> (ø)

... and 93 files with indirect coverage changes

QuantumExplorer and others added 2 commits April 24, 2026 03:47
- Run cargo fmt across touched files.
- Replace `[check_transaction_in_all_wallets_silent]` intra-doc link with
  a plain backtick reference since the function is `pub(crate)` and can't
  be resolved from a `pub` doc under `-D rustdoc::broken-intra-doc-links`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
dash-spv/tests/dashd_sync/helpers.rs (1)

129-143: ⚠️ Potential issue | 🟠 Major

Accept IS-backed mempool arrivals here.

MempoolTransactionReceived now also covers transactions first seen with an InstantSend(..) context. Keeping the record.context == TransactionContext::Mempool guard means this helper will ignore a valid mempool event and eventually time out, which makes the dashd sync tests race-sensitive.

Minimal fix
-                    Ok(WalletEvent::MempoolTransactionReceived { ref record, .. }) if record.context == TransactionContext::Mempool => return Some(record.txid),
+                    Ok(WalletEvent::MempoolTransactionReceived { ref record, .. }) => return Some(record.txid),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dash-spv/tests/dashd_sync/helpers.rs` around lines 129 - 143, The helper
wait_for_mempool_tx currently only accepts
WalletEvent::MempoolTransactionReceived when record.context ==
TransactionContext::Mempool, which ignores valid mempool arrivals that are first
seen as InstantSend; update the match arm for
WalletEvent::MempoolTransactionReceived in wait_for_mempool_tx to also accept
InstantSend contexts (e.g., change the guard to allow record.context ==
TransactionContext::Mempool || matches!(record.context,
TransactionContext::InstantSend(_)) or pattern-match the InstantSend variant) so
the function returns Some(record.txid) for both mempool and InstantSend
arrivals.
key-wallet-manager/src/lib.rs (1)

502-583: ⚠️ Potential issue | 🟡 Minor

Emitted balance can be stale when the caller passes update_balance=false.

At line 562, the balance carried by the emitted event is whatever info.balance() returns after check_core_transaction(..., update_balance) — but when the caller invokes check_transaction_in_all_wallets(..., update_state_if_found=true, update_balance=false), state is modified while the cached balance is not refreshed (the internal process_mempool_transaction / process_block flows demonstrate this cached-balance behavior at process_block.rs lines 102-106). That means consumers of the emitted event would see the pre-state-change balance, breaking the "event carries post-event balance" invariant.

Either document this precondition on check_transaction_in_all_wallets (e.g. "pass update_balance=true to ensure emitted events carry the post-change balance") or internally force a refresh before reading balance when emit_events && state_modified.

🔧 Option A: always refresh before reading balance in the emit path
                     if emit_events && check_result.state_modified {
-                        // Re-read balance after state change
-                        let balance = self
-                            .wallet_infos
-                            .get(&wallet_id)
-                            .map(|info| info.balance())
-                            .unwrap_or_default();
+                        // Re-read balance after state change — force a refresh
+                        // so the event always carries the post-event balance
+                        // regardless of the caller's `update_balance` flag.
+                        let balance = self
+                            .wallet_infos
+                            .get_mut(&wallet_id)
+                            .map(|info| {
+                                info.update_balance();
+                                info.balance()
+                            })
+                            .unwrap_or_default();
                         self.emit_context_routed_events(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet-manager/src/lib.rs` around lines 502 - 583, The emitted event can
contain a stale balance when check_transaction_in_all_wallets_inner calls
emit_context_routed_events after check_core_transaction was run with
update_balance=false; fix by ensuring the cached WalletInfo balance is refreshed
before reading it for the event: inside check_transaction_in_all_wallets_inner
(the branch where emit_events && check_result.state_modified) call a WalletInfo
refresh method to recompute the balance from the updated state (e.g. implement
and call a WalletInfo::refresh_balance_from_state or similar) or otherwise
force-update the cache for wallet_infos.get(&wallet_id) prior to calling
.balance(), then pass that fresh balance into emit_context_routed_events
(alternatively document the function to require update_balance=true when
emit_events is requested if you prefer not to add a refresh).
🧹 Nitpick comments (2)
key-wallet-manager/src/lib.rs (1)

594-649: LGTM — context routing mirrors the documented state machine exactly.

The four documented routes (mempool new → MempoolTransactionReceived; IS-lock new → MempoolTransactionReceived with IS context; IS-lock existing → TransactionInstantSendLocked; in-block → BlockProcessChange) map 1:1 to the match arms here, and the InBlock | InChainLockedBlock arm correctly carries a single BlockProcessChange for direct callers while process_block.rs uses the silent batched path for the bundled-per-wallet case.

One subtle observation worth leaving in a code comment: the TransactionInstantSendLocked arm consumes (moves) records but doesn't use them — intentional since the event variant doesn't carry records, but a one-line note would prevent future confusion.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet-manager/src/lib.rs` around lines 594 - 649, The
TransactionInstantSendLocked match arm in emit_context_routed_events
intentionally consumes the parameter records but does not use it; add a one-line
code comment inside that arm clarifying that records is moved (consumed)
intentionally because the TransactionInstantSendLocked event does not carry
per-record data, to prevent future confusion (reference: fn
emit_context_routed_events, param records, match arm creating
WalletEvent::TransactionInstantSendLocked).
key-wallet-manager/src/process_block.rs (1)

77-137: Consolidate balance refresh into the silent checker by setting update_balance: true.

The manual refresh loop at lines 102–106 is redundant. The check_core_transaction method (when called with update_balance=true) updates balance for each affected wallet inside its own control flow, scoped to wallets where is_relevant=true. Passing update_balance=true to line 93 would eliminate the need for lines 102–106, consolidating the balance invariant into a single code path.

♻️ Suggested simplification
-        // Refresh cached balances only for affected wallets *before* emitting
-        // so the event carries the post-update balance.
         let check_result =
-            self.check_transaction_in_all_wallets_silent(tx, context.clone(), true, false).await;
+            self.check_transaction_in_all_wallets_silent(tx, context.clone(), true, true).await;

         let is_relevant = !check_result.affected_wallets.is_empty();
         let net_amount = if is_relevant {
             check_result.total_received as i64 - check_result.total_sent as i64
         } else {
             0
         };

-        for wallet_id in &check_result.affected_wallets {
-            if let Some(info) = self.wallet_infos.get_mut(wallet_id) {
-                info.update_balance();
-            }
-        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet-manager/src/process_block.rs` around lines 77 - 137, The manual
per-wallet balance refresh loop in process_mempool_transaction is redundant—pass
update_balance=true into check_transaction_in_all_wallets_silent (ensure the
call to check_transaction_in_all_wallets_silent(tx, context.clone(), true,
false) has the update flag set) so balances are refreshed inside the checker,
then remove the loop that iterates check_result.affected_wallets calling
self.wallet_infos.get_mut(...).update_balance(); keep the subsequent event
emission that reads balances from self.wallet_infos (e.g.,
emit_context_routed_events) unchanged so they observe the refreshed balances.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@key-wallet-manager/src/test_helpers.rs`:
- Around line 94-129: The InstantSend branch currently trusts whatever
InstantLock the test caller provided, allowing default/mismatched locks to pass;
update the InstantSend handling in the test helper
(TransactionContext::InstantSend) to verify the lock belongs to the transaction
by either asserting the emitted WalletEvent::TransactionInstantSendLocked
contains an InstantLock with txid equal to the transaction's txid() or ensure
the submitted context uses dummy_instant_lock(tx.txid()) so the helper can match
on that txid; specifically add a check against tx.txid() when matching
TransactionInstantSendLocked (and when matching MempoolTransactionReceived for
the InstantSend case if the event carries the lock) to guarantee the lock
payload is tied to the correct transaction.

In `@key-wallet/src/transaction_checking/account_checker.rs`:
- Around line 49-55: The emitted affected_records field loses account identity
and per-account insert/update status; replace Vec<TransactionRecord> with a
typed per-account payload (e.g., define a new AffectedRecord struct containing
account_index (or AccountId), account_type, record: TransactionRecord, and
is_new_transaction) and change the field in the AccountChecker result from
affected_records: Vec<TransactionRecord> to affected_records:
Vec<AffectedRecord>; update the code paths that build affected_records inside
account_checker.rs to populate account_index/account_type and per-record
is_new_transaction, and update all consumers of affected_records to read the new
struct fields instead of assuming a single global is_new_transaction or account
context.

---

Outside diff comments:
In `@dash-spv/tests/dashd_sync/helpers.rs`:
- Around line 129-143: The helper wait_for_mempool_tx currently only accepts
WalletEvent::MempoolTransactionReceived when record.context ==
TransactionContext::Mempool, which ignores valid mempool arrivals that are first
seen as InstantSend; update the match arm for
WalletEvent::MempoolTransactionReceived in wait_for_mempool_tx to also accept
InstantSend contexts (e.g., change the guard to allow record.context ==
TransactionContext::Mempool || matches!(record.context,
TransactionContext::InstantSend(_)) or pattern-match the InstantSend variant) so
the function returns Some(record.txid) for both mempool and InstantSend
arrivals.

In `@key-wallet-manager/src/lib.rs`:
- Around line 502-583: The emitted event can contain a stale balance when
check_transaction_in_all_wallets_inner calls emit_context_routed_events after
check_core_transaction was run with update_balance=false; fix by ensuring the
cached WalletInfo balance is refreshed before reading it for the event: inside
check_transaction_in_all_wallets_inner (the branch where emit_events &&
check_result.state_modified) call a WalletInfo refresh method to recompute the
balance from the updated state (e.g. implement and call a
WalletInfo::refresh_balance_from_state or similar) or otherwise force-update the
cache for wallet_infos.get(&wallet_id) prior to calling .balance(), then pass
that fresh balance into emit_context_routed_events (alternatively document the
function to require update_balance=true when emit_events is requested if you
prefer not to add a refresh).

---

Nitpick comments:
In `@key-wallet-manager/src/lib.rs`:
- Around line 594-649: The TransactionInstantSendLocked match arm in
emit_context_routed_events intentionally consumes the parameter records but does
not use it; add a one-line code comment inside that arm clarifying that records
is moved (consumed) intentionally because the TransactionInstantSendLocked event
does not carry per-record data, to prevent future confusion (reference: fn
emit_context_routed_events, param records, match arm creating
WalletEvent::TransactionInstantSendLocked).

In `@key-wallet-manager/src/process_block.rs`:
- Around line 77-137: The manual per-wallet balance refresh loop in
process_mempool_transaction is redundant—pass update_balance=true into
check_transaction_in_all_wallets_silent (ensure the call to
check_transaction_in_all_wallets_silent(tx, context.clone(), true, false) has
the update flag set) so balances are refreshed inside the checker, then remove
the loop that iterates check_result.affected_wallets calling
self.wallet_infos.get_mut(...).update_balance(); keep the subsequent event
emission that reads balances from self.wallet_infos (e.g.,
emit_context_routed_events) unchanged so they observe the refreshed balances.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 42647109-3014-4d88-99eb-8ec2a9e50564

📥 Commits

Reviewing files that changed from the base of the PR and between 62d7af7 and aa99ca4.

📒 Files selected for processing (14)
  • dash-spv-ffi/src/bin/ffi_cli.rs
  • dash-spv-ffi/src/callbacks.rs
  • dash-spv-ffi/tests/dashd_sync/callbacks.rs
  • dash-spv/ARCHITECTURE.md
  • dash-spv/tests/dashd_sync/helpers.rs
  • dash-spv/tests/dashd_sync/tests_mempool.rs
  • key-wallet-manager/src/accessors.rs
  • key-wallet-manager/src/event_tests.rs
  • key-wallet-manager/src/events.rs
  • key-wallet-manager/src/lib.rs
  • key-wallet-manager/src/process_block.rs
  • key-wallet-manager/src/test_helpers.rs
  • key-wallet/src/transaction_checking/account_checker.rs
  • key-wallet/src/transaction_checking/wallet_checker.rs
💤 Files with no reviewable changes (1)
  • key-wallet-manager/src/accessors.rs

Comment on lines +94 to +129
match ctx {
TransactionContext::Mempool => {
assert!(
matches!(&event, WalletEvent::MempoolTransactionReceived { wallet_id: wid, record, .. } if *wid == wallet_id && record.context == *ctx),
"context[{}]: expected MempoolTransactionReceived(Mempool), got {:?}",
i,
event
);
}
TransactionContext::InstantSend(_) => {
if i == 0 {
assert!(
matches!(&event, WalletEvent::MempoolTransactionReceived { wallet_id: wid, record, .. } if *wid == wallet_id && record.context == *ctx),
"context[{}]: expected MempoolTransactionReceived(InstantSend), got {:?}",
i,
event
);
} else {
assert!(
matches!(&event, WalletEvent::TransactionInstantSendLocked { wallet_id: wid, .. } if *wid == wallet_id),
"context[{}]: expected TransactionInstantSendLocked, got {:?}",
i,
event
);
}
}
TransactionContext::InBlock(info) | TransactionContext::InChainLockedBlock(info) => {
let expected_height = info.height();
assert!(
matches!(&event, WalletEvent::BlockProcessChange { wallet_id: wid, height, transactions_updated, .. } if *wid == wallet_id && *height == expected_height && transactions_updated.len() == 1),
"context[{}]: expected BlockProcessChange(height={}, 1 tx), got {:?}",
i,
expected_height,
event
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Assert that the InstantLock actually belongs to this tx.

This helper currently treats whatever InstantSend(lock) the caller passed as the expected value. Since several call sites use InstantLock::default(), these lifecycle tests never prove that the stored/emitted lock is tied to tx.txid(). Add an explicit txid check here, or build the submitted context with dummy_instant_lock(tx.txid()), so a mismatched lock payload can’t slip through.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet-manager/src/test_helpers.rs` around lines 94 - 129, The
InstantSend branch currently trusts whatever InstantLock the test caller
provided, allowing default/mismatched locks to pass; update the InstantSend
handling in the test helper (TransactionContext::InstantSend) to verify the lock
belongs to the transaction by either asserting the emitted
WalletEvent::TransactionInstantSendLocked contains an InstantLock with txid
equal to the transaction's txid() or ensure the submitted context uses
dummy_instant_lock(tx.txid()) so the helper can match on that txid; specifically
add a check against tx.txid() when matching TransactionInstantSendLocked (and
when matching MempoolTransactionReceived for the InstantSend case if the event
carries the lock) to guarantee the lock payload is tied to the correct
transaction.

Comment on lines +49 to +55
/// Transaction records recorded or updated by this check.
///
/// Contains one entry per affected account. Entries are present when the
/// transaction was newly recorded, confirmed in a block, or InstantSend-locked
/// on top of an existing record. Use `is_new_transaction` to know whether
/// these records are new or updates to existing ones.
pub affected_records: Vec<TransactionRecord>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Keep account metadata with each emitted record.

TransactionRecord is account-scoped (net_amount, input/output details) but it does not carry account_index or account type. Flattening this to Vec<TransactionRecord> means a wallet event that touches multiple accounts can no longer be mapped back to the correct account, and the single is_new_transaction flag also can’t describe mixed insert/update cases. That makes the new atomic event payload ambiguous for downstream persistence.

Possible shape
+pub struct AffectedRecord {
+    pub account_type_match: CoreAccountTypeMatch,
+    pub record: TransactionRecord,
+    pub is_new: bool,
+}
+
-    pub affected_records: Vec<TransactionRecord>,
+    pub affected_records: Vec<AffectedRecord>,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet/src/transaction_checking/account_checker.rs` around lines 49 - 55,
The emitted affected_records field loses account identity and per-account
insert/update status; replace Vec<TransactionRecord> with a typed per-account
payload (e.g., define a new AffectedRecord struct containing account_index (or
AccountId), account_type, record: TransactionRecord, and is_new_transaction) and
change the field in the AccountChecker result from affected_records:
Vec<TransactionRecord> to affected_records: Vec<AffectedRecord>; update the code
paths that build affected_records inside account_checker.rs to populate
account_index/account_type and per-record is_new_transaction, and update all
consumers of affected_records to read the new struct fields instead of assuming
a single global is_new_transaction or account context.

@github-actions github-actions Bot added the merge-conflict The PR conflicts with the target branch. label Apr 24, 2026
@github-actions
Copy link
Copy Markdown
Contributor

This PR has merge conflicts with the base branch. Please rebase or merge the base branch into your branch to resolve them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-conflict The PR conflicts with the target branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants