Skip to content

feat(key-wallet): support for more account types + strategy to consume all utxo available#823

Open
ZocoLini wants to merge 1 commit into
devfrom
feat/tx-builder-update
Open

feat(key-wallet): support for more account types + strategy to consume all utxo available#823
ZocoLini wants to merge 1 commit into
devfrom
feat/tx-builder-update

Conversation

@ZocoLini

@ZocoLini ZocoLini commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Generalize tx building by funding account + add All (drain) selection

  • ManagedWalletInfo::build_and_sign_transaction[_with_signer] now take a funding source (AccountTypePreference + index) and a SelectionStrategy, instead of being hardcoded to BIP44 and the default strategy.
  • New SelectionStrategy::All: drains the account, spends every spendable UTXO into a single output worth total. Under All the change address is dropped before sizing, so the fee isn't inflated by a phantom change output.
  • CoinJoin accounts are spend-only at the API level (next_receive/change_address are unimplemented! I can implemente it or return an error but I consider unimplemented a better approach since the idea is to avoid using it, it is not a library erro).
  • dashd integration test: drain a BIP32 account into a BIP44 account end-to-end.

Known limitation: there is currently no way to build a transaction with more than 500 inputs (MAX_STANDARD_TX_INPUTS) through the set_funding method because set_funding pulls all of the account's UTXOs at once. To sweep an account holding >500 UTXOs you must build the transaction manually, taking the account's UTXOs and feeding them in ≤500-input chunks. This is worth calling out because the builder can be used to sweep a CoinJoin account with All, where exceeding 500 inputs is quite likely

Summary by CodeRabbit

  • New Features

    • Added support for draining all spendable funds from one wallet account into another in a single transaction.
    • Expanded transaction building to let users choose the source account type and coin-selection method.
  • Bug Fixes

    • Improved handling for missing accounts and oversized transactions with clearer failures.
    • Prevented incorrect change output calculations when sweeping all funds.
  • Tests

    • Added coverage for full-account drain behavior and balance updates after confirmation.

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds account-preference-based managed wallet transaction building, a new all-UTXO selection strategy, builder limits and errors, updated manager/FFI wiring, and tests including a drain transaction from one account into another.

Changes

Managed wallet drain flow

Layer / File(s) Summary
Account routing and accessors
key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs, key-wallet/src/wallet/managed_wallet_info/mod.rs, key-wallet/src/wallet/managed_wallet_info/transaction_building.rs
WalletInfoInterface adds account lookup/balance helpers, CoinJoin address matches are handled explicitly, and managed-wallet transaction building resolves source/source_index through AccountTypePreference.
All selection
key-wallet/src/wallet/managed_wallet_info/coin_selection.rs
SelectionStrategy::All is added to select every spendable UTXO, compute the post-fee deliverable, and return no change.
Unsigned assembly and limits
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs, key-wallet/src/wallet/managed_wallet_info/transaction_building.rs
TransactionBuilder suppresses change sizing for SelectionStrategy::All, caps selected inputs, assigns the single output amount, and adds BuilderError variants for missing accounts and too many inputs.
Manager and FFI forwarding
key-wallet-manager/src/lib.rs, key-wallet-ffi/src/transaction.rs
The transaction-building API forwards SelectionStrategy and account preference through the manager and FFI entry point.
Transaction tests and drain flow
key-wallet/src/wallet/managed_wallet_info/transaction_building.rs, dash-spv/tests/dashd_sync/tests_transaction.rs
The builder tests and dash-spv integration test use the new signatures, account selection, and drain transaction path.

Sequence Diagram(s)

sequenceDiagram
  participant Test as dash-spv test_drain_account_into_another
  participant Manager as WalletManager<ManagedWalletInfo>
  participant ManagedWalletInfo
  participant CoinSelector
  participant TransactionBuilder
  participant Dashd as dashd regtest

  Test->>Manager: build_and_sign_transaction(..., SelectionStrategy::All)
  Manager->>ManagedWalletInfo: resolve source account and build transaction
  ManagedWalletInfo->>CoinSelector: select_coins_with_size(All, ...)
  ManagedWalletInfo->>TransactionBuilder: assemble_unsigned(...)
  TransactionBuilder-->>ManagedWalletInfo: unsigned transaction
  Test->>Dashd: broadcast, mine, and resync
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

ready-for-review

Suggested reviewers

  • xdustinface
  • llbartekll

Poem

🐰 I hopped through coins and swept them neat,
From BIP32 burrow to BIP44 seat.
One output glowed, no change to hide,
With a twitch of whiskers, the fee rode by.

🚥 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 is clear and matches the main change: generalized account types plus a new strategy to spend all available UTXOs.
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 feat/tx-builder-update

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.

@ZocoLini ZocoLini force-pushed the feat/tx-builder-update branch from bd92c00 to 748cf33 Compare June 25, 2026 10:28
@ZocoLini ZocoLini requested a review from xdustinface June 25, 2026 10:33
@codecov

codecov Bot commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 63.52941% with 62 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.84%. Comparing base (d9fe0a3) to head (9649b93).
⚠️ Report is 4 commits behind head on dev.

Files with missing lines Patch % Lines
...wallet/managed_wallet_info/transaction_building.rs 69.62% 24 Missing ⚠️
...allet/managed_wallet_info/wallet_info_interface.rs 0.00% 18 Missing ⚠️
.../wallet/managed_wallet_info/transaction_builder.rs 47.61% 11 Missing ⚠️
key-wallet-manager/src/lib.rs 0.00% 4 Missing ⚠️
key-wallet-ffi/src/transaction.rs 0.00% 3 Missing ⚠️
key-wallet/src/wallet/managed_wallet_info/mod.rs 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #823      +/-   ##
==========================================
- Coverage   73.04%   72.84%   -0.20%     
==========================================
  Files         323      323              
  Lines       72046    72187     +141     
==========================================
- Hits        52623    52583      -40     
- Misses      19423    19604     +181     
Flag Coverage Δ
core 76.74% <ø> (ø)
ffi 45.87% <0.00%> (-1.48%) ⬇️
rpc 20.00% <ø> (ø)
spv 90.34% <ø> (+0.03%) ⬆️
wallet 71.79% <64.67%> (-0.04%) ⬇️
Files with missing lines Coverage Δ
...t/src/wallet/managed_wallet_info/coin_selection.rs 82.85% <100.00%> (+1.81%) ⬆️
key-wallet/src/wallet/managed_wallet_info/mod.rs 67.52% <0.00%> (-1.18%) ⬇️
key-wallet-ffi/src/transaction.rs 0.00% <0.00%> (ø)
key-wallet-manager/src/lib.rs 75.26% <0.00%> (-0.54%) ⬇️
.../wallet/managed_wallet_info/transaction_builder.rs 83.47% <47.61%> (-1.43%) ⬇️
...allet/managed_wallet_info/wallet_info_interface.rs 73.85% <0.00%> (-6.65%) ⬇️
...wallet/managed_wallet_info/transaction_building.rs 86.94% <69.62%> (-1.47%) ⬇️

... and 17 files with indirect coverage changes

@ZocoLini ZocoLini force-pushed the feat/tx-builder-update branch from 748cf33 to 9649b93 Compare June 25, 2026 10:52
@ZocoLini ZocoLini marked this pull request as ready for review June 25, 2026 10:52

@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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs (2)

83-86: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Skip change-address derivation for drain transactions.

set_funding derives a change address before assemble_unsigned clears it for SelectionStrategy::All, so a sweep can still advance/mark change-address state even though no change output is emitted.

As per coding guidelines, key-wallet/**/*.rs: "Use gap limit and staged address generation instead of unbounded address derivation in Rust address pool implementations."

Proposed fix
     pub fn set_funding(mut self, funds_acc: &mut ManagedCoreFundsAccount, acc: &Account) -> Self {
         self.inputs = funds_acc.utxos.values().cloned().collect();
-        self.change_addr = funds_acc.next_change_address(Some(&acc.account_xpub), true).ok();
+        self.change_addr = if self.selection_strategy == SelectionStrategy::All {
+            None
+        } else {
+            funds_acc.next_change_address(Some(&acc.account_xpub), true).ok()
+        };
         self
     }

Also applies to: 256-260

🤖 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/wallet/managed_wallet_info/transaction_builder.rs` around
lines 83 - 86, `TransactionBuilder::set_funding` is deriving and consuming a
change address even for drain/sweep flows, which can advance
`ManagedCoreFundsAccount` state unnecessarily. Update the funding setup so
change-address derivation is skipped when the transaction will use
`SelectionStrategy::All` (or equivalent drain mode), and keep
`assemble_unsigned` as the place that clears/omits change handling for those
cases. Use the existing `set_funding` and `assemble_unsigned` flow, plus
`ManagedCoreFundsAccount::next_change_address`, to ensure no change address is
generated or marked for sweeps.

Source: Coding guidelines


294-319: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not validate drains against the caller’s placeholder output amount.

For SelectionStrategy::All, line 319 overwrites the only output with total_input - fee, but line 294 can still reject the transaction if the caller supplied a non-zero placeholder amount larger than the account balance.

Proposed fix
-        if total_input < total_output + selection.estimated_fee {
+        let spend_amount_for_checks =
+            if self.selection_strategy == SelectionStrategy::All { 0 } else { total_output };
+
+        if total_input < spend_amount_for_checks + selection.estimated_fee {
             return Err(BuilderError::InsufficientFunds {
                 available: total_input,
-                required: total_output + selection.estimated_fee,
+                required: spend_amount_for_checks + selection.estimated_fee,
             });
         }
 
         let change_amount =
-            total_input.saturating_sub(total_output).saturating_sub(selection.estimated_fee);
+            total_input.saturating_sub(spend_amount_for_checks).saturating_sub(selection.estimated_fee);
🤖 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/wallet/managed_wallet_info/transaction_builder.rs` around
lines 294 - 319, The drain path in `TransactionBuilder` is still being checked
against the caller’s placeholder output amount before `SelectionStrategy::All`
overwrites it. Adjust the insufficient-funds validation in the builder logic so
the `All` strategy skips validating against `total_output` and instead only
ensures the balance covers the fee, then let the existing
`tx_outputs`/single-output rewrite set the final amount.
key-wallet-ffi/src/transaction.rs (1)

