Skip to content

fix(key-wallet): track self-send change in confirmed balance - #707

Merged
QuantumExplorer merged 3 commits into
v0.42-devfrom
claude/self-send-change-confirmed
May 2, 2026
Merged

fix(key-wallet): track self-send change in confirmed balance#707
QuantumExplorer merged 3 commits into
v0.42-devfrom
claude/self-send-change-confirmed

Conversation

@QuantumExplorer

@QuantumExplorer QuantumExplorer commented May 1, 2026

Copy link
Copy Markdown
Member

Summary

When a mempool transaction spends one of our own UTXOs and pays change back to an internal address, the resulting change UTXO was being placed in the unconfirmed balance bucket. From a user's perspective the confirmed balance appeared to drop by the entire input value as soon as the spend was broadcast — even though the change is just previously-tracked funds returning.

This PR teaches ManagedCoreAccount::update_utxos to recognize that situation and mark the change UTXO as confirmed even while the parent transaction is still in the mempool. The condition is:

  • the output address is in the account's internal (change) pool, and
  • this account owns at least one input being spent (account_match.sent > 0)

The transaction record itself still reports TransactionContext::Mempool and is_confirmed() == false; only the change UTXO's confirmation flag (and therefore the balance bucket it lands in) is adjusted.

External / receive outputs are unchanged. Incoming mempool payments where we do not own an input keep the prior unconfirmed treatment.

Test plan

  • cargo test -p key-wallet --lib (461 passing, including 2 new tests)
    • test_self_send_change_in_mempool_lands_in_confirmed_balance exercises the new path: a confirmed funding UTXO is spent in mempool with change to our internal pool; the change UTXO is is_confirmed = true and the wallet balance reports confirmed = change_amount, unconfirmed = 0.
    • test_external_mempool_payment_remains_unconfirmed is the regression guard: a fresh mempool payment we receive (no owned input) stays in the unconfirmed bucket.
  • cargo test -p key-wallet-manager --lib
  • cargo test -p dash-spv --lib
  • cargo clippy -p key-wallet --all-features --all-targets -- -D warnings
  • cargo fmt --check -p key-wallet

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced mempool transaction handling for more accurate balance reporting. Self-sent transactions that spend wallet inputs and return change to wallet addresses now have their change UTXOs counted as confirmed balance immediately, rather than waiting for block confirmation. External payments remain unconfirmed until they appear on-chain.
  • Tests

    • Added comprehensive tests for mempool transaction balance scenarios.

When a mempool transaction spends one of our own UTXOs and pays change
back to one of our internal addresses, the change UTXO is just our
previously-tracked funds returning. Marking it `is_confirmed = false`
sent the entire change amount into the unconfirmed bucket, making the
user's confirmed balance appear to drop by the full input value while
the change waited in the mempool.

`ManagedCoreAccount::update_utxos` now flags such change outputs as
confirmed regardless of context, using `account_match.sent > 0` as the
"we own an input" signal and `involved_change_addresses` for the
internal-pool match. Outputs to receive (external) addresses, and
mempool payments where we don't own any inputs, keep the existing
unconfirmed treatment.

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

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces a is_trusted boolean field to mark mempool UTXOs created from self-spend transactions (spending owned inputs and paying to change addresses) so they contribute to confirmed balance. The field is computed in update_utxos, used in update_balance, and is covered by comprehensive tests.

Changes

Trusted Mempool UTXO Marking

Layer / File(s) Summary
Data Shape
key-wallet/src/utxo.rs
Utxo struct gains new pub is_trusted: bool field; Utxo::new() initializes it to false.
Core Logic
key-wallet/src/managed_account/mod.rs
update_utxos derives change_addrs from the account and detects has_owned_input (spent funds), then marks outputs as trusted when they are change from self-spends. update_balance now treats is_trusted UTXOs as confirmed alongside block-confirmed and InstantSend-locked ones.
Test Fixtures
key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs
Existing Utxo test fixture updated to explicitly set is_trusted: false.
Tests
key-wallet/src/transaction_checking/wallet_checker.rs
Two new async tests verify behavior: one confirms change from self-spend mempool transactions is marked trusted and credited to confirmed balance; the other confirms external mempool payments remain unconfirmed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

A self-spender hops through the chain,
Change returned—no need to wait in vain!
Trusted mempool marks the path,
Balance counts them now, not after—
Our wallet learns to trust itself again! 🐰✨

🚥 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 clearly and concisely summarizes the main change: tracking self-send change UTXOs in the confirmed balance. It directly reflects the core functionality introduced in this PR.
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/self-send-change-confirmed

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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

@codecov

codecov Bot commented May 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.59%. Comparing base (e079930) to head (c9321e8).
⚠️ Report is 2 commits behind head on v0.42-dev.

Additional details and impacted files
@@              Coverage Diff              @@
##           v0.42-dev     #707      +/-   ##
=============================================
- Coverage      70.59%   70.59%   -0.01%     
=============================================
  Files            320      320              
  Lines          68028    68243     +215     
