Skip to content

refactor: move core tx building and signing logic into the key-wallet-manager crate#727

Open
ZocoLini wants to merge 1 commit intov0.42-devfrom
refactor/tx-building
Open

refactor: move core tx building and signing logic into the key-wallet-manager crate#727
ZocoLini wants to merge 1 commit intov0.42-devfrom
refactor/tx-building

Conversation

@ZocoLini
Copy link
Copy Markdown
Collaborator

@ZocoLini ZocoLini commented May 4, 2026

I want this logic accesible to all rust consumers (eg: platform-wallet), it doesn't make sense at all to have logic in the FFI side

Summary by CodeRabbit

  • Refactor
    • Consolidated transaction building and signing functionality to use centralized wallet manager delegation instead of manual local construction.
    • Enhanced error handling and reporting for transaction builder failures with improved error messages and proper categorization.
    • Reorganized code structure, module imports, and dependencies for improved maintainability and code clarity.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

The transaction building logic is being refactored from the FFI layer into the WalletManager. The FFI function now delegates to a new WalletManager::build_and_sign_transaction method that encapsulates address validation, UTXO selection, input derivation, and fee computation. Error handling is updated to convert builder errors into wallet errors.

Changes

Transaction Building Refactoring

Layer / File(s) Summary
Error Conversion
key-wallet-manager/src/error.rs
New From<BuilderError> for WalletError implementation maps transaction builder failures to WalletError::TransactionBuild.
Manager Implementation
key-wallet-manager/src/lib.rs
New public build_and_sign_transaction method fetches change address, constructs TransactionBuilder with validated outputs, selects inputs via BranchAndBound, derives per-UTXO child keys from wallet root, builds and signs transaction, and computes fees.
FFI Simplification
key-wallet-ffi/src/transaction.rs
wallet_build_and_sign_transaction converts C array outputs to Vec<(Address, amount)>, delegates building/signing to manager.build_and_sign_transaction(), and removes manual builder construction and fee calculation logic.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A builder's burden, now shared and light,
From FFI hands to Manager's sight,
Key derivation and UTXO dance,
Fee-rate precision, a second chance,
Transactions bloom where logic refines! 🌱

🚥 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 accurately describes the main change: moving core transaction building and signing logic from the FFI crate into the key-wallet-manager crate, which is clearly reflected in the code changes.
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 refactor/tx-building

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

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

Caution

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

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

134-156: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: Return tuple not destructured; fee_out is never written.

manager.build_and_sign_transaction returns Result<(Transaction, u64), WalletError>. The tuple is assigned to transaction, but then consensus::serialize(&transaction) attempts to serialize the tuple (Transaction, u64) instead of just the Transaction. Additionally, the fee_out parameter (documented to receive the fee on success) is never written.

Proposed fix
-            let transaction = unwrap_or_return!(
+            let (transaction, fee) = unwrap_or_return!(
                 manager.build_and_sign_transaction(
                     &wallet_id,
                     account_index,
                     outputs,
                     FeeRate::new(fee_per_kb)
                 ),
                 error
             );

             // Serialize the transaction
             let serialized = consensus::serialize(&transaction);
             let size = serialized.len();

             let boxed = serialized.into_boxed_slice();
             let tx_bytes = Box::into_raw(boxed) as *mut u8;

             *tx_bytes_out = tx_bytes;
             *tx_len_out = size;
+            *fee_out = fee;

             (*error).clean();
             true
🤖 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 134 - 156, The code
incorrectly treats the Result from manager.build_and_sign_transaction as a
single Transaction; destructure the result into (transaction, fee) (e.g., let
(transaction, fee) = unwrap_or_return!(manager.build_and_sign_transaction(...),
error);), then call consensus::serialize(&transaction) (not the tuple), set
*fee_out = fee (convert to expected type if necessary) before cleaning the
error, and keep assignments to *tx_bytes_out and *tx_len_out as-is so tx/len and
fee are all written on success.
🧹 Nitpick comments (4)
key-wallet-manager/src/error.rs (1)

102-106: 💤 Low value

Consider removing the redundant prefix in the error conversion.

The Display impl for WalletError::TransactionBuild (line 57) already prefixes messages with "Transaction build failed: ". This conversion adds another prefix "Transaction building error: ", resulting in messages like "Transaction build failed: Transaction building error: <actual error>".

Suggested fix
 impl From<BuilderError> for WalletError {
     fn from(err: BuilderError) -> Self {
-        WalletError::TransactionBuild(format!("Transaction building error: {}", err))
+        WalletError::TransactionBuild(err.to_string())
     }
 }
🤖 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-manager/src/error.rs` around lines 102 - 106, The
From<BuilderError> for WalletError impl is adding a redundant prefix; update the
impl for From<BuilderError> so it constructs WalletError::TransactionBuild using
only the BuilderError message (e.g., err.to_string()) instead of prepending
"Transaction building error: ", so the existing Display for
WalletError::TransactionBuild remains the sole prefix provider.
key-wallet-ffi/src/transaction.rs (1)

130-131: 💤 Low value

Redundant nested unsafe block.

The function is already declared unsafe extern "C", so the inner unsafe block at line 130 is unnecessary.

Suggested simplification
-    unsafe {
-        manager_ref.runtime.block_on(async {
+    manager_ref.runtime.block_on(async {
             // ... function body ...
-        })
-    }
+    })
🤖 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 130 - 131, The inner unsafe
block around the call to manager_ref.runtime.block_on(async { ... }) is
redundant because the enclosing function is already declared unsafe extern "C";
remove that inner `unsafe { ... }` wrapper and directly call
`manager_ref.runtime.block_on(async { ... })` (preserving the async closure
body) so the function remains unsafe but without nested unsafe blocks; locate
this in the function containing the `manager_ref.runtime.block_on` call in
transaction.rs.
key-wallet-manager/src/lib.rs (2)

957-960: 💤 Low value

Move HashMap import to the top of the file.

The use std::collections::HashMap statement inside the function body is unconventional. While it works, placing imports at module scope improves readability and follows Rust conventions.

🤖 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-manager/src/lib.rs` around lines 957 - 960, The local use
statement for HashMap should be moved to module scope: remove the inline `use
std::collections::HashMap;` inside the function and add `use
std::collections::HashMap;` with the other top-level imports so the code that
creates `let mut address_to_path: HashMap<dashcore::Address,
key_wallet::DerivationPath> = HashMap::new();` remains unchanged; this keeps
imports conventional and improves readability.

897-904: 💤 Low value

Method only supports BIP44 accounts; consider documenting this limitation or accepting account type preference.

The method hardcodes AccountTypePreference::BIP44 for change address (line 907) and only queries standard_bip44_accounts (line 920). Users with BIP32-only accounts cannot use this method.

If this is intentional, consider adding documentation. If broader support is needed, the method signature could accept an AccountTypePreference parameter.

🤖 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-manager/src/lib.rs` around lines 897 - 904, The
build_and_sign_transaction method currently forces BIP44-only flow
(AccountTypePreference::BIP44 and standard_bip44_accounts), which excludes
BIP32-only wallets; either document this limitation on
WalletManager::build_and_sign_transaction or make it accept an
AccountTypePreference parameter and branch accordingly: add an
account_type_pref: AccountTypePreference argument to build_and_sign_transaction,
use that value instead of AccountTypePreference::BIP44 when requesting a change
address, and replace queries to standard_bip44_accounts with the account list
lookup that matches the chosen AccountTypePreference (and update any helper
calls that enumerate accounts or derive change addresses), and update all call
sites to pass the desired preference.
🤖 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-manager/src/lib.rs`:
- Around line 969-985: The closure passed to tx_builder.select_inputs currently
calls secp256k1::Secp256k1::new() for every UTXO which is expensive; create a
single Secp256k1 instance outside the closure (e.g., let secp =
secp256k1::Secp256k1::new();) and capture/reuse it inside the closure, leaving
the rest of the logic (address_to_path.get(&utxo.address),
root_xpriv.to_extended_priv_key(wallet.network), derived_xpriv =
root_ext_priv.derive_priv(&secp, path).ok() and returning
Some(derived_xpriv.private_key)) unchanged.

---

Outside diff comments:
In `@key-wallet-ffi/src/transaction.rs`:
- Around line 134-156: The code incorrectly treats the Result from
manager.build_and_sign_transaction as a single Transaction; destructure the
result into (transaction, fee) (e.g., let (transaction, fee) =
unwrap_or_return!(manager.build_and_sign_transaction(...), error);), then call
consensus::serialize(&transaction) (not the tuple), set *fee_out = fee (convert
to expected type if necessary) before cleaning the error, and keep assignments
to *tx_bytes_out and *tx_len_out as-is so tx/len and fee are all written on
success.

---

Nitpick comments:
In `@key-wallet-ffi/src/transaction.rs`:
- Around line 130-131: The inner unsafe block around the call to
manager_ref.runtime.block_on(async { ... }) is redundant because the enclosing
function is already declared unsafe extern "C"; remove that inner `unsafe { ...
}` wrapper and directly call `manager_ref.runtime.block_on(async { ... })`
(preserving the async closure body) so the function remains unsafe but without
nested unsafe blocks; locate this in the function containing the
`manager_ref.runtime.block_on` call in transaction.rs.

In `@key-wallet-manager/src/error.rs`:
- Around line 102-106: The From<BuilderError> for WalletError impl is adding a
redundant prefix; update the impl for From<BuilderError> so it constructs
WalletError::TransactionBuild using only the BuilderError message (e.g.,
err.to_string()) instead of prepending "Transaction building error: ", so the
existing Display for WalletError::TransactionBuild remains the sole prefix
provider.

In `@key-wallet-manager/src/lib.rs`:
- Around line 957-960: The local use statement for HashMap should be moved to
module scope: remove the inline `use std::collections::HashMap;` inside the
function and add `use std::collections::HashMap;` with the other top-level
imports so the code that creates `let mut address_to_path:
HashMap<dashcore::Address, key_wallet::DerivationPath> = HashMap::new();`
remains unchanged; this keeps imports conventional and improves readability.
- Around line 897-904: The build_and_sign_transaction method currently forces
BIP44-only flow (AccountTypePreference::BIP44 and standard_bip44_accounts),
which excludes BIP32-only wallets; either document this limitation on
WalletManager::build_and_sign_transaction or make it accept an
AccountTypePreference parameter and branch accordingly: add an
account_type_pref: AccountTypePreference argument to build_and_sign_transaction,
use that value instead of AccountTypePreference::BIP44 when requesting a change
address, and replace queries to standard_bip44_accounts with the account list
lookup that matches the chosen AccountTypePreference (and update any helper
calls that enumerate accounts or derive change addresses), and update all call
sites to pass the desired preference.
🪄 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: 2f78ef05-f68e-4877-b2a5-173fca126d46

📥 Commits

Reviewing files that changed from the base of the PR and between 95377d5 and 62b36f4.

📒 Files selected for processing (3)
  • key-wallet-ffi/src/transaction.rs
  • key-wallet-manager/src/error.rs
  • key-wallet-manager/src/lib.rs

Comment thread key-wallet-manager/src/lib.rs
@codecov
Copy link
Copy Markdown

codecov Bot commented May 4, 2026

Codecov Report

❌ Patch coverage is 0% with 79 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.82%. Comparing base (95377d5) to head (62b36f4).
⚠️ Report is 1 commits behind head on v0.42-dev.

Files with missing lines Patch % Lines
key-wallet-manager/src/lib.rs 0.00% 61 Missing ⚠️
key-wallet-ffi/src/transaction.rs 0.00% 15 Missing ⚠️
key-wallet-manager/src/error.rs 0.00% 3 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##           v0.42-dev     #727      +/-   ##
=============================================
- Coverage      70.85%   70.82%   -0.03%     
=============================================
  Files            319      319              
  Lines          68184    68207      +23     
=============================================
- Hits           48313    48309       -4     
- Misses         19871    19898      +27     
Flag Coverage Δ
core 75.81% <ø> (ø)
ffi 45.70% <0.00%> (+0.20%) ⬆️
rpc 20.00% <ø> (ø)
spv 87.21% <ø> (-0.03%) ⬇️
wallet 69.37% <0.00%> (-0.24%) ⬇️
Files with missing lines Coverage Δ
key-wallet-manager/src/error.rs 0.00% <0.00%> (ø)
key-wallet-ffi/src/transaction.rs 0.00% <0.00%> (ø)
key-wallet-manager/src/lib.rs 54.63% <0.00%> (-7.48%) ⬇️

... and 2 files with indirect coverage changes

Copy link
Copy Markdown
Member

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

It should be in key wallet and not key wallet manager. (Maybe a convenience method in key wallet manager though).

@ZocoLini
Copy link
Copy Markdown
Collaborator Author

ZocoLini commented May 4, 2026

But there is no struct in key-wallet that knows Wallet and ManagedWalletInfo at the same time, only the Wallet manager does

Comment on lines +939 to +955
// Get the wallet's root extended private key for signing
let root_xpriv = match &wallet.wallet_type {
WalletType::Mnemonic {
root_extended_private_key,
..
} => root_extended_private_key,
WalletType::Seed {
root_extended_private_key,
..
} => root_extended_private_key,
WalletType::ExtendedPrivKey(root_extended_private_key) => root_extended_private_key,
_ => {
return Err(WalletError::TransactionBuild(
"Cannot sign with watch-only wallet".to_string(),
));
}
};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The ios wallet is external signable, so this is incorrect.

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