88-99: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Expose the new source account and selection strategy through FFI.

This FFI entrypoint still hardcodes BIP44 + BranchAndBound, so FFI callers cannot build BIP32/CoinJoin-funded transactions or use the new drain flow.

Consider adding FFI-safe enums/parameters, or a dedicated drain entrypoint if this legacy API must keep its current ABI.

Also applies to: 140-144

🤖 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-ffi/src/transaction.rs` around lines 88 - 99, The FFI entrypoint
wallet_build_and_sign_transaction still hardcodes the source account derivation
and coin selection behavior, so callers cannot choose BIP32/CoinJoin funding or
the new drain flow. Update the FFI surface around
wallet_build_and_sign_transaction (and the related drain path referenced by the
same comment) to accept FFI-safe parameters or enums for the source account and
selection strategy, or add a separate drain-specific entrypoint if the legacy
ABI must remain unchanged. Ensure the Rust-side transaction builder uses those
incoming values instead of always forcing BIP44 and BranchAndBound.
🤖 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/src/wallet/managed_wallet_info/mod.rs`:
- Around line 177-179: The CoinJoin match arm in `managed_wallet_info::mod.rs`
currently panics via `unimplemented!`, but these public library methods should
not crash callers; update the `AccountTypePreference::CoinJoin` handling in both
affected match arms to return `None` just like the other “no address available”
cases. Use the same fix in the corresponding address lookup methods so
`CoinJoin` remains supported without panicking.

---

Outside diff comments:
In `@key-wallet-ffi/src/transaction.rs`:
- Around line 88-99: The FFI entrypoint wallet_build_and_sign_transaction still
hardcodes the source account derivation and coin selection behavior, so callers
cannot choose BIP32/CoinJoin funding or the new drain flow. Update the FFI
surface around wallet_build_and_sign_transaction (and the related drain path
referenced by the same comment) to accept FFI-safe parameters or enums for the
source account and selection strategy, or add a separate drain-specific
entrypoint if the legacy ABI must remain unchanged. Ensure the Rust-side
transaction builder uses those incoming values instead of always forcing BIP44
and BranchAndBound.

In `@key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs`:
- Around line 83-86: `TransactionBuilder::set_funding` is deriving and consuming
a change address even for drain/sweep flows, which can advance
`ManagedCoreFundsAccount` state unnecessarily. Update the funding setup so
change-address derivation is skipped when the transaction will use
`SelectionStrategy::All` (or equivalent drain mode), and keep
`assemble_unsigned` as the place that clears/omits change handling for those
cases. Use the existing `set_funding` and `assemble_unsigned` flow, plus
`ManagedCoreFundsAccount::next_change_address`, to ensure no change address is
generated or marked for sweeps.
- Around line 294-319: The drain path in `TransactionBuilder` is still being
checked against the caller’s placeholder output amount before
`SelectionStrategy::All` overwrites it. Adjust the insufficient-funds validation
in the builder logic so the `All` strategy skips validating against
`total_output` and instead only ensures the balance covers the fee, then let the
existing `tx_outputs`/single-output rewrite set the final amount.
🪄 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: ff46c80b-d76f-4aa0-955c-10843dc294ec

📥 Commits

Reviewing files that changed from the base of the PR and between d9fe0a3 and 9649b93.

📒 Files selected for processing (8)
  • dash-spv/tests/dashd_sync/tests_transaction.rs
  • key-wallet-ffi/src/transaction.rs
  • key-wallet-manager/src/lib.rs
  • key-wallet/src/wallet/managed_wallet_info/coin_selection.rs
  • key-wallet/src/wallet/managed_wallet_info/mod.rs
  • key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
  • key-wallet/src/wallet/managed_wallet_info/transaction_building.rs
  • key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs

Comment on lines +177 to +179
AccountTypePreference::CoinJoin => {
unimplemented!("CoinJoin accounts are spend-only in our current use cases")
}

@coderabbitai coderabbitai Bot Jun 25, 2026

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Return None instead of panicking for CoinJoin address requests.

CoinJoin is a valid AccountTypePreference, so these public library methods can now crash callers instead of reporting “no address available.”

As per coding guidelines, **/*.rs: "Never panic in library code; only use panic! for unrecoverable errors in Rust."

Proposed fix
             AccountTypePreference::CoinJoin => {
-                unimplemented!("CoinJoin accounts are spend-only in our current use cases")
+                None
             }

Apply the same change in both match arms.

Also applies to: 223-225

🤖 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/wallet/managed_wallet_info/mod.rs` around lines 177 - 179, The
CoinJoin match arm in `managed_wallet_info::mod.rs` currently panics via
`unimplemented!`, but these public library methods should not crash callers;
update the `AccountTypePreference::CoinJoin` handling in both affected match
arms to return `None` just like the other “no address available” cases. Use the
same fix in the corresponding address lookup methods so `CoinJoin` remains
supported without panicking.

Source: Coding guidelines

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@xdustinface What would you do here?? Returning None doesn't explain that this functionality isn't supported and returning an error indicates an error in the library but it is not an error, so I wouldn't like to return one either

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.

@ZocoLini You’re right — None is a lossy fix here.

My concern is mainly the unimplemented!, not that None is the only acceptable replacement. If I were designing this API, I’d prefer:

Result<Option<Address>, AddressRequestError>

with something explicit like:

enum AddressRequestError {
    UnsupportedAccountType(AccountTypePreference),
}

That way:

  • Ok(Some(addr)) = got an address
  • Ok(None) = no address currently available
  • Err(UnsupportedAccountType(AccountTypePreference::CoinJoin)) = caller asked for an operation this account type does not support

So this would be an unsupported-operation error, not “the library broke”.

If you want to keep this PR scoped and avoid a signature change, then returning None is still safer than panicking, but I agree it’s not as expressive. In that case, I’d at least document that CoinJoin is spend-only and maybe follow up later with a typed/fallible API.

(•ᴗ•)


🧠 Learnings used
Learnt from: ZocoLini
Repo: dashpay/rust-dashcore PR: 782
File: key-wallet/src/wallet/mod.rs:129-131
Timestamp: 2026-05-22T17:37:15.824Z
Learning: In this repository, avoid adding unit tests for trivial methods such as simple getters, setters, or constructors (e.g., methods that only return a field, assign a value, or initialize state with no additional logic). During code review, do not flag or suggest test coverage solely for these trivial implementations; reserve unit tests for behavior with meaningful logic or side effects.

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.

Hm i think just returning some "not supported" error would be fine? Or just return a struct or tuple which contains some success flag or so? I guess it's kind of okay to panic but also kind of not. I dont have a strong opinion here to be honest but at the end the error feels just a less intense.

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.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@github-actions github-actions Bot added the merge-conflict The PR conflicts with the target branch. label Jun 26, 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.

@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.

Looks good. But i wonder, how will users with > 500 utxos be able to drain them? Maybe we should cap the All strategy at 500 and call it Consolidate or something instead?

Im fine with it either way i guess but its worth a thought. Apparently it's less likely to be relevant for CoinJoin at least.

Image

Comment on lines +177 to +179
AccountTypePreference::CoinJoin => {
unimplemented!("CoinJoin accounts are spend-only in our current use cases")
}

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.

Hm i think just returning some "not supported" error would be fine? Or just return a struct or tuple which contains some success flag or so? I guess it's kind of okay to panic but also kind of not. I dont have a strong opinion here to be honest but at the end the error feels just a less intense.

@ZocoLini

Copy link
Copy Markdown
Collaborator Author

I like All, it better describes what the CoinSelector selects.

About what to do with accounts with +500 UTXOs, we had a discussion about it in the standup, the options I like the most is:

  • Expose a build method in the builder to split the UTXO into multiple Transactions. This would be easy to do hard to allow user to customize how UTXO are splitted
  • The other option is to expose TransactionBuilder though FFI for non Rust consumers and encourage Rust consumer to use the builder so they can build the transactions the way they want

Then, talking about the unimplemented!, I don't want to return an error honestly, what about debug_assert(false, "explanation") and the returning None so only debug builds panic??

@xdustinface

Copy link
Copy Markdown
Collaborator

Then, talking about the unimplemented!, I don't want to return an error honestly, what about debug_assert(false, "explanation") and the returning None so only debug builds panic??

Sure, go for it.

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.

3 participants