=============================================
+ Hits           48026    48177     +151     
- Misses         20002    20066      +64     
Flag Coverage Δ
core 75.81% <ø> (ø)
ffi 43.37% <ø> (+0.04%) ⬆️
rpc 20.00% <ø> (ø)
spv 87.31% <ø> (-0.06%) ⬇️
wallet 69.63% <100.00%> (+0.08%) ⬆️
Files with missing lines Coverage Δ
key-wallet/src/managed_account/mod.rs 56.08% <100.00%> (+1.02%) ⬆️
...-wallet/src/transaction_checking/wallet_checker.rs 99.39% <100.00%> (+0.04%) ⬆️
key-wallet/src/utxo.rs 91.78% <100.00%> (+0.11%) ⬆️
...c/wallet/managed_wallet_info/asset_lock_builder.rs 79.61% <100.00%> (+0.04%) ⬆️

... and 11 files with indirect coverage changes

Replace the overloaded `is_confirmed = context.confirmed() || is_self_send_change`
written in the previous commit with a dedicated `Utxo::is_change` flag.
`is_confirmed` now means strictly "the parent transaction is in a block (or
chain-locked block)"; the new `is_change` flag captures "this output is the
change of a transaction we created (we own at least one input and the
output pays one of our internal addresses)".

`update_balance` credits a UTXO to the `confirmed` bucket when any of
`is_confirmed`, `is_instantlocked`, or `is_change` is set, preserving the
behavior introduced for self-sends in the mempool while keeping the two
properties orthogonal in the data model.

The new field deserializes to `false` (`#[serde(default)]`) so existing
serialized wallet state stays compatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ZocoLini
ZocoLini previously approved these changes May 1, 2026

@xdustinface xdustinface left a comment

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.

It feels a little bit wrong to make it look confirmed even though it isn't but i guess sits alright.

There is just one think we should maybe consider: Calling it is_trusted instead of is_change makes maybe more sense as we then can also let actual self sends which are no change fall into this category?

Comment thread key-wallet/src/utxo.rs Outdated
Comment thread key-wallet/src/utxo.rs Outdated
Address review feedback: the new flag mirrors Bitcoin Core's
`CWalletTx::IsTrusted()` concept, and the name `is_trusted` reads
better than `is_change` for the role it plays in `update_balance`.
The predicate is unchanged (output is to one of our internal addresses
on a transaction that also spends one of our own UTXOs); only the
field, locals, comments, doc references, and tests are renamed.

Also drop `#[cfg_attr(feature = "serde", serde(default))]` on the
field for consistency with the other `Utxo` flags — none of which carry
that attribute.

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

@coderabbitai coderabbitai Bot left a comment

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.

Caution

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

⚠️ Outside diff range comments (1)
key-wallet/src/utxo.rs (1)

17-18: ⚠️ Potential issue | 🟠 Major

Preserve backward compatibility for persisted Utxos.

With derived Deserialize, older wallet snapshots that predate is_trusted will fail to deserialize when the codebase is upgraded. The BTreeMap<OutPoint, Utxo> in ManagedCoreAccount will be unable to parse snapshots lacking this field. Add serde(default) to default the new field to false during deserialization, matching the constructor behavior.

Suggested fix
 pub struct Utxo {
     pub is_locked: bool,
-    pub is_trusted: bool,
+    #[cfg_attr(feature = "serde", serde(default))]
+    pub is_trusted: bool,
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@key-wallet/src/utxo.rs` around lines 17 - 18, The new Utxo field is_trusted
will break deserialization of old wallet snapshots; update the Utxo definition
so deserializing missing is_trusted defaults to false by annotating the field
with serde default (e.g., add #[serde(default)] on the is_trusted field in the
Utxo struct), ensuring the existing derived Deserialize stays in place and
ManagedCoreAccount's BTreeMap<OutPoint, Utxo> can load older snapshots without
errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@key-wallet/src/utxo.rs`:
- Around line 17-18: The new Utxo field is_trusted will break deserialization of
old wallet snapshots; update the Utxo definition so deserializing missing
is_trusted defaults to false by annotating the field with serde default (e.g.,
add #[serde(default)] on the is_trusted field in the Utxo struct), ensuring the
existing derived Deserialize stays in place and ManagedCoreAccount's
BTreeMap<OutPoint, Utxo> can load older snapshots without errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0a0c993e-cb27-4a61-9aac-9076e0b96fa5

📥 Commits

Reviewing files that changed from the base of the PR and between e079930 and c9321e8.

📒 Files selected for processing (4)
  • key-wallet/src/managed_account/mod.rs
  • key-wallet/src/transaction_checking/wallet_checker.rs
  • key-wallet/src/utxo.rs
  • key-wallet/src/wallet/managed_wallet_info/asset_lock_builder.rs

@github-actions github-actions Bot added the ready-for-review CodeRabbit has approved this PR label May 2, 2026
@QuantumExplorer

Copy link
Copy Markdown
Member Author

Going to merge this in.

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

Labels

ready-for-review CodeRabbit has approved this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants