Skip to content

feat(key-wallet): add keep-finalized-transactions Cargo feature#733

Merged
QuantumExplorer merged 4 commits into
v0.42-devfrom
claude/keep-finalized-transactions
May 7, 2026
Merged

feat(key-wallet): add keep-finalized-transactions Cargo feature#733
QuantumExplorer merged 4 commits into
v0.42-devfrom
claude/keep-finalized-transactions

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented May 5, 2026

Summary

Opt-in keep-finalized-transactions Cargo feature that drops the full TransactionRecord for transactions that have reached a finalized state (InstantSend lock or ChainLock — both signal the transaction can never change state again) and keeps only the txid in a per-account finalized_txids: HashSet<Txid>. Bounds memory growth in long-lived wallets without losing dedup.

Public API

Added to ManagedAccountTrait (always available, both feature configs):

  • fn has_transaction(&self, txid: &Txid) -> bool — true if the tx is either still in transactions (active) or has been dropped into finalized_txids (finalized). Used as the dedup signal in confirm_transaction.
  • fn transaction_is_finalized(&self, txid: &Txid) -> bool — true only when the txid is in finalized_txids. Always false with the feature off (no finalization tracking).

ManagedAccountTransactionsTrait::transactions() / transactions_mut() remain as before. With the feature on the map only contains non-finalized records — finalized ones are gone, so iterating the map will not see them.

confirm_transaction now returns Option<TransactionRecord> instead of bool. The record is cloned BEFORE any finalization so callers can still emit events even when the record has just been dropped from the map.

TransactionContext::is_finalized() returns true for InstantSend(_) or InChainLockedBlock(_).

Behavior

  • record_transaction(.., context, ..) — if context.is_finalized(), the record is inserted then immediately dropped, with the txid added to finalized_txids. Returns the cloned record.
  • confirm_transaction(.., context, ..) — short-circuits when the txid is already finalized (no record left to update). Otherwise updates the existing context, captures the record, then finalizes if the new context is finalized.
  • mark_instant_send_utxos (wallet-level) — also calls finalize_transaction so IS-lock arrivals through that path drop the record consistently.

Defaults

  • Cargo feature is OFF by default.
  • Dev-dep on key-wallet enables it, so cargo test -p key-wallet runs the feature-on path. Existing tests that explicitly asserted record content after IS-lock/chainlock are gated to #[cfg(not(feature = \"keep-finalized-transactions\"))].
  • key-wallet-manager and key-wallet-ffi expose a keep-finalized-transactions feature that forwards to key-wallet.

Test plan

  • cargo build --workspace (default — feature off) builds
  • cargo build --workspace --features key-wallet-ffi/keep-finalized-transactions builds
  • cargo test -p key-wallet --lib --test-threads=1 — 463 passed (feature on via dev-dep, includes 4 new feature tests)
  • cargo test -p key-wallet --no-default-features --features test-utils,serde,bincode,getrandom,bls,eddsa,bip38 --lib --test-threads=1 — 463 passed (feature off; gates the new tests, exercises legacy ones)
  • cargo test -p key-wallet-manager --all-features — 31/7/5/2/0 ✅
  • cargo clippy --workspace --tests clean
  • cargo fmt --check clean

New focused tests in key-wallet/src/tests/keep_finalized_transactions_tests.rs:

  • instantsend_drops_record_and_records_finalization — mempool → IS-lock drops the record; has_transaction still true; transaction_is_finalized true.
  • chainlock_drops_record_and_records_finalization — InBlock alone is NOT finalization; chainlock drops the record.
  • first_sighting_already_finalized_drops_record_immediately — record + finalize in a single call when the first event is IS-lock.
  • replays_after_finalize_dont_double_count_or_resurrect_record — stale block events for already-finalized txs don't resurrect the record or change UTXOs/balance.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added keep-finalized-transactions feature flag to enable full transaction history retention in wallets (by default, finalized transactions are pruned).
  • Documentation

    • Updated FFI API documentation to clarify that transaction history functions are only available with the feature enabled.
  • Tests

    • Added comprehensive tests validating transaction record retention behavior with and without the feature.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Review Change Stack

Warning

Rate limit exceeded

@QuantumExplorer has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 53 minutes and 45 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f89d7c3c-6a86-442f-a14c-65ec16565004

📥 Commits

Reviewing files that changed from the base of the PR and between eb5afa2 and 13ed52c.

📒 Files selected for processing (1)
  • key-wallet/Cargo.toml
📝 Walkthrough

Walkthrough

This PR introduces a keep-finalized-transactions Cargo feature flag that controls wallet transaction record retention when transactions reach chain-locked finality. When enabled, full transaction records persist in memory; when disabled (default), chainlocked records are dropped and only their txids retained for deduplication.

Changes

Keep-Finalized-Transactions Feature and Implementation

Layer / File(s) Summary
Feature Definitions
key-wallet/Cargo.toml, key-wallet-manager/Cargo.toml, key-wallet-ffi/Cargo.toml, dash-spv-ffi/Cargo.toml
Feature flag cascades from base library through manager and FFI layers; dev-dependency enables end-to-end testing.
Trait and Type Contracts
key-wallet/src/managed_account/managed_account_trait.rs, key-wallet/src/transaction_checking/transaction_context.rs
ManagedAccountTrait adds has_transaction and transaction_is_finalized methods; TransactionContext adds is_chain_locked detector.
Core Funds Account
key-wallet/src/managed_account/managed_core_funds_account.rs
confirm_transaction returns Option<TransactionRecord> instead of bool; record_transaction gates finalized pruning on chainlock context; trait implementations delegate presence and finality checks.
Keys Account State Management
key-wallet/src/managed_account/managed_core_keys_account.rs
Conditionally adds finalized_txids: HashSet and drop_finalized_transaction helper when pruning; trait implementations check both records and txid set depending on feature state.
Transaction Checking Integration
key-wallet/src/transaction_checking/wallet_checker.rs
Uses new trait methods (has_transaction, transaction_is_finalized) instead of direct map access; refactored finality detection and updated confirmation return handling to Option.
FFI Public API Gating
key-wallet-ffi/src/managed_account.rs, key-wallet-ffi/FFI_API.md
Transaction history query functions (get_transaction_count, get_transactions, free_transactions) feature-gated with conditional test compilation and documentation.
Tests and Validation
key-wallet/src/tests/keep_finalized_transactions_tests.rs, key-wallet/src/tests/mod.rs
Four comprehensive tests validate feature-ON record persistence, feature-OFF record pruning, IS-lock vs chainlock finality semantics, and txid-based query fallback.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Poem

🐰 The wallet now recalls or forgets,
Depending on what we let it set—
ChainLocks bring finality true,
Txids remember what we lose, too! ✨

🚥 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 PR title accurately and concisely describes the main feature addition: introducing a new Cargo feature flag called 'keep-finalized-transactions' to the key-wallet crate.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/keep-finalized-transactions

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 May 5, 2026

Codecov Report

❌ Patch coverage is 93.61702% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.26%. Comparing base (68dff90) to head (13ed52c).

Files with missing lines Patch % Lines
...-wallet/src/transaction_checking/wallet_checker.rs 81.81% 2 Missing ⚠️
.../src/managed_account/managed_core_funds_account.rs 95.23% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           v0.42-dev     #733      +/-   ##
=============================================
- Coverage      71.43%   71.26%   -0.18%     
=============================================
  Files            320      320              
  Lines          68770    68798      +28     
=============================================
- Hits           49128    49028     -100     
- Misses         19642    19770     +128     
Flag Coverage Δ
core 76.51% <ø> (ø)
ffi 44.70% <100.00%> (-1.51%) ⬇️
rpc 20.00% <ø> (ø)
spv 87.61% <ø> (+0.07%) ⬆️
wallet 70.10% <93.02%> (+0.02%) ⬆️
Files with missing lines Coverage Δ
key-wallet-ffi/src/managed_account.rs 52.13% <100.00%> (ø)
...allet/src/managed_account/managed_account_trait.rs 37.24% <ø> (ø)
...t/src/managed_account/managed_core_keys_account.rs 58.97% <100.00%> (+4.68%) ⬆️
...et/src/transaction_checking/transaction_context.rs 64.10% <100.00%> (+2.99%) ⬆️
.../src/managed_account/managed_core_funds_account.rs 76.74% <95.23%> (+0.60%) ⬆️
...-wallet/src/transaction_checking/wallet_checker.rs 99.21% <81.81%> (-0.18%) ⬇️

... and 19 files with indirect coverage changes

Copy link
Copy Markdown
Member Author

@QuantumExplorer QuantumExplorer left a comment

Choose a reason for hiding this comment

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

Self Reviewed

@QuantumExplorer
Copy link
Copy Markdown
Member Author

This replaces #709

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 6, 2026

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

@github-actions github-actions Bot added the merge-conflict The PR conflicts with the target branch. label May 6, 2026
Copy link
Copy Markdown
Collaborator

@xdustinface xdustinface left a comment

Choose a reason for hiding this comment

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

This will lead to missing out on block confirmations for transactions. We should not see transactions as "finalized" or however you wanna call it until they are confirmed fully confirmed via a chainlock (either at the height they are mined or a later height if the confiming block didnt receive a chainlock) i think.

// path so e.g. an IS-locked record can transition to ChainLock
// when the surrounding block confirmation arrives.
#[cfg(not(feature = "keep-finalized-transactions"))]
if self.keys.transaction_is_finalized(&txid) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So you will never know when transaction gets confirmed in chain now?

// the `#[allow(unused_variables)]` keeps clippy happy when the
// `keep-finalized-transactions` feature is on (no `let _ = ...`
// cfg shim required).
#[allow(unused_variables)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why not use the feature gate here too?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ping!

// the `#[allow(unused_variables)]` keeps clippy happy when the
// `keep-finalized-transactions` feature is on (no `let _ = ...`
// cfg shim required).
#[allow(unused_variables)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ping!

{
return self.finalized_txids.contains(txid);
}
#[allow(unreachable_code)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This seems a bit strange.. should use the feature gate properly?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ping!

@QuantumExplorer QuantumExplorer force-pushed the claude/keep-finalized-transactions branch from afe76d9 to a7fca52 Compare May 6, 2026 09:40
@github-actions github-actions Bot removed the merge-conflict The PR conflicts with the target branch. label May 6, 2026
Copy link
Copy Markdown
Collaborator

@xdustinface xdustinface left a comment

Choose a reason for hiding this comment

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

Just a side note, we can still get this PR still in for now i guess but at the moment this will not work really because chainlocks are not propagated/processed in the wallet at this point. There is some work required first to wire them up and some thoughts need to be put in how we deal with historical transactions and if we want to emit events for all of them on after initial sync after the first received chainlock.

Also left few more comments, unused code should be dropped, some renaming we should do i think and the proper feature usage probably makes sense to do too.

/// Use [`Self::transaction_is_finalized_in_block`] for the stricter
/// "fully confirmed in a chainlocked block" answer that drives
/// memory-pruning decisions.
fn transaction_is_finalized(&self, txid: &Txid) -> bool;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is unused.

/// height / block hash before the chainlock catches up). Only
/// `InChainLockedBlock` qualifies. This is the trigger for dropping
/// the full record under the default feature configuration.
fn transaction_is_finalized_in_block(&self, txid: &Txid) -> bool;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
fn transaction_is_finalized_in_block(&self, txid: &Txid) -> bool;
fn transaction_is_finalized(&self, txid: &Txid) -> bool;

This should probably be just called transaction_is_finalized then now when the original one gets dropped to align with the naming of the feature.

/// Returns `true` if this account has already processed `txid`,
/// whether it's still mutable in `transactions` or has been
/// finalized-and-pruned (under the default feature configuration).
/// Used as the dedup signal in `confirm_transaction`.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
/// Used as the dedup signal in `confirm_transaction`.

This seems off? How does it dedup there?

// the `#[allow(unused_variables)]` keeps clippy happy when the
// `keep-finalized-transactions` feature is on (no `let _ = ...`
// cfg shim required).
#[allow(unused_variables)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ping!

// the `#[allow(unused_variables)]` keeps clippy happy when the
// `keep-finalized-transactions` feature is on (no `let _ = ...`
// cfg shim required).
#[allow(unused_variables)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ping!

{
return self.finalized_txids.contains(txid);
}
#[allow(unreachable_code)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Ping!

{
return self.finalized_txids.contains(txid);
}
#[allow(unreachable_code)]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Proper use of the feature gate?

/// ChainLock as final. Use [`Self::is_finalized_in_block`] for the
/// stricter "fully confirmed in a chainlocked block" answer that
/// drives memory-pruning decisions.
pub fn is_finalized(&self) -> bool {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is unused, should be dropped.

/// block confirmation may still arrive and write the height /
/// block hash before the chainlock catches up). Only
/// `InChainLockedBlock` qualifies.
pub fn is_finalized_in_block(&self) -> bool {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
pub fn is_finalized_in_block(&self) -> bool {
pub fn is_chain_locked(&self) -> bool {

We have is_instant_send above so i think we should go with ^ here.

QuantumExplorer and others added 3 commits May 7, 2026 16:14
By default, records of chainlocked transactions are now dropped from
each managed account's in-memory `transactions` map; only their txids
are retained (in a new `finalized_txids` set on `ManagedCoreKeysAccount`)
to keep dedup, `has_transaction`, and finality queries working. The
opt-in `keep-finalized-transactions` Cargo feature reverts to the
old behavior — every processed transaction stays in the map for the
wallet's lifetime.

The drop is driven off `TransactionContext::is_finalized_in_block`
(chainlock only), not `is_finalized` (chainlock or IS-lock), so
IS-locked records survive long enough to absorb the surrounding
block-confirmation event (xdustinface review on #709).

`confirm_transaction` now returns `Option<TransactionRecord>` so
callers always observe the record even when it's about to be dropped.

The feature is forwarded through `key-wallet-manager` and
`key-wallet-ffi`. Three FFI accessors that walk the full
`transactions` map (`managed_core_account_get_transaction_count`,
`managed_core_account_get_transactions`,
`managed_core_account_free_transactions`) are gated to the feature
because they would otherwise return a partial history.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Integration tests under `dash-spv-ffi/tests/dashd_sync/` walk the full
per-account transaction history (`managed_core_account_get_transactions`,
`managed_core_account_get_transaction_count`,
`managed_core_account_free_transactions`) to verify end-to-end wallet
sync against a regtest dashd. These accessors are gated behind the
`keep-finalized-transactions` feature on `key-wallet-ffi`, so under the
default feature set they aren't compiled in and the test build fails to
resolve the imports.

Add `key-wallet-ffi` as a dev-dependency with the feature enabled.
Cargo unifies features across the build graph at test time, which
makes the gated accessors available in the test binaries without
expanding the lib crate's default feature surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Address xdustinface review feedback on PR #733:

- Drop `TransactionContext::is_finalized()` (the soft IS-or-chainlock
  check). The wallet now treats only a chainlock as finality.
- Rename `TransactionContext::is_finalized_in_block()` to
  `is_chain_locked()` so the predicate name describes the variant
  rather than overloading "finalized" — same naming style as the
  existing `is_instant_send()` helper.
- Drop `ManagedAccountTrait::transaction_is_finalized()` (the
  unused soft-finality variant) and rename
  `transaction_is_finalized_in_block()` to `transaction_is_finalized()`.
  Now that there's a single finalization concept, the trait method
  name aligns with the feature name.
- Replace runtime `if cfg!(...) { ... } else { ... }` branches in
  `ManagedCoreKeysAccount::has_transaction` and
  `transaction_is_finalized` with two `#[cfg]`-gated function bodies
  — one per feature configuration. Same for the chainlock-driven
  drop call in `confirm_transaction` / `record_transaction`: the
  `let drop_now = …` binding now only exists when the feature is off,
  removing the `#[allow(unused_variables)]` workaround.
- Rewrite the doc on `has_transaction` so it's clear what it actually
  reports (live record OR finalized-txid marker) and how callers use
  it (distinguish brand-new sightings from re-processings) instead of
  the misleading "dedup signal in `confirm_transaction`" wording.

Drop logic still triggers on the strict `is_chain_locked()` check —
IS-locked-but-not-yet-chainlocked records still survive so the
surrounding block-confirmation event can populate height / block
hash before the chainlock catches up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer force-pushed the claude/keep-finalized-transactions branch from 2f0f2e9 to eb5afa2 Compare May 7, 2026 09:22
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: 1

🧹 Nitpick comments (1)
key-wallet/src/tests/keep_finalized_transactions_tests.rs (1)

1-155: ⚡ Quick win

Align this new test module with the repository’s required test-module layout.

Please move/merge these cases into one of the prescribed key-wallet/src/tests modules (e.g., transaction-focused module) instead of introducing a new top-level test filename.

As per coding guidelines, key-wallet/src/tests/**/*.rs: "Organize unit tests by functionality into separate test modules: account_tests.rs, address_pool_tests.rs, transaction_tests.rs, wallet_tests.rs, integration_tests.rs, balance_tests.rs, spent_outpoints_tests.rs, special_transaction_tests.rs, advanced_transaction_tests.rs, managed_account_collection_tests.rs, backup_restore_tests.rs, edge_case_tests.rs, performance_tests.rs".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@key-wallet/src/tests/keep_finalized_transactions_tests.rs` around lines 1 -
155, You created a new top-level test file with several transaction-focused
tests (test_chainlocked_record_kept_when_feature_on,
test_chainlocked_record_dropped_when_feature_off,
test_islocked_record_kept_when_feature_off,
test_islocked_then_chainlocked_drops_at_chainlock) which violates the repo
test-module layout; move/merge these tests into the existing transaction test
module (e.g., key-wallet/src/tests/transaction_tests.rs). To fix: remove this
new file and copy each test function (preserving their cfg attributes and tokio
signatures) into transaction_tests.rs, add or reconcile required imports
(ManagedAccountTrait, TestWalletContext, BlockInfo, TransactionContext,
InstantLock where cfg(not(feature = "keep-finalized-transactions"))), and run
cargo test to ensure no duplicate symbols or missing imports; adjust any
module-level docs to match surrounding tests and keep the tests grouped by
functionality in transaction_tests.rs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@key-wallet/Cargo.toml`:
- Around line 27-28: Update the feature documentation comment that references
the old method name `transaction_is_finalized_in_block` to the current API
method `transaction_is_finalized`; specifically, replace the stale symbol in the
comment so it matches the implemented API name (e.g., update any occurrence of
`transaction_is_finalized_in_block` to `transaction_is_finalized` near the
feature docs mentioning `has_transaction` / `transaction_is_finalized`).

---

Nitpick comments:
In `@key-wallet/src/tests/keep_finalized_transactions_tests.rs`:
- Around line 1-155: You created a new top-level test file with several
transaction-focused tests (test_chainlocked_record_kept_when_feature_on,
test_chainlocked_record_dropped_when_feature_off,
test_islocked_record_kept_when_feature_off,
test_islocked_then_chainlocked_drops_at_chainlock) which violates the repo
test-module layout; move/merge these tests into the existing transaction test
module (e.g., key-wallet/src/tests/transaction_tests.rs). To fix: remove this
new file and copy each test function (preserving their cfg attributes and tokio
signatures) into transaction_tests.rs, add or reconcile required imports
(ManagedAccountTrait, TestWalletContext, BlockInfo, TransactionContext,
InstantLock where cfg(not(feature = "keep-finalized-transactions"))), and run
cargo test to ensure no duplicate symbols or missing imports; adjust any
module-level docs to match surrounding tests and keep the tests grouped by
functionality in transaction_tests.rs.
🪄 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: 3b9d0bdb-919d-4d45-b9ad-6fd64a818a6a

📥 Commits

Reviewing files that changed from the base of the PR and between 68dff90 and eb5afa2.

📒 Files selected for processing (13)
  • dash-spv-ffi/Cargo.toml
  • key-wallet-ffi/Cargo.toml
  • key-wallet-ffi/FFI_API.md
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-manager/Cargo.toml
  • key-wallet/Cargo.toml
  • key-wallet/src/managed_account/managed_account_trait.rs
  • key-wallet/src/managed_account/managed_core_funds_account.rs
  • key-wallet/src/managed_account/managed_core_keys_account.rs
  • key-wallet/src/tests/keep_finalized_transactions_tests.rs
  • key-wallet/src/tests/mod.rs
  • key-wallet/src/transaction_checking/transaction_context.rs
  • key-wallet/src/transaction_checking/wallet_checker.rs

Comment thread key-wallet/Cargo.toml Outdated
The previous commit renamed the trait method to `transaction_is_finalized`
but missed this Cargo.toml feature-doc reference. Caught by CodeRabbit on
PR #733.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer merged commit 8dd5f1b into v0.42-dev May 7, 2026
37 checks passed
@QuantumExplorer QuantumExplorer deleted the claude/keep-finalized-transactions branch May 7, 2026 10:04
QuantumExplorer added a commit that referenced this pull request May 7, 2026
…#742)

* refactor(key-wallet): wire ManagedCoreKeysAccount into the collection

Promotes the dead-code `ManagedCoreKeysAccount` introduced in #711 into the
active type for accounts that derive special-purpose keys but don't track
funds. Cuts ~10 keys-only accounts per wallet from carrying always-empty
`balance` / `utxos` / `spent_outpoints` state.

Storage split on `ManagedAccountCollection`:
- Identity, asset-lock, and provider fields move from
  `ManagedCoreFundsAccount` → `ManagedCoreKeysAccount`.
- Standard, CoinJoin, and DashPay fields stay on `ManagedCoreFundsAccount`.

New borrowed enum `ManagedAccountRef<'a> { Funds, Keys }` (and mutable
counterpart `ManagedAccountRefMut<'a>`) plus owned `OwnedManagedCoreAccount`.
Spanning collection accessors (`get_by_account_type_match`, `all_accounts`,
…) return the borrowed enum. The enum delegates the funds-agnostic surface
(transactions, monitor revision, address-pool helpers, record/confirm
transaction) so most callers don't need to dispatch on the variant; funds-
only operations (`as_funds()`, `as_funds_mut()`) require an explicit unwrap.

`ManagedCoreKeysAccount` gains the methods needed to be a first-class
participant in transaction matching:
- `record_transaction` / `confirm_transaction` (no UTXO/balance work)
- `check_transaction_for_match`, `check_asset_lock_transaction_for_match`,
  and the four `check_provider_*_key_in_transaction_for_match` variants
- `classify_address` / `check_provider_payout` helpers

`wallet/managed_wallet_info` accessors that returned `&mut
ManagedCoreFundsAccount` for identity / asset-lock / provider accounts now
return `&mut ManagedCoreKeysAccount`. Funds-only surfaces
(`account_balances`, `update_balance`, `mark_instant_send_utxos`,
matured-coinbase / immature) filter to the funds variant.

`ManagedAccountCollection::insert` accepts either variant via
`OwnedManagedCoreAccount`; explicit `insert_funds` / `insert_keys` helpers
are exposed for callers that statically know the variant. `get`, `get_mut`,
`remove`, `contains_key` now scope to the funds-bearing index lookup
(Standard BIP44/32 + CoinJoin) — keys accounts use type-keyed accessors.

C ABI preserved on the FFI side. `FFIManagedCoreAccount` now wraps an
internal `Funds | Keys` enum carrying an `Arc<…>`, with new `as_funds` /
`as_keys` / `keys_account` accessors and a `new_keys` constructor. Funds-
only entry points (`get_balance`, `get_utxo_count`) gracefully return
defaults on the keys variant; trait-shared entry points
(`get_network`, `get_account_type`, address-pool getters, transactions)
work on both. Variant-aware constructors are wired through
`managed_wallet_get_account`, `managed_wallet_get_top_up_account_with_registration_index`,
and the per-type collection getters.

This is a clean replay of the work from PR #716 on the current
architecture (the original branch carried merge noise from unrelated
discarded branches and depended on pre-#711 / pre-#733 struct layouts that
no longer exist).

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

* fix(key-wallet-ffi): rustdoc + regenerate FFI_API.md

- Drop intra-doc links to private `FFIManagedCoreAccountInner` variants on
  `keys_account()` so `cargo doc -D warnings` (and the Documentation CI job)
  passes; the enum is internal and doesn't need to appear in public rustdoc.
- Regenerate `key-wallet-ffi/FFI_API.md` to pick up the updated descriptions
  on `managed_core_account_get_balance` / `_get_utxo_count` (the verify-ffi
  pre-commit hook).

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

* fix(key-wallet-ffi): include all account variants in managed_wallet_get_account_count

The count was only summing standard / coinjoin / identity_registration /
identity_topup buckets — every other variant `managed_wallet_get_account`
can return (identity_topup_not_bound, identity_invitation, asset-lock,
provider, dashpay, platform-payment) was excluded. Pre-existing miscount,
made more visible by the keys-account split now exposing those getters
through the variant-aware FFI surface.

`provider_operator_keys` (BLS) and `provider_platform_keys` (EdDSA) are
feature-gated on the immutable `AccountCollection` and counted only when
the corresponding feature is enabled.

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

* refactor(key-wallet): add `all_funding_accounts(_mut)` and drop in-loop `as_funds()` filters

Five funds-only callsites in `wallet_info_interface.rs` were iterating
`all_accounts()` and filtering each ref via `as_funds()` / `as_funds_mut()`
inside the loop body — pure noise, since balance / UTXO state only ever
lives on Standard / CoinJoin / DashPay accounts.

Add focused accessors on `ManagedAccountCollection`:

- `all_funding_accounts(&self) -> Vec<&ManagedCoreFundsAccount>`
- `all_funding_accounts_mut(&mut self) -> Vec<&mut ManagedCoreFundsAccount>`

Migrate `account_balances`, `utxos`, `update_balance`, `immature_transactions`,
and `matured_coinbase_records` to use them. `monitored_addresses`,
`transaction_history`, `monitor_revision`, and `mark_instant_send_utxos`
keep using `all_accounts(_mut)` — they legitimately span both variants.

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

* refactor(key-wallet): rename insert_{funds,keys} -> insert_{funds,keys}_bearing_account

`insert_funds` / `insert_keys` were ambiguous about *what* "funds" or
"keys" referred to. The longer names make the variant intent explicit
and match the surrounding "funds-bearing" / "keys-only" terminology
already used in the doc comments and field comments.

The generic `insert(impl Into<OwnedManagedCoreAccount>)` keeps its name —
that's the variant-agnostic entry point.

No public callers existed outside this file (verified via grep across
key-wallet, key-wallet-ffi, key-wallet-manager, dash-spv) so this is a
local rename with no downstream impact.

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

* refactor(key-wallet): simplify keys-account `record_transaction` to its real semantics

The keys-account `record_transaction` was carrying three branches that
are statically unreachable on the keys-account data model:

- `change_addrs` — for non-Standard `CoreAccountTypeMatch` variants,
  `involved_change_addresses()` is always `&[]`. Identity / asset-lock /
  provider accounts own a single address pool with no external/internal
  split, so the `OutputRole::Change` arm can never match.
- `OutputRole::Sent` — gated on `has_inputs = account_match.sent > 0`,
  but the keys-account `check_transaction_for_match` sets `sent = 0`
  unconditionally (keys accounts don't track per-account UTXOs, so
  there's no signal that they contributed inputs).
- `direction = Internal | Outgoing` — both gated on `has_inputs`,
  same dead-branch reason. A keys account always sees direction
  `Incoming` (or `CoinJoin` for the unlikely-but-possible case where
  the upstream classifier flagged a CoinJoin tx that touched a
  keys-account address).

Drop the dead branches, rename `receive_addrs` → `owned_addrs` (no
receive/change distinction exists for these account types), and document
the actual semantics on the method's doc comment. Behavior is preserved
— the previous code happened to compute the right answer through dead
arms; the new code expresses it directly.

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

* refactor(key-wallet): make keys-account `record_transaction` a thin marker

Keys-account flows (identity registration, asset lock, provider-key
registration / update) are typically funded from a Standard or CoinJoin
account in the same wallet. The funding account's `record_transaction`
already populates `input_details` (from its UTXO set) and
`output_details` (classified receive / change / sent), so the
keys-account record reproducing them would double-count and risk
drifting out of sync if the classification logic changes.

Treat the keys-account record as the thin marker it really is: "this
tx involved this keys account" plus `net_amount`, and nothing more.

- `direction = TransactionDirection::Internal` — from the wallet's
  perspective, the tx moves funds from one of its accounts to another.
- `input_details = Vec::new()` — already correct (keys accounts don't
  track per-account UTXOs).
- `output_details = Vec::new()` — the funding account's record carries
  the per-output classification.

Drop the now-unused `OutputDetail` / `OutputRole` / `Address` imports
and gate the `HashSet` import behind the same feature flag as
`finalized_txids`.

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

* test(key-wallet): cover special-tx → keys-account matching paths end-to-end

For each special-transaction type that drives one of the new keys-account
`check_*_for_match` methods on `ManagedCoreKeysAccount`, construct a
transaction and assert `ManagedWalletInfo::check_core_transaction` flags
the right `AccountTypeToCheck`. 13 tests in
`tests/special_transaction_matching_tests.rs`:

- AssetLockPayloadType → 6 tests covering each of the keys-account
  variants (Identity{Registration, TopUp, TopUpNotBound, Invitation},
  AssetLock{Address, Shielded}TopUp).
- ProviderRegistrationPayloadType → 4 tests, one per provider-key match
  field (owner_key_hash, voting_key_hash, operator_public_key (BLS),
  platform_node_id (EdDSA)). The BLS / EdDSA tests are feature-gated
  the same way the matching impls are.
- ProviderUpdateRegistrarPayloadType → 2 tests (voting + operator key
  changes).
- 1 test pinning the keys-account `TransactionRecord` shape: direction
  Internal, empty input/output details (the post-PR thin-marker
  contract).

Skipped on purpose:

- AssetUnlock / Coinbase — match Standard, not the keys-only variants.
- QuorumCommitment — no key/address fields the wallet matches against.
- ProviderUpdateRevocation — payload has no key-hash / pubkey field
  for matching.

Also fixes a real PR-introduced bug surfaced by writing the IdentityTopUp
test: `add_managed_account` / `add_managed_account_from_xpub` always
constructed `ManagedCoreFundsAccount` regardless of the account type, then
relied on `insert()` accepting it — which now correctly errors for
keys-only types after the variant split. The fix dispatches via a small
`owned_from_account` helper that picks the right variant, and the BLS /
EdDSA paths (always ProviderOperatorKeys / ProviderPlatformKeys, both
keys-only) drop their `ManagedCoreFundsAccount::from_*_account` calls in
favor of `ManagedCoreKeysAccount::from_*_account`.

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

* refactor(key-wallet): keep `from_account_collection` field-direct insertions

The earlier refactor in this PR routed every `from_account_collection`
case through `managed_collection.insert(managed_account)` where
`managed_account` was an `OwnedManagedCoreAccount`. That swapped 14
trivial field-typed inserts (e.g. `standard_bip44_accounts.insert(*index,
managed)`) for 14 generic-insert calls — each having to carry a
`.expect()` for an invariant the old code never had to articulate.

Restore the field-direct shape:

- `create_managed_account_from_account_type` (returned the owned enum) is
  renamed to `build_managed_account_type` and returns the inner
  [`ManagedAccountType`]. That's the actual shared work — building the
  address pools.
- Per-variant thin wrappers wrap it as the right concrete type:
  - `create_managed_funds_account_from_account` → `ManagedCoreFundsAccount`
  - `create_managed_keys_account_from_account` → `ManagedCoreKeysAccount`
  - `create_managed_keys_account_from_bls_account` (ProviderOperatorKeys, always keys)
  - `create_managed_keys_account_from_eddsa_account` (ProviderPlatformKeys, always keys)
- `from_account_collection` does the field-direct insert each variant
  expects — same shape as the original code.
- `is_funds_bearing_account_type` was only used by the old funnel and
  is now removed.

The variant-aware `insert(impl Into<OwnedManagedCoreAccount>)` and the
explicit `insert_funds_bearing_account` / `insert_keys_bearing_account`
remain as the public entry points for callers that genuinely need the
runtime-dispatched insert (e.g. `add_managed_account`).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants