Skip to content

Conversation

@QuantumExplorer
Copy link
Member

@QuantumExplorer QuantumExplorer commented Aug 28, 2025

Summary by CodeRabbit

  • New Features

    • Multi-network (bit-flag) wallets, serialized wallet import/export, wallet ID enumeration, externally-signable/watch-only options, richer account & address-pool FFI accessors.
  • Breaking Changes

    • Balance FFI removed; many FFI wallet/account APIs renamed/updated to use multi-network flags and typed account enums; some legacy creation helpers removed/renamed.
  • Security

    • Zeroization for mnemonics, seeds, chain codes, root keys and wallets.
  • Refactor

    • Stronger network validation with clearer errors and consolidated account typing.
  • Chores / Tests

    • Bincode serialization feature added; extensive serialization, account-collection and managed-account tests added.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 28, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Refactors wallet creation to return WalletId and accept network slices; expands FFI for multi-network bitflags, typed account/collection handles, and serialized-wallet (bincode) export/import; removes FFI balance APIs; adds zeroization for sensitive types and broad test adjustments.

Changes

Cohort / File(s) Summary
dash-spv callsite
dash-spv/src/main.rs
Updated create_wallet_from_mnemonic call: removed explicit WalletId/label args, pass mnemonic/label and &[network] per new WalletManager signature.
WalletManager core & examples
key-wallet-manager/src/wallet_manager/mod.rs, key-wallet-manager/examples/wallet_creation.rs, key-wallet-manager/Cargo.toml
Creation/import API changed: creation returns WalletId, supports &[Network], added create_wallet_with_random_mnemonic, bincode serialization/import, feature bincode and zeroize deps.
key-wallet core (multi-network & zeroize)
key-wallet/src/**/*, key-wallet/Cargo.toml
Wallet constructors (new_random, from_mnemonic, from_seed, from_extended_key) now accept &[Network]; from_xpub gains watch/external flag; add compute_wallet_id_from_root_extended_pub_key, Zeroize impls/derives for Wallet, root keys, Mnemonic, Seed, ChainCode.
FFI types & header / manifest
key-wallet-ffi/include/key_wallet_ffi.h, key-wallet-ffi/cbindgen.toml, key-wallet-ffi/src/types.rs, key-wallet-ffi/Cargo.toml
FFINetwork becomes bitflags (NO/ALL), added parsing/try_into helpers; new forward-declared FFI types; Cargo features bincode, eddsa, bls added and default features updated.
FFI wallet/account/managed-account surfaces
key-wallet-ffi/src/wallet.rs, key-wallet-ffi/src/account.rs, key-wallet-ffi/src/managed_account.rs, key-wallet-ffi/src/account_collection.rs, key-wallet-ffi/src/managed_account_collection.rs
Introduced typed FFIAccount/FFIManagedAccount, FFIAccountType, account and managed-account collection FFI modules, many new extern APIs, Arc-backed ownership, getters, and null-safety/error handling.
FFI address pool & address APIs
key-wallet-ffi/src/address_pool.rs, key-wallet-ffi/src/address.rs
Added FFIAddressPool/FFIAddressInfo/FFIAddressPoolInfo, address-pool accessors; replaced numeric account_type with FFIAccountType, removed registration_index, and added try_into() network validation with explicit errors.
FFI wallet-manager serialization & IDs
key-wallet-ffi/src/wallet_manager.rs, key-wallet-ffi/src/wallet_manager_serialization_tests.rs, key-wallet-ffi/src/wallet_manager_tests.rs
Added bincode-gated FFI: create->serialized bytes, free bytes, import-from-bytes, and wallet_manager_get_wallet_ids; mnemonic creation now returns/tracks wallet_id; serialization tests added.
FFI balance removal
key-wallet-ffi/src/balance.rs, key-wallet-ffi/src/balance_tests.rs
Removed balance module and its tests; FFIBalance and wallet_get_balance / wallet_get_account_balance removed.
FFI provider/keys/transactions/derivation/keys/utxo
key-wallet-ffi/src/provider_keys.rs, key-wallet-ffi/src/transaction.rs, key-wallet-ffi/src/transaction_checking.rs, key-wallet-ffi/src/derivation.rs, key-wallet-ffi/src/keys.rs, key-wallet-ffi/src/utxo.rs
Replaced network.into() with fallible try_into() or parse for single-network validation; renamed params (network→networks/_network); removed wallet_get_provider_key_address; added explicit InvalidInput errors for ambiguous networks.
FFI lib exports & tests
key-wallet-ffi/src/lib.rs, key-wallet-ffi/src/*_tests.rs, key-wallet-ffi/tests/*
New modules: account_collection, managed_account, managed_account_collection; removed balance export; tests updated/added for account types, multi-network, serialization/import; some watch-only/xpub and balance tests removed.
key-wallet-manager tests & SPV
key-wallet-manager/tests/*, key-wallet-manager/tests/spv_integration_tests.rs
Tests updated to use WalletManager creation APIs that return WalletId (random mnemonic flow); fixed-ID usages replaced; serialized-wallet tests added; some SPV tests removed.
Various tests updated/removed across crates
key-wallet-ffi/tests/*, key-wallet/src/tests/*, key-wallet-manager/tests/*
Widespread test updates to pass &[Network::X] slices, adapt to new APIs and removed features; some whole test suites removed (e.g., coinjoin tests, certain FFI balance/watch-only tests).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant FFI as key-wallet-ffi
  participant Mgr as WalletManager
  participant Core as key-wallet

  rect #e6f5ff
  note right of App: Create wallet and get serialized bytes (bincode)
  App->>FFI: wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes(mnemonic, pass, FFINetwork flags, opts, flags)
  FFI->>Mgr: create_wallet_from_mnemonic_return_serialized_bytes(mnemonic, pass, &[Network], ...)
  Mgr->>Core: Wallet::from_mnemonic(mnemonic, &[Network], opts)
  Core-->>Mgr: Wallet + root pubkey
  Mgr-->>FFI: (wallet_id, serialized_bytes)
  FFI-->>App: wallet_id, bytes
  end

  rect #f0fff0
  note right of App: Import serialized wallet bytes into manager
  App->>FFI: wallet_manager_import_wallet_from_bytes(bytes, len)
  FFI->>Mgr: import_wallet_from_bytes(bytes)
  Mgr->>Core: deserialize -> Wallet
  Core-->>Mgr: Wallet
  Mgr-->>FFI: wallet_id
  FFI-->>App: wallet_id
  end

  rect #fff5e6
  note right of App: Single-network validation for account ops
  App->>FFI: wallet_get_account(..., FFINetwork flags)
  FFI->>FFI: try_into_single_network()
  alt single network
    FFI->>Core: lookup account on Network
    Core-->>FFI: account handle
    FFI-->>App: success (FFIAccount)
  else ambiguous/none
    FFI-->>App: error InvalidInput ("Must specify exactly one network")
  end
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Poem

I nibble keys beneath the moon’s soft light,
Flags of networks flutter—one, or many, bright.
IDs from roots, bytes tucked in a cozy lair,
Secrets zeroized with quiet, careful care.
Hop, import, restore — wallets bounce with delight. 🐇✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/cleanUpKeyWallet5

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbit in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbit in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbit gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbit read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbit help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbit ignore or @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbit summary or @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbit or @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
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: 17

Caution

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

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

34-45: Enforce wallet_id ownership to prevent cross-wallet contamination

Before reading accounts from the passed Wallet, ensure it belongs to this ManagedWalletInfo.

Apply this diff near the start of add_managed_account:

-        // First check if the account exists in the wallet
+        // Validate wallet ownership
+        if wallet.wallet_id != self.wallet_id {
+            return Err(Error::InvalidParameter(
+                "wallet_id does not match ManagedWalletInfo".to_string(),
+            ));
+        }
+        // First check if the account exists in the wallet

86-109: Factor passphrase verification into a single helper; zeroize seed

Three functions duplicate passphrase checks and hold sensitive material in memory. Centralize verification and zeroize the seed after use.

Apply this diff to add a reusable verifier and use it (shown for add_managed_account_with_passphrase; mirror in the BLS/EdDSA variants):

@@
-        // Verify this is a passphrase wallet
-        match &wallet.wallet_type {
-            WalletType::MnemonicWithPassphrase { mnemonic, .. } => {
-                // Verify the passphrase by deriving and comparing
-                let seed = mnemonic.to_seed(passphrase);
-                let root_key = crate::wallet::root_extended_keys::RootExtendedPrivKey::new_master(&seed)?;
-
-                // Compare with wallet's stored public key
-                let derived_pub = root_key.to_root_extended_pub_key();
-                let wallet_pub = wallet.root_extended_pub_key();
-
-                if derived_pub.root_public_key != wallet_pub.root_public_key {
-                    return Err(Error::InvalidParameter(
-                        "Invalid passphrase".to_string()
-                    ));
-                }
-
-                // Passphrase is valid, proceed with adding the managed account
-                self.add_managed_account(wallet, account_type, network)
-            }
-            _ => Err(Error::InvalidParameter(
-                "add_managed_account_with_passphrase can only be used with wallets created with a passphrase".to_string()
-            )),
-        }
+        verify_wallet_passphrase(wallet, passphrase)?;
+        self.add_managed_account(wallet, account_type, network)

Add (outside this impl) a helper that zeroizes the seed:

use zeroize::Zeroize;

fn verify_wallet_passphrase(wallet: &Wallet, passphrase: &str) -> Result<()> {
    match &wallet.wallet_type {
        WalletType::MnemonicWithPassphrase { mnemonic, .. } => {
            let mut seed = mnemonic.to_seed(passphrase);
            let root_key =
                crate::wallet::root_extended_keys::RootExtendedPrivKey::new_master(&seed)?;
            // seed no longer needed
            seed.zeroize();

            let derived_pub = root_key.to_root_extended_pub_key();
            let wallet_pub = wallet.root_extended_pub_key();
            if derived_pub.root_public_key != wallet_pub.root_public_key {
                return Err(Error::InvalidParameter("Invalid passphrase".to_string()));
            }
            Ok(())
        }
        _ => Err(Error::InvalidParameter(
            "function requires a wallet created with a passphrase".to_string(),
        )),
    }
}

Note: If zeroize is not yet a dependency, consider adding it (optional feature if desired).

Also applies to: 201-224, 325-348


117-122: Enforce xpub/network consistency
In key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs’s add_managed_account_from_xpub, add a guard before calling Account::new to reject mismatched networks:

if account_xpub.network != network {
    return Err(Error::InvalidParameter("xpub network mismatch".into()));
}
// Create an Account with no wallet ID (standalone managed account)
let account = Account::new(None, account_type, account_xpub, network)?;

Account::new (and its from_xpub) currently don’t verify that account_xpub.network == network, so this check is required to prevent silent network mismatches.

key-wallet-ffi/tests/test_valid_addr.rs (1)

17-48: Make the test assertive; it currently passes even on failure.

  • Using if let Some(...) allows the test to silently succeed when the account is missing.
  • The parse/require-network match only prints; no assertion on success.

Apply this to fail fast and validate expectations:

-    if let Some(account) = wallet.get_bip44_account(Network::Testnet, 0) {
+    let account = wallet
+        .get_bip44_account(Network::Testnet, 0)
+        .expect("Expected BIP44 account 0 on testnet to exist");
@@
-        println!("Generated testnet address: {}", address);
+        println!("Generated testnet address: {}", address);
@@
-        // Now try to validate it
-        let addr_str = address.to_string();
-        match key_wallet::Address::from_str(&addr_str) {
-            Ok(parsed) => {
-                println!("Successfully parsed generated address");
-                match parsed.require_network(dashcore::Network::Testnet) {
-                    Ok(_) => println!("✓ Address is valid for testnet"),
-                    Err(e) => println!("✗ Address not valid for testnet: {}", e),
-                }
-            }
-            Err(e) => {
-                println!("Failed to parse generated address: {}", e);
-            }
-        }
+        // Now validate it
+        let addr_str = address.to_string();
+        let parsed = key_wallet::Address::from_str(&addr_str)
+            .expect("Failed to parse generated address");
+        parsed
+            .require_network(dashcore::Network::Testnet)
+            .expect("Address not valid for testnet");
@@
-    }
+    // unreachable: the expect above will fail the test if account is missing
key-wallet-ffi/src/address.rs (1)

37-50: UB: freeing Box<[T]> with a thin pointer; fix address_array_free.

Box::from_raw(std::slice::from_raw_parts_mut(...)) is invalid; reconstruct a fat pointer with ptr::slice_from_raw_parts_mut before Box::from_raw. Also avoid double-unsafe nesting and iterate with iter_mut().

Apply:

 pub unsafe extern "C" fn address_array_free(addresses: *mut *mut c_char, count: usize) {
     if !addresses.is_null() {
-        unsafe {
-            let slice = std::slice::from_raw_parts_mut(addresses, count);
-            for addr in slice {
-                if !addr.is_null() {
-                    let _ = CString::from_raw(*addr);
-                }
-            }
-            // Free the array itself
-            let _ = Box::from_raw(std::slice::from_raw_parts_mut(addresses, count));
-        }
+        let slice = std::slice::from_raw_parts_mut(addresses, count);
+        for addr in slice.iter_mut() {
+            if !addr.is_null() {
+                let _ = CString::from_raw(*addr);
+            }
+        }
+        // Free the array itself (allocated via Box::into_raw(Box<[*mut c_char]>))
+        let fat_ptr: *mut [*mut c_char] = std::ptr::slice_from_raw_parts_mut(addresses, count);
+        let _ = Box::from_raw(fat_ptr);
     }
 }
key-wallet-ffi/src/managed_wallet.rs (1)

1101-1111: Tests: avoid libc::free on Box allocations; use address_array_free.

Manually freeing each string then calling libc::free is UB; free the array and strings via the provided FFI helper.

Apply:

-        unsafe {
-            let addresses = std::slice::from_raw_parts(addresses_out, count_out);
-            for &addr_ptr in addresses {
-                let addr_str = CStr::from_ptr(addr_ptr).to_string_lossy();
-                assert!(!addr_str.is_empty());
-                println!("External address: {}", addr_str);
-                let _ = CString::from_raw(addr_ptr);
-            }
-            libc::free(addresses_out as *mut libc::c_void);
-        }
+        unsafe {
+            let addresses = std::slice::from_raw_parts(addresses_out, count_out);
+            for &addr_ptr in addresses {
+                let addr_str = CStr::from_ptr(addr_ptr).to_string_lossy();
+                assert!(!addr_str.is_empty());
+                println!("External address: {}", addr_str);
+            }
+            // Free the array and all inner strings correctly
+            crate::address::address_array_free(addresses_out, count_out);
+        }
key-wallet-ffi/src/keys.rs (2)

1006-1013: Bug: invalid deref of index in ChildNumber match (likely compile error).

index is a u32 in typical BIP32 enums; *index dereferences a non-pointer. Use index directly.

-            key_wallet::ChildNumber::Normal {
-                index,
-            } => (*index, false),
-            key_wallet::ChildNumber::Hardened {
-                index,
-            } => (*index, true),
+            key_wallet::ChildNumber::Normal { index } => (index, false),
+            key_wallet::ChildNumber::Hardened { index } => (index, true),

1058-1069: Fix: unsound/freeing boxed slices reconstructed via from_raw_parts_mut.

Box::from_raw expects a fat pointer *mut [T]. Reconstruct it with std::ptr::slice_from_raw_parts_mut(ptr, len). Current code passes a reference &mut [T] and will not compile; even if coerced, it’s incorrect.

-    if !indices.is_null() && count > 0 {
-        unsafe {
-            // Reconstruct the boxed slice from the raw pointer and let it drop
-            let _ = Box::from_raw(std::slice::from_raw_parts_mut(indices, count));
-        }
-    }
-    if !hardened.is_null() && count > 0 {
-        unsafe {
-            // Reconstruct the boxed slice from the raw pointer and let it drop
-            let _ = Box::from_raw(std::slice::from_raw_parts_mut(hardened, count));
-        }
-    }
+    if !indices.is_null() && count > 0 {
+        // Recreate fat pointer and drop the Box<[u32]> allocated in derivation_path_parse
+        let _ = unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(indices, count)) };
+    }
+    if !hardened.is_null() && count > 0 {
+        let _ = unsafe { Box::from_raw(std::ptr::slice_from_raw_parts_mut(hardened, count)) };
+    }
key-wallet-ffi/src/provider_keys.rs (1)

239-247: Zeroize private key buffer before free.

Avoid leaving key material in heap after free.

-        if !info.private_key.is_null() {
-            libc::free(info.private_key as *mut libc::c_void);
-            info.private_key = ptr::null_mut();
-        }
+        if !info.private_key.is_null() {
+            if info.private_key_len > 0 {
+                // Zeroize private key bytes before freeing
+                std::ptr::write_bytes(info.private_key, 0, info.private_key_len);
+                info.private_key_len = 0;
+            }
+            libc::free(info.private_key as *mut libc::c_void);
+            info.private_key = ptr::null_mut();
+        }
key-wallet-ffi/src/derivation.rs (2)

582-585: Doc fix: correct the deallocation function name.

The function returns an FFIExtendedPubKey* that must be freed via derivation_xpub_free.

-/// - The returned pointer must be freed with `extended_public_key_free`
+/// - The returned pointer must be freed with `derivation_xpub_free`

620-621: Doc fix: correct the string free function.

Use the actual exported symbol.

-/// - The returned string must be freed with `string_free`
+/// - The returned string must be freed with `derivation_string_free`
key-wallet-ffi/src/transaction.rs (2)

201-226: Potential u64→u32 timestamp truncation.

Casting timestamp to u32 can overflow silently. Validate bounds or saturate; prefer explicit error.

-                    timestamp: if timestamp > 0 {
-                        Some(timestamp as u32)
-                    } else {
-                        None
-                    },
+                    timestamp: if timestamp == 0 {
+                        None
+                    } else if timestamp <= u32::MAX as u64 {
+                        Some(timestamp as u32)
+                    } else {
+                        FFIError::set_error(
+                            error,
+                            FFIErrorCode::InvalidInput,
+                            "Timestamp exceeds u32::MAX".to_string(),
+                        );
+                        return false;
+                    },

Apply the same guard in the InChainLockedBlock branch.


268-274: UB: freeing transaction bytes without length.

Box::from_raw(tx_bytes) only drops a single u8, not the allocated slice; this is undefined behavior if the buffer was a Box<[u8]>. Mirror wallet_manager_free_wallet_bytes and accept length.

-#[no_mangle]
-pub unsafe extern "C" fn transaction_bytes_free(tx_bytes: *mut u8) {
-    if !tx_bytes.is_null() {
-        unsafe {
-            let _ = Box::from_raw(tx_bytes);
-        }
-    }
-}
+#[no_mangle]
+pub unsafe extern "C" fn transaction_bytes_free(tx_bytes: *mut u8, tx_len: usize) {
+    if !tx_bytes.is_null() && tx_len > 0 {
+        // Reconstruct the boxed slice for proper drop of DST allocation
+        let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(tx_bytes, tx_len));
+    }
+}

If ABI stability is required, keep the old symbol as a no-op shim and introduce transaction_bytes_free_with_len(tx_bytes, tx_len).

key-wallet/src/tests/edge_case_tests.rs (1)

31-45: This test is a no-op; either implement it or ignore it.

The test builds cases but performs no assertions. Either parse and validate errors or mark the test ignored to avoid false positives.

 #[test]
-fn test_invalid_derivation_paths() {
+#[ignore] // TODO(qa): implement parsing or remove
+fn test_invalid_derivation_paths() {
key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs (1)

114-120: Infinite recursion in trait impl for ManagedWalletInfo::from_wallet and from_wallet_with_name.

Both methods call themselves via Self::..., causing unbounded recursion at runtime. Delegate to the inherent constructors on ManagedWalletInfo instead.

-    fn from_wallet(wallet: &Wallet) -> Self {
-        Self::from_wallet_with_name(wallet, String::new())
-    }
+    fn from_wallet(wallet: &Wallet) -> Self {
+        ManagedWalletInfo::from_wallet(wallet)
+    }
@@
-    fn from_wallet_with_name(wallet: &Wallet, name: String) -> Self {
-        Self::from_wallet_with_name(wallet, name)
-    }
+    fn from_wallet_with_name(wallet: &Wallet, name: String) -> Self {
+        ManagedWalletInfo::from_wallet_with_name(wallet, name)
+    }
key-wallet-manager/src/wallet_manager/mod.rs (1)

113-165: Method documentation doesn't match implementation

The method returns a WalletId but the documentation still says "add it to the manager", which is misleading since the primary purpose is now to create and return the wallet ID.

Apply this diff to fix the documentation:

-    /// Create a new wallet from mnemonic and add it to the manager
-    /// Returns the computed wallet ID
+    /// Create a wallet from mnemonic and add it to the manager
+    ///
+    /// # Returns
+    /// The computed wallet ID of the newly created wallet
key-wallet-ffi/src/types.rs (1)

79-89: Defaulting unknown Network to Dash conflicts with prior preference to Testnet.

Per prior preference (fallback to testnet prefixes), switch the fallback mapping to Testnet to keep behavior consistent across crates.

 impl From<Network> for FFINetwork {
     fn from(n: Network) -> Self {
         match n {
             Network::Dash => FFINetwork::Dash,
             Network::Testnet => FFINetwork::Testnet,
             Network::Regtest => FFINetwork::Regtest,
             Network::Devnet => FFINetwork::Devnet,
-            _ => FFINetwork::Dash, // Default to Dash for unknown networks
+            _ => FFINetwork::Testnet, // Default to Testnet for unknown networks (preferred fallback)
         }
     }
 }
🧹 Nitpick comments (53)
key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs (2)

49-63: DRY: deduplicate “get/dup-check/insert” managed-account pattern

The same insertion sequence is repeated 6x. Extract a helper to reduce errors and keep logic consistent.

Apply this diff in each location:

-        // Get or create the managed account collection for this network
-        let managed_collection = self.accounts.entry(network).or_default();
-
-        // Check if managed account already exists
-        if managed_collection.contains_managed_account_type(managed_account.managed_type()) {
-            return Err(Error::InvalidParameter(format!(
-                "Managed account type {:?} already exists for network {:?}",
-                account_type, network
-            )));
-        }
-
-        // Insert into the collection
-        managed_collection.insert(managed_account);
-        Ok(())
+        self.insert_managed(network, account_type, managed_account)

Add this helper in an impl ManagedWalletInfo block:

impl ManagedWalletInfo {
    fn insert_managed(
        &mut self,
        network: Network,
        account_type: AccountType,
        managed_account: ManagedAccount,
    ) -> Result<()> {
        let managed_collection = self.accounts.entry(network).or_default();
        if managed_collection.contains_managed_account_type(managed_account.managed_type()) {
            return Err(Error::InvalidParameter(format!(
                "Managed {:?} already exists for {:?}",
                account_type, network
            )));
        }
        managed_collection.insert(managed_account);
        Ok(())
    }
}

Also applies to: 123-137, 169-183, 247-261, 293-307, 371-385


388-463: Add coverage: xpub and passphrase flows (+feature-gated BLS/EdDSA)

Consider unit tests for:

  • add_managed_account_with_passphrase (valid/invalid passphrase).
  • add_managed_account_from_xpub (mismatched network should fail).
  • Feature-gated: BLS/EdDSA variants, including duplicate-insert rejection.

I can draft these tests aligned with existing helpers; want me to open a follow-up PR?

key-wallet/src/tests/advanced_transaction_tests.rs (2)

47-49: Prefer u32::MAX over magic literal 0xffffffff.

Improves readability and avoids type inference surprises in tests.

Apply this diff:

-            sequence: 0xffffffff,
+            sequence: u32::MAX,

Also applies to: 222-224, 289-291, 327-329


8-8: Remove unused import if not needed.

If Hash isn’t used by trait methods, drop it to keep clippy clean.

-use dashcore::hashes::Hash;
key-wallet/src/account/account_type.rs (2)

7-7: Avoid layering inversion: move the From for AccountTypeToCheck out of account module.

Having account_type depend on transaction_checking couples layers and risks cyclic deps. Define this impl in transaction_checking::transaction_router instead and keep account_type decoupled.

Apply this diff here:

-use crate::transaction_checking::transaction_router::AccountTypeToCheck;
@@
-impl From<AccountType> for AccountTypeToCheck {
-    fn from(value: AccountType) -> Self {
-        match value {
-            AccountType::Standard { standard_account_type, .. } => match standard_account_type {
-                StandardAccountType::BIP44Account => AccountTypeToCheck::StandardBIP44,
-                StandardAccountType::BIP32Account => AccountTypeToCheck::StandardBIP32,
-            },
-            AccountType::CoinJoin { .. } => AccountTypeToCheck::CoinJoin,
-            AccountType::IdentityRegistration => AccountTypeToCheck::IdentityRegistration,
-            AccountType::IdentityTopUp { .. } => AccountTypeToCheck::IdentityTopUp,
-            AccountType::IdentityTopUpNotBoundToIdentity => AccountTypeToCheck::IdentityTopUpNotBound,
-            AccountType::IdentityInvitation => AccountTypeToCheck::IdentityInvitation,
-            AccountType::ProviderVotingKeys => AccountTypeToCheck::ProviderVotingKeys,
-            AccountType::ProviderOwnerKeys => AccountTypeToCheck::ProviderOwnerKeys,
-            AccountType::ProviderOperatorKeys => AccountTypeToCheck::ProviderOperatorKeys,
-            AccountType::ProviderPlatformKeys => AccountTypeToCheck::ProviderPlatformKeys,
-        }
-    }
-}

Then add in transaction_checking/transaction_router (or a nearby module):

use crate::account::account_type::{AccountType, StandardAccountType};

impl From<AccountType> for AccountTypeToCheck {
    fn from(value: AccountType) -> Self {
        match value {
            AccountType::Standard { standard_account_type, .. } => match standard_account_type {
                StandardAccountType::BIP44Account => AccountTypeToCheck::StandardBIP44,
                StandardAccountType::BIP32Account => AccountTypeToCheck::StandardBIP32,
            },
            AccountType::CoinJoin { .. } => AccountTypeToCheck::CoinJoin,
            AccountType::IdentityRegistration => AccountTypeToCheck::IdentityRegistration,
            AccountType::IdentityTopUp { .. } => AccountTypeToCheck::IdentityTopUp,
            AccountType::IdentityTopUpNotBoundToIdentity => AccountTypeToCheck::IdentityTopUpNotBound,
            AccountType::IdentityInvitation => AccountTypeToCheck::IdentityInvitation,
            AccountType::ProviderVotingKeys => AccountTypeToCheck::ProviderVotingKeys,
            AccountType::ProviderOwnerKeys => AccountTypeToCheck::ProviderOwnerKeys,
            AccountType::ProviderOperatorKeys => AccountTypeToCheck::ProviderOperatorKeys,
            AccountType::ProviderPlatformKeys => AccountTypeToCheck::ProviderPlatformKeys,
        }
    }
}

Also applies to: 68-95


68-95: Optional ergonomics: also implement From<&AccountType>.

Keeps call sites flexible without relying on Copy semantics.

impl From<&AccountType> for AccountTypeToCheck {
    fn from(value: &AccountType) -> Self { (*value).into() }
}
key-wallet/src/address_metadata_tests.rs (1)

12-24: Track the ManagedAccount TODOs with issues and test plans.

Convert these notes into actionable tasks (GH issues) so we don’t lose them; ManagedAccount coverage will be important.

I can draft test scaffolding (including proptest cases) once the ManagedAccount APIs stabilize—want me to open issues with checklists?

key-wallet/src/wallet/root_extended_keys.rs (1)

375-401: New Cow API is sensible; reduce duplication in match.
Combine identical arms for brevity; no behavior change.

-            WalletType::ExternalSignable(root_extended_public_key) => {
-                Cow::Borrowed(root_extended_public_key)
-            }
-            WalletType::WatchOnly(root_extended_public_key) => {
-                Cow::Borrowed(root_extended_public_key)
-            }
+            WalletType::ExternalSignable(root_extended_public_key)
+            | WalletType::WatchOnly(root_extended_public_key) => Cow::Borrowed(root_extended_public_key),
key-wallet/Cargo.toml (1)

44-44: Prefer explicit default-features = false on zeroize.
Not strictly required, but prevents accidental feature drift.

-zeroize = { version = "1.8", features = ["derive"] }
+zeroize = { version = "1.8", default-features = false, features = ["derive"] }
key-wallet-ffi/src/keys.rs (2)

49-59: Good: switched to fallible network conversion with clear InvalidInput on multi/none.

This aligns FFI behavior with single-network enforcement and consistent error paths. Consider deduping the error string via a shared constant to avoid drift.

+const EXACTLY_ONE_NETWORK_ERR: &str = "Must specify exactly one network";
-                "Must specify exactly one network".to_string(),
+                EXACTLY_ONE_NETWORK_ERR.to_string(),

Also applies to: 109-119, 193-203, 273-283, 352-362, 515-525, 600-610, 682-692, 763-773


598-611: Style: inconsistent raw-pointer deref safety style.

Some functions use let wallet = unsafe { &*wallet };, others rely on unsafe fn context. Pick one style for readability.

Also applies to: 680-693, 761-774

key-wallet/src/seed.rs (1)

120-134: Optional: avoid revealing seed bytes in Display.

Even partial exposure can leak sensitive information via logs. Consider redacting all bytes.

-        write!(f, "Seed({}...{})", start, end)
+        write!(f, "Seed(REDACTED)")
key-wallet-ffi/src/account_tests.rs (1)

45-51: Add success checks for wallet creation and guard against param-validation regressions

  • After wallet_create_from_mnemonic, assert Success and non-null wallet.
  • After wallet_get_account, assert it’s not an InvalidInput (to catch enum wiring regressions).

Apply:

         let wallet = unsafe {
             wallet::wallet_create_from_mnemonic(
                 mnemonic.as_ptr(),
                 passphrase.as_ptr(),
                 FFINetwork::Testnet,
                 &mut error,
             )
         };
+        assert!(!wallet.is_null());
+        assert_eq!(error.code, FFIErrorCode::Success);

         // Try to get the default account (should exist)
         let result = unsafe {
             wallet_get_account(wallet, FFINetwork::Testnet, 0, FFIAccountType::StandardBIP44)
         };
+        assert_ne!(result.error_code, FFIErrorCode::InvalidInput as i32);
key-wallet/src/wallet/passphrase_test.rs (1)

24-26: Network slice API updates — LGTM; add a wrong-passphrase negative check

The conversion to &[Network] is consistent. Consider also asserting failure with an incorrect passphrase to harden behavior.

Add within test_add_account_with_passphrase:

// Wrong passphrase should fail
let wrong = wallet.add_account_with_passphrase(
    AccountType::Standard { index: 2, standard_account_type: StandardAccountType::BIP44Account },
    network,
    "wrong_passphrase"
);
assert!(wrong.is_err());
assert!(wrong.unwrap_err().to_string().contains("passphrase"));

Also applies to: 73-76, 107-111, 153-154

key-wallet/src/tests/performance_tests.rs (1)

92-95: Network slice changes — LGTM; gate perf tests to avoid CI flakiness

These tests use tight timing thresholds and can be noisy on shared runners. Consider feature-gating or marking them ignored by default.

Add before each performance test:

#[cfg_attr(not(feature = "perf-tests"), ignore)]

Or gate the whole module:

#![cfg(any(test, feature = "perf-tests"))]

Also applies to: 138-141, 206-209, 305-307

key-wallet/src/tests/integration_tests.rs (1)

20-22: Network slice conversion — LGTM; consider exercising multi-network creation directly

You add accounts on Dash/Devnet post-creation; also consider initializing the wallet with multiple networks to test multi-network account bootstrapping.

Example:

-        &[Network::Testnet],
+        &[Network::Testnet, Network::Dash, Network::Devnet],

Also applies to: 76-79

key-wallet-manager/Cargo.toml (1)

26-26: Gate zeroize behind the bincode feature (if not used elsewhere).

This avoids pulling zeroize for consumers not using serialization.

-zeroize = { version = "1.8", features = ["derive"] }
+zeroize = { version = "1.8", features = ["derive"], optional = true }
key-wallet-ffi/src/derivation.rs (1)

736-740: Optional: zeroize secrets on free.

Consider zeroizing the private key material before drop if supported by key_wallet::bip32::ExtendedPrivKey.

I can draft a zeroize-on-drop implementation gated by a feature if ExtendedPrivKey exposes zeroizable fields.

key-wallet-manager/tests/spv_integration_tests.rs (1)

202-209: Prefer expect() over .ok() to surface failures.

Hide-and-assert via wallet_count() makes debugging harder if creation fails.

-    for _i in 0..3 {
-        spv.base
-            .create_wallet_with_random_mnemonic(
-                WalletAccountCreationOptions::Default,
-                Network::Testnet,
-            )
-            .ok();
-    }
+    for _i in 0..3 {
+        spv.base
+            .create_wallet_with_random_mnemonic(
+                WalletAccountCreationOptions::Default,
+                Network::Testnet,
+            )
+            .expect("Failed to create wallet");
+    }
key-wallet-ffi/src/wallet_tests.rs (1)

285-292: Tighten assertions for wallet_add_account result.

Assert success implies non-null account and error_code==0; failure implies null account and error_code!=0.

-        // Some implementations may not support adding accounts, so just verify it doesn't crash
-        // and the error code is set appropriately
-        assert!(!result.account.is_null() || result.error_code != 0);
+        // Success -> account != null and error_code == 0; Failure -> account == null and error_code != 0
+        if result.account.is_null() {
+            assert_ne!(result.error_code, 0);
+        } else {
+            assert_eq!(result.error_code, 0);
+        }
key-wallet-manager/tests/test_serialized_wallets.rs (2)

20-20: Reduce derivation cap to speed tests.

100_000 can slow CI; a much smaller cap exercises the path.

-            Some(100_000),
+            Some(1_000),

Also applies to: 36-36, 55-55


31-48: Add assertions about watch-only/external-signable semantics after import.

If an API exists to query wallet capabilities, import these serialized bytes and assert “no private keys”/“externally signable” flags.

I can extend the tests once you confirm the accessor (e.g., WalletManager::wallet_info(id) or similar) to validate flags on the imported wallet.

Also applies to: 50-65

key-wallet-ffi/src/account.rs (3)

157-167: Avoid parameter shadowing; keep naming consistent with other functions.

Use network_rust (as above) to avoid shadowing the FFINetwork param and to keep consistency.

Apply this diff:

-    let network: key_wallet::Network = match network.try_into() {
+    let network_rust: key_wallet::Network = match network.try_into() {
         Ok(n) => n,
         Err(_) => {
             FFIError::set_error(
                 error,
                 FFIErrorCode::InvalidInput,
                 "Must specify exactly one network".to_string(),
             );
             return 0;
         }
     };
-    match wallet.inner().accounts.get(&network) {
+    match wallet.inner().accounts.get(&network_rust) {

169-178: Minor clippy clean-up: convert bool to usize without cast.

Use usize::from(...) to avoid casts and keep clippy happy under -D warnings.

-            let count = accounts.standard_bip44_accounts.len()
+            let count = accounts.standard_bip44_accounts.len()
                 + accounts.standard_bip32_accounts.len()
                 + accounts.coinjoin_accounts.len()
-                + accounts.identity_registration.is_some() as usize
+                + usize::from(accounts.identity_registration.is_some())
                 + accounts.identity_topup.len();

113-117: Safe free for FFIAccount.

Box::from_raw drop is correct and idempotent at the callsite. Document “caller must null the pointer after free” in the header for extra safety.

key-wallet/src/wallet_comprehensive_tests.rs (2)

176-176: Comment mismatch with setup (None creates no defaults).

This test uses WalletAccountCreationOptions::None earlier; update the comment to avoid confusion.

-        // Different passphrases should generate different root keys
+        // Different passphrases should generate different root keys

(Only the preceding comment needs rewording; code is fine.)


209-216: Stronger multi-network coverage suggestion.

To exercise multi-network initialization as well as add_account, create wallets with the full networks slice (e.g., &[Testnet, Dash, Devnet]) in both creation and restore paths. Today you only pass Testnet and rely on add_account to populate others.

Also applies to: 251-258

key-wallet/src/tests/backup_restore_tests.rs (2)

176-179: Inaccurate comment about default account creation.

This file uses WalletAccountCreationOptions::None; index 0 is not created by default. Tweak the comment to avoid misleading readers.

-                index: 1, // Use index 1 since 0 is created by default
+                index: 1, // Using index 1; with None, no default accounts are created

284-292: Comment vs behavior.

“account 0 is created by default” conflicts with None; the code correctly computes initial_account_count, so just update the comment.

-    // Initial state - account 0 is created by default, no need to add it
+    // Initial state with None options: no accounts created by default
key-wallet-ffi/src/transaction.rs (2)

73-73: Unused network in wallet_sign_transaction.

Same note as build; confirm intended API or drop the param until needed to avoid FFI churn.


49-56: Clear “not yet implemented” errors.

Returning WalletError is fine for stubs; consider FFIErrorCode::NotImplemented to distinguish from runtime wallet failures.

Also applies to: 90-96

key-wallet/src/tests/edge_case_tests.rs (4)

57-60: Callsite updates to multi-network API look correct.

Passing slices (e.g., &[Network::Testnet]) matches the new constructors and should exercise per-network account creation. Consider adding at least one test with multiple networks (e.g., &[Network::Dash, Network::Testnet]) to assert account collections are created for each network.

Also applies to: 66-71, 85-88, 92-97, 136-141, 240-246, 257-258, 281-286, 291-298, 301-306, 338-345, 368-374


206-214: Heavy test may stress CI (10k outputs).

10k outputs inflate memory/time; unless you specifically need this bound, consider reducing to 1k or gating behind a feature.

Would you like me to add a cargo feature (e.g., "heavy-tests") and gate this with #[cfg(feature = "heavy-tests")]?


10-10: Remove unused import to keep clippy -D warnings clean.

-use dashcore::hashes::Hash;

315-329: Depth-255 derivation-path test is fine; consider property testing.

Looks good. Optionally switch to proptest to cover a range of depths without fixed upper bound.

key-wallet-ffi/src/wallet_manager_tests.rs (1)

1297-1596: FFI serialization/import tests are solid; replace unsafe manual copy with safe slice-to-vec.

Great coverage across watch-only and externally signable flows, plus invalid mnemonic. Replace the manual Vec::with_capacity + set_len copy with a safer to_vec() to avoid accidental UB if lengths drift.

-        let wallet_bytes_copy = unsafe {
-            let mut copy = Vec::with_capacity(wallet_bytes_len_out);
-            ptr::copy_nonoverlapping(wallet_bytes_out, copy.as_mut_ptr(), wallet_bytes_len_out);
-            copy.set_len(wallet_bytes_len_out);
-            copy
-        };
+        let wallet_bytes_copy = unsafe {
+            std::slice::from_raw_parts(wallet_bytes_out, wallet_bytes_len_out)
+        }.to_vec();
key-wallet-manager/tests/integration_test.rs (3)

29-36: Mnemonic-based creation callsite looks correct.

Parameters align with the new signature; unwrap handled via assert. Consider also testing Network::Dash to satisfy mainnet/testnet coverage preference.


65-69: Assertion couples to Default account count (8).

This may become brittle if Default changes. Consider asserting “>= 1” for existence plus targeted checks on specific known accounts, or fetch the expected count from a helper constant.


76-82: Updated callsites to create_wallet_with_random_mnemonic are consistent.

Looks good across tests. Optional: add a second test parametrized over Network to exercise both Dash and Testnet.

Also applies to: 133-139, 159-165

key-wallet-manager/src/wallet_manager/mod.rs (2)

255-259: Improve error message for serialization failures

The error message could be more helpful by preserving the original error details rather than just formatting it.

Consider preserving the error chain:

-        let serialized_bytes = bincode::encode_to_vec(&final_wallet, bincode::config::standard())
-            .map_err(|e| {
-            WalletError::InvalidParameter(format!("Failed to serialize wallet: {}", e))
-        })?;
+        let serialized_bytes = bincode::encode_to_vec(&final_wallet, bincode::config::standard())
+            .map_err(|e| {
+                WalletError::Serialization(format!("Failed to encode wallet: {}", e))
+            })?;

Note: This would require adding a Serialization variant to the WalletError enum if it doesn't already exist.


466-498: Consider extracting duplicate wallet existence check

The wallet existence check pattern (lines 480-483) is repeated across multiple functions. Consider extracting it into a helper method for consistency and maintainability.

Consider adding a helper method:

impl<T: WalletInfoInterface> WalletManager<T> {
    fn ensure_wallet_not_exists(&self, wallet_id: &WalletId) -> Result<(), WalletError> {
        if self.wallets.contains_key(wallet_id) {
            Err(WalletError::WalletExists(*wallet_id))
        } else {
            Ok(())
        }
    }
}

Then use it consistently:

-        // Check if wallet already exists
-        if self.wallets.contains_key(&wallet_id) {
-            return Err(WalletError::WalletExists(wallet_id));
-        }
+        // Check if wallet already exists
+        self.ensure_wallet_not_exists(&wallet_id)?;
key-wallet/src/wallet/mod.rs (1)

149-150: Document zeroization limitations more prominently

The comments mention that SecretKey doesn't implement Zeroize, which is an important security limitation. Consider adding a module-level or type-level documentation warning about this.

Consider adding a security note to the module documentation:

//! Complete wallet management for Dash
//! 
//! This module provides comprehensive wallet functionality including
//! multiple accounts, seed management, and transaction coordination.
//! 
//! # Security Note
//! 
//! While this module implements `Zeroize` for sensitive data, note that
//! `secp256k1::SecretKey` does not implement `Zeroize`. The chain codes
//! and other sensitive data are properly zeroized, but the secret keys
//! themselves may remain in memory after dropping.

Also applies to: 173-173

key-wallet/src/wallet/initialization.rs (1)

99-102: Potential performance issue with cloning account creation options

The code clones account_creation_options for each network iteration. For complex options with many accounts, this could be inefficient.

Consider borrowing the options instead of cloning if the methods don't need ownership:

-        for network in networks {
-            wallet.create_accounts_from_options(account_creation_options.clone(), *network)?;
-        }
+        for network in networks {
+            wallet.create_accounts_from_options(&account_creation_options, *network)?;
+        }

This would require updating the create_accounts_from_options method signature to accept a reference.

Also applies to: 157-160, 189-196, 308-311, 345-348

key-wallet-ffi/src/address_pool.rs (1)

619-698: Consider refactoring the address marking logic

The address marking logic iterates through all account types sequentially with repeated code. This could be simplified using a more functional approach.

Consider refactoring to reduce code duplication:

// Helper function to try marking address in an optional account
fn try_mark_in_account(account: Option<&mut ManagedAccount>, address: &Address) -> bool {
    account.map_or(false, |acc| acc.mark_address_used(address))
}

// Then in the main function:
let marked = collection.standard_bip44_accounts.values_mut()
    .chain(collection.standard_bip32_accounts.values_mut())
    .chain(collection.coinjoin_accounts.values_mut())
    .chain(collection.identity_topup.values_mut())
    .any(|account| account.mark_address_used(&address))
    || try_mark_in_account(collection.identity_registration.as_mut(), &address)
    || try_mark_in_account(collection.identity_topup_not_bound.as_mut(), &address)
    || try_mark_in_account(collection.identity_invitation.as_mut(), &address)
    || try_mark_in_account(collection.provider_voting_keys.as_mut(), &address)
    || try_mark_in_account(collection.provider_owner_keys.as_mut(), &address)
    || try_mark_in_account(collection.provider_operator_keys.as_mut(), &address)
    || try_mark_in_account(collection.provider_platform_keys.as_mut(), &address);
key-wallet-ffi/src/wallet_manager.rs (4)

96-106: Ensure TryInto is in scope (MSRV-friendly).

Multiple sites call network.try_into(). To avoid edition/prelude surprises and keep MSRV 1.89 happy, import the trait explicitly once at the top of this module.

Apply this diff (top of file, near other use statements):

 use std::sync::Mutex;
+use std::convert::TryInto;

Also applies to: 694-704, 787-797, 967-978, 1046-1056, 1142-1152, 1190-1200


287-297: Use consistent error code for lock poisoning.

Here lock failure maps to InvalidInput; elsewhere in this file you use WalletError for lock-related failures. Recommend unifying on WalletError.

Apply this diff:

-            FFIError::set_error(
-                error,
-                FFIErrorCode::InvalidInput,
-                "Failed to lock manager".to_string(),
-            );
+            FFIError::set_error(
+                error,
+                FFIErrorCode::WalletError,
+                "Failed to lock manager".to_string(),
+            );

512-528: Document “count” semantics for wallet IDs.

Count is number of 32-byte IDs, not bytes. Header comment already implies this; consider adding a short Rust-side doc note for future maintainers.


578-588: Returning const wallet handles matches the FFI design.

This aligns with the “read-only by default” learning. The inline comment about lifetime storage is outdated—these are owned handles; consider pruning it to avoid confusion.

-            // Note: We need to store this somewhere that will outlive this function
-            // For now, we'll return a raw pointer to the wallet
-            // In a real implementation, you might want to store these in the FFIWalletManager
+            // Return an owned FFIWallet handle (const for read-only by default).
key-wallet-ffi/src/wallet.rs (3)

96-108: Passphrase handling works; consider zeroizing the heap copy.

from_mnemonic_with_passphrase receives a heap String. For defense-in-depth, zeroize it after use (requires adding the zeroize crate).

-        match Wallet::from_mnemonic_with_passphrase(
-            mnemonic,
-            passphrase_str.to_string(),
+        let mut passphrase_owned = passphrase_str.to_string();
+        let result = Wallet::from_mnemonic_with_passphrase(
+            mnemonic,
+            passphrase_owned.clone(),
             &networks_rust,
             creation_options,
-        ) {
+        );
+        #[cfg(feature = "zeroize")]
+        {
+            use zeroize::Zeroize;
+            passphrase_owned.zeroize();
+        }
+        match result {

Note: add zeroize as an optional dependency and feature-gate if needed.

Also applies to: 111-127


170-171: Seed-based creation is good; consider zeroizing temporary seed copy.

You make an owned [u8; 64]. Zeroize after wallet creation.

     let mut seed_array = [0u8; 64];
     seed_array.copy_from_slice(seed_bytes);
     let seed = Seed::new(seed_array);
@@
-    match Wallet::from_seed(seed, &networks_rust, creation_options) {
+    let result = Wallet::from_seed(seed, &networks_rust, creation_options);
+    #[cfg(feature = "zeroize")]
+    {
+        use zeroize::Zeroize;
+        seed_array.zeroize();
+    }
+    match result {

Also applies to: 193-195, 203-216


552-572: Bytes variant assumes UTF-8/base58 text; clarify API or parse real bytes.

Current “bytes” path decodes UTF-8 then parses base58. Either rename param/docs to indicate it expects a textual xpub buffer, or accept raw xpub bytes and decode accordingly.

Also applies to: 585-609

key-wallet-ffi/src/types.rs (1)

474-486: SpecificAccounts: clarify IdentityTopUp semantics.

Pushing ffi_type.to_account_type(0) means IdentityTopUp gets registration_index=0 unless also provided via topup_indices. Consider documenting or enforcing that top-ups come via topup_indices to avoid silent index=0 surprises.

key-wallet-ffi/include/key_wallet_ffi.h (1)

2069-2074: Wallet ID buffer semantics could be emphasized.

Consider adding “count is number of 32-byte IDs; total bytes = count*32” to avoid misuse by C callers. cbindgen doc tweak should suffice.

#[cfg(test)]
mod account_tests;
#[cfg(test)]

Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Tests appear ungated; ensure they don’t compile in non-test builds.
If lib.rs doesn’t gate mod tests; with #[cfg(test)], these modules inflate non-test builds.

Apply gating on each module:

- mod account_tests;
+#[cfg(test)]
+mod account_tests;
- mod address_pool_tests;
+#[cfg(test)]
+mod address_pool_tests;
- mod advanced_transaction_tests;
+#[cfg(test)]
+mod advanced_transaction_tests;
- mod backup_restore_tests;
+#[cfg(test)]
+mod backup_restore_tests;
- mod edge_case_tests;
+#[cfg(test)]
+mod edge_case_tests;
- mod immature_transaction_tests;
+#[cfg(test)]
+mod immature_transaction_tests;
- mod integration_tests;
+#[cfg(test)]
+mod integration_tests;
- mod managed_account_collection_tests;
+#[cfg(test)]
+mod managed_account_collection_tests;
- mod performance_tests;
+#[cfg(test)]
+mod performance_tests;
- mod special_transaction_tests;
+#[cfg(test)]
+mod special_transaction_tests;
- mod transaction_tests;
+#[cfg(test)]
+mod transaction_tests;
- mod utxo_tests;
+#[cfg(test)]
+mod utxo_tests;
- mod wallet_tests;
+#[cfg(test)]
+mod wallet_tests;

Run to verify parent gating:

Also applies to: 8-8, 10-10, 12-12, 14-14, 16-16, 18-18, 20-20, 22-22, 24-24, 26-26, 28-28


🏁 Script executed:

#!/bin/bash
set -euo pipefail
# Expect at least one #[cfg(test)] mod tests; declaration
rg -nP -C2 '^\s*#\s*\[cfg\(test\)\]\s*\n\s*mod\s+tests\b' key-wallet/src || {
  echo "Missing #[cfg(test)] for top-level tests module in key-wallet/src"; exit 1;
}

Length of output: 244


Add #[cfg(test)] to gate the top‐level tests module
In key-wallet/src/lib.rs, annotate the mod tests; declaration so none of the tests/ tree compiles in non‐test builds:

- mod tests;
+ #[cfg(test)]
+ mod tests;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In key-wallet/src/lib.rs around line 6, the top-level tests module is
unconditionally included so the entire tests/ tree compiles in non-test builds;
add the attribute #[cfg(test)] immediately above the mod tests; declaration to
gate the tests so they are only compiled for test builds (ensure there is no
other attribute on the same line and keep the attribute directly above the mod
line).

Comment on lines +245 to +259
impl zeroize::Zeroize for RootExtendedPubKey {
fn zeroize(&mut self) {
// Replace the public key with a dummy value (generator point G)
// This is a best-effort zeroization since PublicKey doesn't implement Zeroize
self.root_public_key = secp256k1::PublicKey::from_slice(&[
0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce,
0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81,
0x5b, 0x16, 0xf8, 0x17, 0x98,
])
.expect("hardcoded generator point should be valid");

// Zeroize the chain code
self.root_chain_code.zeroize();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Zeroize must not panic; replace expect() and use UFCS for chain code.
Panic in zeroize is a foot-gun and clippy-unfriendly. Use a non-panicking fallback and UFCS for Zeroize.

-        self.root_public_key = secp256k1::PublicKey::from_slice(&[
-            0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce,
-            0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81,
-            0x5b, 0x16, 0xf8, 0x17, 0x98,
-        ])
-        .expect("hardcoded generator point should be valid");
+        if let Ok(pk) = secp256k1::PublicKey::from_slice(&[
+            0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, 0xce,
+            0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, 0xf2, 0x81,
+            0x5b, 0x16, 0xf8, 0x17, 0x98,
+        ]) {
+            self.root_public_key = pk;
+        } else {
+            debug_assert!(false, "hardcoded generator point should be valid");
+        }
@@
-        self.root_chain_code.zeroize();
+        zeroize::Zeroize::zeroize(&mut self.root_chain_code);

Optional (outside range) for consistency:

impl zeroize::ZeroizeOnDrop for RootExtendedPubKey {}
🤖 Prompt for AI Agents
In key-wallet/src/wallet/root_extended_keys.rs around lines 245 to 259, remove
the panic-causing expect() and instead perform a non-panicking match on
secp256k1::PublicKey::from_slice(...)—assign the parsed public key when Ok(...)
and silently skip replacement (or leave the existing value) on Err(...) to avoid
panics; also call the chain code zeroization via UFCS as
zeroize::Zeroize::zeroize(&mut self.root_chain_code); (optionally add impl
zeroize::ZeroizeOnDrop for RootExtendedPubKey {} elsewhere for consistency).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

500-512: Skip IdentityTopUp in special_account_types conversion
In FFIWalletAccountCreationOptions.to_wallet_options (key-wallet-ffi/src/types.rs:500–512), mapping special_account_types always uses to_account_type(0), so FFIAccountType::IdentityTopUp is forced to registration_index = 0 instead of using the supplied topup_indices. Add a guard to skip it here and rely on topup_indices for identity top-up accounts:

-for &ffi_type in slice {
-    accounts.push(ffi_type.to_account_type(0));
-}
+for &ffi_type in slice {
+    // Skip IdentityTopUp here—use `topup_indices` for registration indices
+    if matches!(ffi_type, FFIAccountType::IdentityTopUp) {
+        continue;
+    }
+    accounts.push(ffi_type.to_account_type(0));
+}

Alternatively, change to_wallet_options to return a Result and error on IdentityTopUp in special_account_types.

♻️ Duplicate comments (1)
key-wallet-ffi/src/types.rs (1)

241-244: Docstring is stale: function no longer returns Option.

Update to reflect the new signature and semantics.

-    /// Convert to AccountType with optional indices
-    /// Returns None if required parameters are missing (e.g., registration_index for IdentityTopUp)
+    /// Convert to key_wallet::AccountType using the provided `index` where applicable.
+    /// For IdentityTopUp, `index` is interpreted as the `registration_index`.
🧹 Nitpick comments (1)
key-wallet-ffi/src/types.rs (1)

66-76: Use a typed error for TryFrom to align with error-guidelines.

Replace &'static str with a thiserror-backed type for consistency and better interop.

Apply this diff here:

 impl TryFrom<FFINetwork> for Network {
-    type Error = &'static str;
+    type Error = FFINetworkError;
@@
-            Some(network) => Ok(network),
-            None => Err("FFINetwork must represent exactly one network"),
+            Some(network) => Ok(network),
+            None => Err(FFINetworkError::MultipleOrNone),

And add (outside this hunk):

#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
pub enum FFINetworkError {
    #[error("FFINetwork must represent exactly one network")]
    MultipleOrNone,
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 12bdb50 and c9a0d88.

📒 Files selected for processing (2)
  • key-wallet-ffi/src/lib.rs (1 hunks)
  • key-wallet-ffi/src/types.rs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • key-wallet-ffi/src/lib.rs
🧰 Additional context used
📓 Path-based instructions (4)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use proper error types with the thiserror crate and propagate errors appropriately
Use the tokio runtime for async operations
Adhere to MSRV: code must compile on Rust 1.89 (avoid newer language/features)
Keep code formatted with rustfmt (cargo fmt)
Keep the codebase clippy-clean (clippy with -D warnings)

Files:

  • key-wallet-ffi/src/types.rs
{dash-network-ffi/**/*.rs,dash-spv-ffi/**/*.rs,key-wallet-ffi/**/*.rs,swift-dash-core-sdk/**/*.{swift,m,mm,c,h}}

📄 CodeRabbit inference engine (CLAUDE.md)

Be especially careful with memory safety at FFI boundaries

Files:

  • key-wallet-ffi/src/types.rs
{**/*.rs,**/Cargo.toml}

📄 CodeRabbit inference engine (CLAUDE.md)

Use feature flags and conditional compilation for optional features

Files:

  • key-wallet-ffi/src/types.rs
{key-wallet/**/*.rs,key-wallet-ffi/**/*.rs,swift-dash-core-sdk/**/*.swift}

📄 CodeRabbit inference engine (CLAUDE.md)

{key-wallet/**/*.rs,key-wallet-ffi/**/*.rs,swift-dash-core-sdk/**/*.swift}: Use secure random number generation for keys
Never log or expose private keys

Files:

  • key-wallet-ffi/src/types.rs
🧠 Learnings (3)
📓 Common learnings
Learnt from: QuantumExplorer
PR: dashpay/rust-dashcore#108
File: key-wallet-ffi/src/wallet_manager.rs:270-318
Timestamp: 2025-08-21T05:01:58.949Z
Learning: In the key-wallet-ffi design, wallets retrieved from the wallet manager via lookup functions should return const pointers (*const FFIWallet) to enforce read-only access and prevent unintended modifications. The wallet manager should control wallet lifecycle and mutations through specific APIs rather than allowing external mutation of retrieved wallet references.
📚 Learning: 2025-08-21T05:01:58.949Z
Learnt from: QuantumExplorer
PR: dashpay/rust-dashcore#108
File: key-wallet-ffi/src/wallet_manager.rs:270-318
Timestamp: 2025-08-21T05:01:58.949Z
Learning: In the key-wallet-ffi design, wallets retrieved from the wallet manager via lookup functions should return const pointers (*const FFIWallet) to enforce read-only access and prevent unintended modifications. The wallet manager should control wallet lifecycle and mutations through specific APIs rather than allowing external mutation of retrieved wallet references.

Applied to files:

  • key-wallet-ffi/src/types.rs
📚 Learning: 2025-08-16T04:14:07.718Z
Learnt from: CR
PR: dashpay/rust-dashcore#0
File: dash-spv-ffi/CLAUDE.md:0-0
Timestamp: 2025-08-16T04:14:07.718Z
Learning: Applies to dash-spv-ffi/src/**/*.rs : Expose complex Rust types to C as opaque pointers (e.g., FFIDashSpvClient*)

Applied to files:

  • key-wallet-ffi/src/types.rs
🧬 Code graph analysis (1)
key-wallet-ffi/src/types.rs (4)
key-wallet/src/account/mod.rs (4)
  • new (72-79)
  • network (153-155)
  • index (117-119)
  • account_type (149-151)
key-wallet/src/managed_account/managed_account_trait.rs (4)
  • network (24-24)
  • balance (36-36)
  • index (60-62)
  • account_type (18-18)
key-wallet/src/account/account_type.rs (2)
  • from (69-94)
  • index (100-121)
key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs (4)
  • balance (69-69)
  • balance (194-196)
  • accounts (81-81)
  • accounts (241-243)
🪛 GitHub Actions: Continuous integration
key-wallet-ffi/src/types.rs

[error] 102-102: Command 'cargo fmt --all -- --check' failed due to formatting differences. See diff at key-wallet-ffi/src/types.rs:102. Run 'cargo fmt' to fix formatting.

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: fuzz (hashes_sha256)
🔇 Additional comments (2)
key-wallet-ffi/src/types.rs (2)

24-29: Defaulting “no networks” to Testnet may be surprising—confirm intent.

Zero/NoNetworks → Testnet can mask caller errors. Consider returning an empty set or requiring an explicit network at call sites.

Would you like me to switch to returning an empty Vec to surface misconfiguration?

-            // If no networks specified, default to testnet
-            return vec![Network::Testnet];
+            // No networks specified -> return empty set to let callers decide defaults
+            return Vec::new();

91-116: Apply Rust formatting locally
Rust formatting couldn’t be verified in this environment. Run cargo fmt --all -- --check, fix any reported issues, and commit the changes to unblock CI.

Comment on lines 7 to 17
/// FFI Network type (bit flags for multiple networks)
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub enum FFINetwork {
Dash = 0,
Testnet = 1,
Regtest = 2,
Devnet = 3,
NoNetworks = 0,
Dash = 1,
Testnet = 2,
Regtest = 4,
Devnet = 8,
AllNetworks = 15, // Dash | Testnet | Regtest | Devnet
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Stabilize FFI layout for FFINetwork and avoid c_uint casts.

Use a fixed integer representation for the bitflags and operate on u32 throughout. This removes implicit signedness/width assumptions about C enums and tightens FFI stability.

Apply this diff:

-/// FFI Network type (bit flags for multiple networks)
-#[repr(C)]
+/// FFI Network type (bit flags for multiple networks)
+#[repr(u32)]
 #[derive(Debug, Clone, Copy)]
 pub enum FFINetwork {
@@
-    pub fn parse_networks(&self) -> Vec<Network> {
-        let flags = *self as c_uint;
+    pub fn parse_networks(&self) -> Vec<Network> {
+        let flags = *self as u32;
@@
-        if flags == FFINetwork::NoNetworks as c_uint || flags == 0 {
+        if flags == FFINetwork::NoNetworks as u32 || flags == 0 {
@@
-        if flags & (FFINetwork::Dash as c_uint) != 0 {
+        if flags & (FFINetwork::Dash as u32) != 0 {
@@
-        if flags & (FFINetwork::Testnet as c_uint) != 0 {
+        if flags & (FFINetwork::Testnet as u32) != 0 {
@@
-        if flags & (FFINetwork::Regtest as c_uint) != 0 {
+        if flags & (FFINetwork::Regtest as u32) != 0 {
@@
-        if flags & (FFINetwork::Devnet as c_uint) != 0 {
+        if flags & (FFINetwork::Devnet as u32) != 0 {
@@
-    pub fn try_into_single_network(&self) -> Option<Network> {
-        let flags = *self as c_uint;
+    pub fn try_into_single_network(&self) -> Option<Network> {
+        let flags = *self as u32;
@@
-            x if x == FFINetwork::Dash as c_uint => Some(Network::Dash),
-            x if x == FFINetwork::Testnet as c_uint => Some(Network::Testnet),
-            x if x == FFINetwork::Regtest as c_uint => Some(Network::Regtest),
-            x if x == FFINetwork::Devnet as c_uint => Some(Network::Devnet),
+            x if x == FFINetwork::Dash as u32 => Some(Network::Dash),
+            x if x == FFINetwork::Testnet as u32 => Some(Network::Testnet),
+            x if x == FFINetwork::Regtest as u32 => Some(Network::Regtest),
+            x if x == FFINetwork::Devnet as u32 => Some(Network::Devnet),

Also applies to: 19-47, 49-64

Comment on lines 91 to 116
/// FFI Balance type for representing wallet balances
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct FFIBalance {
/// Confirmed balance in satoshis
pub confirmed: u64,
/// Unconfirmed balance in satoshis
pub unconfirmed: u64,
/// Immature balance in satoshis (e.g., mining rewards)
pub immature: u64,
/// Total balance (confirmed + unconfirmed) in satoshis
pub total: u64,
}


impl From<key_wallet::WalletBalance> for FFIBalance {
fn from(balance: key_wallet::WalletBalance) -> Self {
FFIBalance {
confirmed: balance.confirmed,
unconfirmed: balance.unconfirmed,
immature: balance.locked, // Map locked to immature for now
total: balance.total,
}
}
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

FFIBalance docs and semantics are inconsistent with the mapping.

Field ‘immature’ is sourced from WalletBalance::locked; the docs claim “immature (e.g., mining rewards)” and ‘total’ is documented as “confirmed + unconfirmed”, which likely omits locked. This is misleading at the FFI boundary.

Apply this doc-only fix now; consider renaming the field to locked in a future breaking change.

-/// FFI Balance type for representing wallet balances
+/// FFI Balance type for representing wallet balances
@@
-    /// Immature balance in satoshis (e.g., mining rewards)
+    /// Locked balance in satoshis (temporarily unspendable; mapped from WalletBalance::locked)
     pub immature: u64,
@@
-    /// Total balance (confirmed + unconfirmed) in satoshis
+    /// Total balance in satoshis (as reported by WalletBalance::total)
     pub total: u64,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// FFI Balance type for representing wallet balances
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct FFIBalance {
/// Confirmed balance in satoshis
pub confirmed: u64,
/// Unconfirmed balance in satoshis
pub unconfirmed: u64,
/// Immature balance in satoshis (e.g., mining rewards)
pub immature: u64,
/// Total balance (confirmed + unconfirmed) in satoshis
pub total: u64,
}
impl From<key_wallet::WalletBalance> for FFIBalance {
fn from(balance: key_wallet::WalletBalance) -> Self {
FFIBalance {
confirmed: balance.confirmed,
unconfirmed: balance.unconfirmed,
immature: balance.locked, // Map locked to immature for now
total: balance.total,
}
}
}
/// FFI Balance type for representing wallet balances
#[repr(C)]
#[derive(Debug, Clone, Copy, Default)]
pub struct FFIBalance {
/// Confirmed balance in satoshis
pub confirmed: u64,
/// Unconfirmed balance in satoshis
pub unconfirmed: u64,
/// Locked balance in satoshis (temporarily unspendable; mapped from WalletBalance::locked)
pub immature: u64,
/// Total balance in satoshis (as reported by WalletBalance::total)
pub total: u64,
}
🧰 Tools
🪛 GitHub Actions: Continuous integration

[error] 102-102: Command 'cargo fmt --all -- --check' failed due to formatting differences. See diff at key-wallet-ffi/src/types.rs:102. Run 'cargo fmt' to fix formatting.

🤖 Prompt for AI Agents
In key-wallet-ffi/src/types.rs around lines 91 to 116, the FFIBalance field docs
are inconsistent with the mapping: `immature` is actually populated from
WalletBalance::locked and `total` currently documents "confirmed + unconfirmed"
while the struct includes locked; update the field comments to accurately
reflect the runtime mapping (e.g., change the `immature` comment to indicate it
represents locked funds from the wallet and change the `total` comment to state
it includes confirmed, unconfirmed, and locked/immature funds), and add a short
note that this is a mapping choice at the FFI boundary (and consider renaming
the field to `locked` in a future breaking change).

Copy link
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: 12

Caution

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

⚠️ Outside diff range comments (5)
key-wallet-ffi/src/types.rs (2)

79-89: Align unknown-network fallback with project preference (Testnet).

Current default maps unknown variants to Dash; prior guidance prefers defaulting to Testnet for unknown networks.

-            _ => FFINetwork::Dash, // Default to Dash for unknown networks
+            _ => FFINetwork::Testnet, // Default to Testnet for unknown/unsupported variants

487-499: IdentityTopUp leakage via special_account_types (index forced to 0).

SpecificAccounts pushes every FFIAccountType via to_account_type(0), which creates IdentityTopUp { registration_index: 0 } when requested here, duplicating/conflicting with topup_indices. Filter out IdentityTopUp (indexed type) from special_account_types.

-                    for &ffi_type in slice {
-                        accounts.push(ffi_type.to_account_type(0));
-                    }
+                    for &ffi_type in slice {
+                        // Exclude indexed types; IdentityTopUp must be specified via `topup_indices`
+                        if !matches!(ffi_type, FFIAccountType::IdentityTopUp) {
+                            accounts.push(ffi_type.to_account_type(0));
+                        }
+                    }
key-wallet-manager/src/wallet_manager/mod.rs (3)

801-807: Same bug for change addresses.

Propagate mark_as_used to next_change_address.

- .next_change_address(Some(&wallet_account.account_xpub), true)
+ .next_change_address(Some(&wallet_account.account_xpub), mark_as_used)

Also applies to: 832-843, 857-865, 873-883, 898-906


156-165: Avoid unnecessary clone when inserting wallet.

No need to clone wallet before insertion.

- let wallet_mut = wallet.clone();
- self.wallets.insert(wallet_id, wallet_mut);
+ self.wallets.insert(wallet_id, wallet);

634-639: Replace hardcoded true with mark_as_used in all next_receive_address calls
In key-wallet-manager/src/wallet_manager/mod.rs (lines 634–639, 666–676, 691–699, 706–718) you’re always passing true to next_receive_address, ignoring the mark_as_used parameter and causing duplicate marking logic. Update each

- .next_receive_address(Some(&wallet_account.account_xpub), true)
+ .next_receive_address(Some(&wallet_account.account_xpub), mark_as_used)

Additionally, ripgrep found four occurrences of .next_receive_address(None, true) in key-wallet/src/transaction_checking/transaction_router/tests/provider.rs (lines 162, 298, 435, 634). Decide whether those tests should be updated to use a test‐scoped mark_as_used flag or remain hardcoded for isolation.

♻️ Duplicate comments (3)
key-wallet-ffi/src/types.rs (3)

91-104: Docs don’t match runtime mapping for FFIBalance.

immature is mapped from WalletBalance::locked; total is WalletBalance::total (not confirmed + unconfirmed).

-    /// Immature balance in satoshis (e.g., mining rewards)
+    /// Locked balance in satoshis (temporarily unspendable; mapped from WalletBalance::locked)
@@
-    /// Total balance (confirmed + unconfirmed) in satoshis
+    /// Total balance in satoshis (as reported by WalletBalance::total)

227-230: Docstring is stale: function does not return Option.

Update to reflect the actual signature and semantics.

-    /// Convert to AccountType with optional indices
-    /// Returns None if required parameters are missing (e.g., registration_index for IdentityTopUp)
+    /// Convert to AccountType using the provided index where applicable.
+    /// For IdentityTopUp, the index is used as registration_index.

7-17: Stabilize FFINetwork layout and stop relying on c_uint casts.

Use a fixed underlying type for bitflags and operate on u32. This avoids ABI ambiguity with C enums and keeps the bit math explicit and portable.

-/// FFI Network type (bit flags for multiple networks)
-#[repr(C)]
+/// FFI Network type (bit flags for multiple networks)
+#[repr(u32)]
 #[derive(Debug, Clone, Copy, PartialEq)]
 pub enum FFINetwork {
     NoNetworks = 0,
     Dash = 1,
     Testnet = 2,
     Regtest = 4,
     Devnet = 8,
     AllNetworks = 15, // Dash | Testnet | Regtest | Devnet
 }
@@
-    pub fn parse_networks(&self) -> Vec<Network> {
-        let flags = *self as c_uint;
+    pub fn parse_networks(&self) -> Vec<Network> {
+        let flags = *self as u32;
@@
-        if flags == FFINetwork::NoNetworks as c_uint || flags == 0 {
+        if flags == FFINetwork::NoNetworks as u32 || flags == 0 {
@@
-        if flags & (FFINetwork::Dash as c_uint) != 0 {
+        if flags & (FFINetwork::Dash as u32) != 0 {
@@
-        if flags & (FFINetwork::Testnet as c_uint) != 0 {
+        if flags & (FFINetwork::Testnet as u32) != 0 {
@@
-        if flags & (FFINetwork::Regtest as c_uint) != 0 {
+        if flags & (FFINetwork::Regtest as u32) != 0 {
@@
-        if flags & (FFINetwork::Devnet as c_uint) != 0 {
+        if flags & (FFINetwork::Devnet as u32) != 0 {
@@
-    pub fn try_into_single_network(&self) -> Option<Network> {
-        let flags = *self as c_uint;
+    pub fn try_into_single_network(&self) -> Option<Network> {
+        let flags = *self as u32;
@@
-            x if x == FFINetwork::Dash as c_uint => Some(Network::Dash),
-            x if x == FFINetwork::Testnet as c_uint => Some(Network::Testnet),
-            x if x == FFINetwork::Regtest as c_uint => Some(Network::Regtest),
-            x if x == FFINetwork::Devnet as c_uint => Some(Network::Devnet),
+            x if x == FFINetwork::Dash as u32 => Some(Network::Dash),
+            x if x == FFINetwork::Testnet as u32 => Some(Network::Testnet),
+            x if x == FFINetwork::Regtest as u32 => Some(Network::Regtest),
+            x if x == FFINetwork::Devnet as u32 => Some(Network::Devnet),

Also applies to: 19-47

🧹 Nitpick comments (15)
key-wallet-ffi/src/types.rs (1)

66-76: TryFrom error message is okay; consider including the offending flags.

Optional: include numeric flags to aid debugging across FFI boundaries.

key-wallet-ffi/src/account.rs (2)

303-322: Duplicate logic across account_get_account_type variants.

Minor: factor into a shared helper to reduce drift, e.g., fn account_type_with_index(at: &key_wallet::AccountType) -> (FFIAccountType, c_uint).

Also applies to: 418-438, 535-555


582-620: Counting accounts: avoid bool-to-usize casts to keep clippy clean.

clippy -D warnings may flag is_some() as usize. Use explicit addition.

-            let count = accounts.standard_bip44_accounts.len()
-                + accounts.standard_bip32_accounts.len()
-                + accounts.coinjoin_accounts.len()
-                + accounts.identity_registration.is_some() as usize
-                + accounts.identity_topup.len();
+            let mut count = accounts.standard_bip44_accounts.len()
+                + accounts.standard_bip32_accounts.len()
+                + accounts.coinjoin_accounts.len()
+                + accounts.identity_topup.len();
+            if accounts.identity_registration.is_some() { count += 1; }
key-wallet-ffi/src/managed_account_collection.rs (1)

90-145: Good: Retrieves managed wallet info, clones collection, and frees wallet info.

Consider setting error to success explicitly on success to make the contract obvious.

key-wallet-ffi/src/account_tests.rs (1)

195-200: Consider using assertions helper for xpub validation.

The xpub validation could be more robust by checking the full format beyond just the prefix.

Consider adding a helper function to validate xpub format more thoroughly:

-                assert!(xpub_string.starts_with("tpub")); // Testnet xpub should start with tpub
+                assert!(xpub_string.starts_with("tpub"), "Expected testnet xpub to start with 'tpub', got: {}", xpub_string);
+                assert!(xpub_string.len() > 100, "xpub string seems too short: {}", xpub_string.len());
key-wallet-ffi/tests/test_managed_account_collection.rs (1)

15-16: Consider extracting test constants to a shared test utilities module.

The TEST_MNEMONIC constant is duplicated across multiple test files. Consider creating a shared test utilities module to avoid duplication and ensure consistency.

Create a shared test utilities module:

// In tests/common/mod.rs or similar
pub const TEST_MNEMONIC: &str = 
    "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
    
pub const TEST_PASSPHRASE: &str = "";

Then import it in test files:

-const TEST_MNEMONIC: &str =
-    "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
+mod common;
+use common::TEST_MNEMONIC;
key-wallet-manager/tests/integration_test.rs (2)

143-152: UTXO management tests could be expanded.

The test acknowledges that UTXOs are created by processing transactions but doesn't actually test this flow. Consider adding a test that processes a transaction to create UTXOs.

Would you like me to generate a test that processes a transaction to create UTXOs and verify the balance updates correctly?


86-105: Document InvalidNetwork behavior
In key-wallet-manager/src/wallet_manager/mod.rs, expand the /// Invalid network doc comment (and update the crate README or API docs) to note that WalletError::InvalidNetwork is the expected result when account collections haven’t been initialized for a given network.

key-wallet-manager/examples/wallet_creation.rs (1)

160-169: Minor: iterate wallet IDs by reference to avoid copy.

WalletId is a [u8; 32]. Iterating by value copies; it’s negligible but you can iterate by reference explicitly for clarity.

Apply:

- for (i, wallet_id) in [wallet_id, wallet_id2].iter().enumerate() {
-     match manager.get_wallet_balance(wallet_id) {
+ for (i, wid) in [wallet_id, wallet_id2].iter().enumerate() {
+     match manager.get_wallet_balance(wid) {
key-wallet-ffi/src/wallet_manager.rs (3)

205-219: Document sensitive output semantics.

When downgrade_to_pubkey_wallet is false, serialized bytes may include private key material. Callers must store/transport securely and zeroize after use.

 /// - The caller must free the returned wallet_bytes using wallet_manager_free_wallet_bytes()
+/// - SECURITY: If `downgrade_to_pubkey_wallet` is false, `wallet_bytes_out` contains private keys.
+///   Callers must handle it securely and zeroize the buffer after use by calling
+///   `wallet_manager_free_wallet_bytes` (which zeroizes before free).

478-483: Add freeing guidance to docs for wallet IDs buffer.

Make it explicit that callers must free with wallet_manager_free_wallet_ids.

 /// - `count_out` must be a valid pointer to receive the count
 /// - `error` must be a valid pointer to an FFIError structure or null
 /// - The caller must ensure all pointers remain valid for the duration of this call
+/// - The returned buffer must be freed with `wallet_manager_free_wallet_ids`

1011-1018: Mention how to free returned address arrays.

The function allocates an array of C strings; callers should free via wallet_manager_free_addresses.

 /// - `error` must be a valid pointer to an FFIError structure or null
 /// - The caller must ensure all pointers remain valid for the duration of this call
+/// - The returned array must be freed with `wallet_manager_free_addresses`
key-wallet-manager/src/wallet_manager/mod.rs (1)

1067-1091: Derive errors with thiserror for better interop.

Adopt thiserror to provide Display and source automatically, keeping MSRV-compatible.

-#[derive(Debug)]
-pub enum WalletError {
+use thiserror::Error;
+#[derive(Error, Debug)]
+pub enum WalletError {
-    WalletCreation(String),
+    #[error("Wallet creation failed: {0}")]
+    WalletCreation(String),
-    WalletNotFound(WalletId),
+    #[error("Wallet not found: {0:x?}")]
+    WalletNotFound(WalletId),
-    WalletExists(WalletId),
+    #[error("Wallet already exists: {0:x?}")]
+    WalletExists(WalletId),
-    InvalidMnemonic(String),
+    #[error("Invalid mnemonic: {0}")]
+    InvalidMnemonic(String),
    ...
}
key-wallet-ffi/src/account_collection.rs (2)

1521-1522: Fix misleading test comment.

It says “AllAccounts” but the test uses SpecificAccounts. Update for clarity.

-            // With AllAccounts but empty lists, collection should still exist
+            // With SpecificAccounts and empty lists, collection may be absent or empty

145-161: Optional: consider consistent error reporting for not-found cases.

Today these return null without setting FFIError. Exposing an optional FFIError* out-param (mirroring wallet_get_account_collection) would help callers distinguish “not found” vs misuse.

Happy to sketch a minimal pattern you can reuse across these getters if you want it in this PR.

Also applies to: 210-226, 275-291, 339-354, 380-396, 442-457, 483-498, 526-541, 567-582

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between c9a0d88 and 9fde1f3.

📒 Files selected for processing (19)
  • dash-spv/src/main.rs (1 hunks)
  • key-wallet-ffi/Cargo.toml (2 hunks)
  • key-wallet-ffi/IMPORT_WALLET_FFI.md (1 hunks)
  • key-wallet-ffi/src/account.rs (6 hunks)
  • key-wallet-ffi/src/account_collection.rs (1 hunks)
  • key-wallet-ffi/src/account_tests.rs (5 hunks)
  • key-wallet-ffi/src/address_pool.rs (11 hunks)
  • key-wallet-ffi/src/lib.rs (2 hunks)
  • key-wallet-ffi/src/managed_account.rs (1 hunks)
  • key-wallet-ffi/src/managed_account_collection.rs (1 hunks)
  • key-wallet-ffi/src/types.rs (8 hunks)
  • key-wallet-ffi/src/wallet_manager.rs (10 hunks)
  • key-wallet-ffi/tests/test_account_collection.rs (1 hunks)
  • key-wallet-ffi/tests/test_managed_account_collection.rs (1 hunks)
  • key-wallet-manager/examples/wallet_creation.rs (3 hunks)
  • key-wallet-manager/src/wallet_manager/mod.rs (5 hunks)
  • key-wallet-manager/tests/integration_test.rs (5 hunks)
  • key-wallet-manager/tests/test_serialized_wallets.rs (1 hunks)
  • key-wallet/src/managed_account/mod.rs (3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • key-wallet-manager/tests/test_serialized_wallets.rs
  • key-wallet-ffi/Cargo.toml
  • dash-spv/src/main.rs
🧰 Additional context used
📓 Path-based instructions (5)
**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.rs: Use proper error types with the thiserror crate and propagate errors appropriately
Use the tokio runtime for async operations
Adhere to MSRV: code must compile on Rust 1.89 (avoid newer language/features)
Keep code formatted with rustfmt (cargo fmt)
Keep the codebase clippy-clean (clippy with -D warnings)

Files:

  • key-wallet-ffi/tests/test_account_collection.rs
  • key-wallet-ffi/tests/test_managed_account_collection.rs
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-ffi/src/lib.rs
  • key-wallet-manager/tests/integration_test.rs
  • key-wallet-ffi/src/account_tests.rs
  • key-wallet/src/managed_account/mod.rs
  • key-wallet-ffi/src/address_pool.rs
  • key-wallet-ffi/src/managed_account_collection.rs
  • key-wallet-ffi/src/account_collection.rs
  • key-wallet-ffi/src/account.rs
  • key-wallet-manager/src/wallet_manager/mod.rs
  • key-wallet-ffi/src/wallet_manager.rs
  • key-wallet-manager/examples/wallet_creation.rs
  • key-wallet-ffi/src/types.rs
{dash-network-ffi/**/*.rs,dash-spv-ffi/**/*.rs,key-wallet-ffi/**/*.rs,swift-dash-core-sdk/**/*.{swift,m,mm,c,h}}

📄 CodeRabbit inference engine (CLAUDE.md)

Be especially careful with memory safety at FFI boundaries

Files:

  • key-wallet-ffi/tests/test_account_collection.rs
  • key-wallet-ffi/tests/test_managed_account_collection.rs
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-ffi/src/lib.rs
  • key-wallet-ffi/src/account_tests.rs
  • key-wallet-ffi/src/address_pool.rs
  • key-wallet-ffi/src/managed_account_collection.rs
  • key-wallet-ffi/src/account_collection.rs
  • key-wallet-ffi/src/account.rs
  • key-wallet-ffi/src/wallet_manager.rs
  • key-wallet-ffi/src/types.rs
{**/*.rs,**/Cargo.toml}

📄 CodeRabbit inference engine (CLAUDE.md)

Use feature flags and conditional compilation for optional features

Files:

  • key-wallet-ffi/tests/test_account_collection.rs
  • key-wallet-ffi/tests/test_managed_account_collection.rs
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-ffi/src/lib.rs
  • key-wallet-manager/tests/integration_test.rs
  • key-wallet-ffi/src/account_tests.rs
  • key-wallet/src/managed_account/mod.rs
  • key-wallet-ffi/src/address_pool.rs
  • key-wallet-ffi/src/managed_account_collection.rs
  • key-wallet-ffi/src/account_collection.rs
  • key-wallet-ffi/src/account.rs
  • key-wallet-manager/src/wallet_manager/mod.rs
  • key-wallet-ffi/src/wallet_manager.rs
  • key-wallet-manager/examples/wallet_creation.rs
  • key-wallet-ffi/src/types.rs
**/tests/**/*.rs

📄 CodeRabbit inference engine (CLAUDE.md)

**/tests/**/*.rs: Write unit tests for new functionality
Use proptest for property-based testing where appropriate

Files:

  • key-wallet-ffi/tests/test_account_collection.rs
  • key-wallet-ffi/tests/test_managed_account_collection.rs
  • key-wallet-manager/tests/integration_test.rs
{key-wallet/**/*.rs,key-wallet-ffi/**/*.rs,swift-dash-core-sdk/**/*.swift}

📄 CodeRabbit inference engine (CLAUDE.md)

{key-wallet/**/*.rs,key-wallet-ffi/**/*.rs,swift-dash-core-sdk/**/*.swift}: Use secure random number generation for keys
Never log or expose private keys

Files:

  • key-wallet-ffi/tests/test_account_collection.rs
  • key-wallet-ffi/tests/test_managed_account_collection.rs
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-ffi/src/lib.rs
  • key-wallet-ffi/src/account_tests.rs
  • key-wallet/src/managed_account/mod.rs
  • key-wallet-ffi/src/address_pool.rs
  • key-wallet-ffi/src/managed_account_collection.rs
  • key-wallet-ffi/src/account_collection.rs
  • key-wallet-ffi/src/account.rs
  • key-wallet-ffi/src/wallet_manager.rs
  • key-wallet-ffi/src/types.rs
🧠 Learnings (7)
📓 Common learnings
Learnt from: QuantumExplorer
PR: dashpay/rust-dashcore#108
File: key-wallet-ffi/src/wallet_manager.rs:270-318
Timestamp: 2025-08-21T05:01:58.949Z
Learning: In the key-wallet-ffi design, wallets retrieved from the wallet manager via lookup functions should return const pointers (*const FFIWallet) to enforce read-only access and prevent unintended modifications. The wallet manager should control wallet lifecycle and mutations through specific APIs rather than allowing external mutation of retrieved wallet references.
📚 Learning: 2025-08-16T04:14:07.718Z
Learnt from: CR
PR: dashpay/rust-dashcore#0
File: dash-spv-ffi/CLAUDE.md:0-0
Timestamp: 2025-08-16T04:14:07.718Z
Learning: Applies to dash-spv-ffi/tests/unit/**/*.rs : Add a corresponding Rust unit test for each new FFI function

Applied to files:

  • key-wallet-ffi/tests/test_account_collection.rs
  • key-wallet-ffi/tests/test_managed_account_collection.rs
  • key-wallet-ffi/src/account_tests.rs
  • key-wallet-ffi/src/wallet_manager.rs
📚 Learning: 2025-08-21T05:01:58.949Z
Learnt from: QuantumExplorer
PR: dashpay/rust-dashcore#108
File: key-wallet-ffi/src/wallet_manager.rs:270-318
Timestamp: 2025-08-21T05:01:58.949Z
Learning: In the key-wallet-ffi design, wallets retrieved from the wallet manager via lookup functions should return const pointers (*const FFIWallet) to enforce read-only access and prevent unintended modifications. The wallet manager should control wallet lifecycle and mutations through specific APIs rather than allowing external mutation of retrieved wallet references.

Applied to files:

  • key-wallet-ffi/IMPORT_WALLET_FFI.md
  • key-wallet-ffi/src/managed_account.rs
  • key-wallet-ffi/src/managed_account_collection.rs
  • key-wallet-ffi/src/account.rs
  • key-wallet-manager/src/wallet_manager/mod.rs
  • key-wallet-ffi/src/wallet_manager.rs
  • key-wallet-ffi/src/types.rs
📚 Learning: 2025-08-16T04:14:07.718Z
Learnt from: CR
PR: dashpay/rust-dashcore#0
File: dash-spv-ffi/CLAUDE.md:0-0
Timestamp: 2025-08-16T04:14:07.718Z
Learning: Applies to dash-spv-ffi/src/**/*.rs : Expose complex Rust types to C as opaque pointers (e.g., FFIDashSpvClient*)

Applied to files:

  • key-wallet-ffi/src/lib.rs
  • key-wallet-ffi/src/types.rs
📚 Learning: 2025-08-25T17:15:15.391Z
Learnt from: CR
PR: dashpay/rust-dashcore#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-25T17:15:15.391Z
Learning: Applies to {dash-network-ffi/**/*.rs,dash-spv-ffi/**/*.rs,key-wallet-ffi/**/*.rs,swift-dash-core-sdk/**/*.{swift,m,mm,c,h}} : Be especially careful with memory safety at FFI boundaries

Applied to files:

  • key-wallet-ffi/src/lib.rs
  • key-wallet-ffi/src/types.rs
📚 Learning: 2025-08-25T17:15:15.391Z
Learnt from: CR
PR: dashpay/rust-dashcore#0
File: CLAUDE.md:0-0
Timestamp: 2025-08-25T17:15:15.391Z
Learning: Applies to {dash-network,dash-spv}/tests/**/*.rs : Provide integration tests for network operations

Applied to files:

  • key-wallet-ffi/src/account_tests.rs
📚 Learning: 2025-06-15T15:31:44.136Z
Learnt from: QuantumExplorer
PR: dashpay/rust-dashcore#74
File: key-wallet/src/address.rs:30-40
Timestamp: 2025-06-15T15:31:44.136Z
Learning: In the key-wallet crate, QuantumExplorer prefers keeping wildcard arms that default unknown Network variants to testnet prefixes rather than using exhaustive matches or panics. This fallback behavior is intentional.

Applied to files:

  • key-wallet-ffi/src/account_tests.rs
  • key-wallet-ffi/src/address_pool.rs
  • key-wallet-ffi/src/types.rs
🧬 Code graph analysis (12)
key-wallet-ffi/tests/test_account_collection.rs (1)
key-wallet-ffi/src/wallet.rs (2)
  • wallet_create_from_mnemonic_with_options (29-131)
  • wallet_free (443-449)
key-wallet-ffi/tests/test_managed_account_collection.rs (5)
key-wallet-ffi/src/managed_account.rs (4)
  • error (60-70)
  • success (51-57)
  • new (26-30)
  • managed_account_free (476-480)
key-wallet-ffi/src/wallet_manager.rs (5)
  • wallet_manager_add_wallet_from_mnemonic_with_options (51-155)
  • wallet_manager_create (31-38)
  • wallet_manager_free (1246-1252)
  • wallet_manager_free_wallet_ids (1263-1270)
  • wallet_manager_get_wallet_ids (478-522)
key-wallet-ffi/src/managed_account_collection.rs (20)
  • new (25-31)
  • managed_wallet_get_account_collection (90-145)
  • managed_account_collection_count (782-824)
  • managed_account_collection_get_bip44_indices (201-228)
  • managed_account_collection_get_bip44_account (172-190)
  • managed_account_collection_free (154-160)
  • managed_account_collection_get_bip32_indices (268-295)
  • managed_account_collection_get_coinjoin_indices (335-361)
  • managed_account_collection_has_identity_registration (397-406)
  • managed_account_collection_has_identity_invitation (547-556)
  • managed_account_collection_has_provider_voting_keys (592-601)
  • managed_account_collection_has_provider_owner_keys (635-644)
  • managed_account_collection_get_identity_registration (373-389)
  • managed_account_collection_get_provider_voting_keys (568-584)
  • managed_account_collection_summary (837-966)
  • managed_account_collection_summary_data (980-1067)
  • managed_account_collection_summary_free (1076-1117)
  • managed_account_collection_get_bip32_account (240-257)
  • managed_account_collection_get_coinjoin_account (307-324)
  • managed_account_collection_get_identity_topup (416-433)
key-wallet-ffi/src/types.rs (1)
  • default_options (386-400)
key-wallet-ffi/src/utils.rs (1)
  • string_free (17-23)
key-wallet-ffi/src/managed_account.rs (6)
key-wallet-ffi/src/wallet_manager.rs (7)
  • wallet_manager_get_managed_wallet_info (598-644)
  • wallet_manager_get_wallet (536-584)
  • wallet_manager_add_wallet_from_mnemonic_with_options (51-155)
  • wallet_manager_create (31-38)
  • wallet_manager_free (1246-1252)
  • wallet_manager_free_wallet_ids (1263-1270)
  • wallet_manager_get_wallet_ids (478-522)
key-wallet-ffi/src/managed_wallet.rs (1)
  • managed_wallet_info_free (761-767)
key-wallet-ffi/src/error.rs (1)
  • set_success (63-69)
key-wallet-ffi/src/wallet.rs (1)
  • wallet_free_const (464-472)
key-wallet-ffi/src/address_pool.rs (1)
  • address_pool_free (216-220)
key-wallet-ffi/src/types.rs (1)
  • default_options (386-400)
key-wallet-ffi/src/lib.rs (2)
key-wallet-ffi/src/types.rs (1)
  • error (162-173)
key-wallet-ffi/src/error.rs (1)
  • error (44-49)
key-wallet-manager/tests/integration_test.rs (2)
key-wallet-manager/src/wallet_manager/mod.rs (3)
  • wallet_count (355-357)
  • new (70-76)
  • new (104-110)
key-wallet/src/wallet/managed_wallet_info/mod.rs (1)
  • new (54-64)
key-wallet-ffi/src/account_tests.rs (3)
key-wallet-ffi/src/types.rs (1)
  • error (162-173)
key-wallet/src/managed_account/mod.rs (4)
  • network (790-792)
  • index (141-143)
  • account_type (782-784)
  • is_watch_only (802-804)
key-wallet/src/account/mod.rs (4)
  • network (153-155)
  • index (117-119)
  • account_type (149-151)
  • is_watch_only (157-159)
key-wallet/src/managed_account/mod.rs (2)
key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs (2)
  • monitored_addresses (60-60)
  • monitored_addresses (162-173)
key-wallet/src/managed_account/managed_account_trait.rs (2)
  • monitored_addresses (48-48)
  • monitored_addresses_mut (51-51)
key-wallet-ffi/src/address_pool.rs (5)
key-wallet-ffi/src/error.rs (4)
  • error (44-49)
  • set_error (53-59)
  • set_success (63-69)
  • success (36-41)
key-wallet-ffi/src/utils.rs (1)
  • rust_string_to_c (26-31)
key-wallet/src/managed_account/address_pool.rs (4)
  • network (1099-1102)
  • new (356-371)
  • new (1058-1068)
  • address_info (814-816)
key-wallet-ffi/src/managed_account.rs (2)
  • managed_account_get_external_address_pool (596-620)
  • managed_wallet_get_account (86-203)
key-wallet-ffi/src/wallet_manager.rs (5)
  • wallet_manager_add_wallet_from_mnemonic_with_options (51-155)
  • wallet_manager_create (31-38)
  • wallet_manager_free (1246-1252)
  • wallet_manager_free_wallet_ids (1263-1270)
  • wallet_manager_get_wallet_ids (478-522)
key-wallet-ffi/src/managed_account_collection.rs (3)
key-wallet-ffi/src/managed_account.rs (2)
  • error (60-70)
  • new (26-30)
key-wallet-ffi/src/wallet_manager.rs (1)
  • wallet_manager_get_managed_wallet_info (598-644)
key-wallet-ffi/src/managed_wallet.rs (1)
  • managed_wallet_info_free (761-767)
key-wallet-manager/src/wallet_manager/mod.rs (5)
key-wallet/src/wallet/mod.rs (2)
  • zeroize (136-184)
  • wallet_id (115-115)
key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs (6)
  • wallet_id (30-30)
  • wallet_id (122-124)
  • from_wallet (23-23)
  • from_wallet (114-116)
  • accounts (81-81)
  • accounts (241-243)
key-wallet/src/mnemonic.rs (3)
  • from_phrase (220-227)
  • generate (106-129)
  • generate (133-144)
key-wallet/src/wallet/initialization.rs (4)
  • from_mnemonic (144-163)
  • from_mnemonic_with_passphrase (173-199)
  • from_extended_key (336-351)
  • from_xpub (229-245)
key-wallet/src/wallet/managed_wallet_info/mod.rs (1)
  • from_wallet (80-99)
key-wallet-ffi/src/wallet_manager.rs (2)
key-wallet-ffi/src/types.rs (1)
  • error (162-173)
key-wallet-ffi/src/error.rs (3)
  • error (44-49)
  • set_error (53-59)
  • set_success (63-69)
key-wallet-ffi/src/types.rs (3)
key-wallet/src/account/mod.rs (4)
  • new (72-79)
  • network (153-155)
  • index (117-119)
  • account_type (149-151)
key-wallet/src/account/account_type.rs (2)
  • from (69-94)
  • index (100-121)
key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs (2)
  • balance (69-69)
  • balance (194-196)
🪛 LanguageTool
key-wallet-ffi/IMPORT_WALLET_FFI.md

[grammar] ~21-~21: There might be a mistake here.
Context: ... Pointer to an FFIWalletManager instance - wallet_bytes: Pointer to bincode-serialized wallet b...

(QB_NEW_EN)


[grammar] ~22-~22: There might be a mistake here.
Context: ...inter to bincode-serialized wallet bytes - wallet_bytes_len: Length of the wallet bytes - `wallet_i...

(QB_NEW_EN)


[grammar] ~23-~23: There might be a mistake here.
Context: ...t_bytes_len: Length of the wallet bytes - wallet_id_out`: Pointer to a 32-byte buffer that will ...

(QB_NEW_EN)


[grammar] ~24-~24: There might be a mistake here.
Context: ...e buffer that will receive the wallet ID - error: Pointer to an FFIError structure for e...

(QB_NEW_EN)


[grammar] ~87-~87: There might be a mistake here.
Context: ...2.0.0-rc.3). The serialization includes: - Wallet seed and key material - Account i...

(QB_NEW_EN)


[grammar] ~88-~88: There might be a mistake here.
Context: ...includes: - Wallet seed and key material - Account information - Address pools and ...

(QB_NEW_EN)


[grammar] ~89-~89: There might be a mistake here.
Context: ...d and key material - Account information - Address pools and indices - Transaction ...

(QB_NEW_EN)


[grammar] ~90-~90: There might be a mistake here.
Context: ... information - Address pools and indices - Transaction history - Other wallet metad...

(QB_NEW_EN)


[grammar] ~91-~91: There might be a mistake here.
Context: ... pools and indices - Transaction history - Other wallet metadata ## Safety Conside...

(QB_NEW_EN)


[style] ~104-~104: ‘with respect to’ might be wordy. Consider a shorter alternative.
Context: ...ocking, so this function is thread-safe with respect to other wallet manager operations on the ...

(EN_WORDINESS_PREMIUM_WITH_RESPECT_TO)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: fuzz (hashes_ripemd160)
  • GitHub Check: fuzz (hashes_sha1)
  • GitHub Check: fuzz (dash_deserialize_block)
  • GitHub Check: fuzz (hashes_sha256)
  • GitHub Check: fuzz (hashes_json)
  • GitHub Check: fuzz (hashes_sha512)
  • GitHub Check: fuzz (dash_deserialize_witness)
  • GitHub Check: fuzz (hashes_sha512_256)
  • GitHub Check: fuzz (hashes_cbor)
  • GitHub Check: fuzz (dash_script_bytes_to_asm_fmt)
  • GitHub Check: fuzz (dash_deserialize_amount)
  • GitHub Check: fuzz (dash_deserialize_script)
  • GitHub Check: fuzz (dash_deser_net_msg)
  • GitHub Check: fuzz (dash_outpoint_string)
  • GitHub Check: fuzz (dash_deserialize_address)
  • GitHub Check: RPC Tests (stable, true)
  • GitHub Check: Clippy (Non-strict)
  • GitHub Check: SPV Components Tests
🔇 Additional comments (33)
key-wallet/src/managed_account/mod.rs (3)

74-75: LGTM: Properly initialized.


270-274: Inconsistency with wallet_info_interface monitored_addresses aggregation.

WalletInfoInterface::monitored_addresses() currently collects account.all_addresses(), ignoring this new monitored_addresses set. Either rename that method to all_addresses or update it to use ManagedAccountTrait::monitored_addresses.

I can prep a follow-up diff in wallet_info_interface.rs to align semantics. Proceed?

Also applies to: 781-787


830-836: Deprecate monitored_addresses_mut and add validated mutators
Replace the mutable accessor in the trait and impl with add_monitored_address(&mut self, address: Address) and remove_monitored_address(&mut self, address: &Address) that enforce validity; confirm no external consumers rely on monitored_addresses_mut before removal.

key-wallet-ffi/src/account.rs (4)

82-116: LGTM: Validates wallet ptr, enforces single-network, returns opaque handle.


224-232: Memory management for error_message is correct.

account_result_free_error safely frees and nulls the CString.


283-293: Returning a pointer into inner storage requires careful lifetime handling.

The pointer is valid only while the account handle lives; the doc comments state this. Consider returning a copy (32-byte heap) to avoid use-after-free risks on the C side.

I can add a variant that returns an owned 32-byte buffer and a corresponding free function if desired.


350-367: Public-key string encodings: confirm consumer expectations.

BLS/EdDSA xpubs are returned as hex of raw key bytes (no version/prefix). Ensure this matches SDK expectations; otherwise, add a type tag or length prefix.

Also applies to: 467-484

key-wallet-ffi/src/managed_account_collection.rs (3)

837-966: Summary string builder: solid and user-friendly.

No issues.


980-1067: Summary struct allocation/free looks correct.

Fields behind cfgs are handled, and arrays are freed via Vec::from_raw_parts.

Also applies to: 1076-1117


201-229: free_u32_array is exported and documented. All index-returning FFI functions include doc comments instructing callers to free returned arrays with free_u32_array, which is defined and exposed as #[no_mangle] pub unsafe extern "C" fn free_u32_array in key-wallet-ffi/src/account_collection.rs.

key-wallet-ffi/src/lib.rs (1)

8-16: LGTM: module wiring and re-exports updated coherently.

Also applies to: 34-35

key-wallet-ffi/IMPORT_WALLET_FFI.md (1)

1-104: Documentation looks comprehensive and well-structured!

The documentation provides clear guidance on the wallet_manager_import_wallet_from_bytes FFI function, including proper safety considerations, error handling, and usage examples. The thread safety note is particularly valuable for users of this API.

key-wallet-ffi/src/account_tests.rs (3)

11-26: Good improvement with typed FFIAccountType!

The migration from raw integers to the typed FFIAccountType enum improves type safety at the FFI boundary. The test properly validates error handling for null wallet pointers.


150-164: Comprehensive enum value validation!

Good test coverage for verifying the numerical values of FFIAccountType variants. This ensures ABI compatibility across FFI boundaries.


238-262: Excellent null safety testing!

Comprehensive coverage of null pointer handling for all getter functions. This is crucial for FFI safety and preventing crashes in calling code.

key-wallet-ffi/tests/test_account_collection.rs (2)

13-142: Comprehensive test coverage with proper memory management!

Excellent test that exercises various account types and properly cleans up allocated memory using free_u32_array. The assertions validate the expected counts for each account type.


183-196: Good null safety validation!

Testing that null pointers don't crash the API is essential for FFI robustness.

key-wallet-ffi/tests/test_managed_account_collection.rs (3)

94-224: Well-structured test with comprehensive account type coverage!

The test properly exercises special account types and validates their existence. Good memory management with proper cleanup of all allocated resources.


385-419: Excellent summary data validation!

The test thoroughly validates the structure and contents of the summary data, including proper array slice validation and boolean flag checks.


512-562: Good edge case testing for wrong network scenarios!

Testing behavior when requesting accounts for a different network than the wallet was created on is important for multi-network support.

key-wallet-manager/tests/integration_test.rs (1)

29-38: API improvements with direct WalletId return!

Good change to have wallet creation methods return WalletId directly. This aligns with the learning about controlling wallet lifecycle through the manager APIs.

key-wallet-manager/examples/wallet_creation.rs (1)

26-31: API migration looks correct.

Correctly adopts WalletManager::{create_wallet_with_random_mnemonic,create_wallet_from_mnemonic} returning WalletId and passes networks as a slice. No issues spotted.

Also applies to: 49-56, 57-67, 132-137, 139-145

key-wallet-ffi/src/wallet_manager.rs (2)

371-466: LGTM: import path and error mapping.

import_wallet_from_bytes validates inputs, maps errors, pushes wallet_id, and copies out ID correctly.


535-541: Const wallet pointer return is correct.

Returning *const FFIWallet aligns with the read-only FFI design. Matches prior guidance.

key-wallet-manager/src/wallet_manager/mod.rs (1)

190-270: Serialized-bytes path looks sound.

Downgrade flow zeroizes the private wallet before drop and uses feature-gated bincode v2 API. Good separation and checks for duplicates.

key-wallet-ffi/src/address_pool.rs (2)

851-880: LGTM: single-address accessor and error handling.

Returns allocated FFIAddressInfo and provides matching free; bounds are checked.


897-946: LGTM: range accessor semantics and cleanup.

Validates inputs, returns null with NotFound on empty ranges, and provides array free helper.

key-wallet-ffi/src/managed_account.rs (1)

86-103: LGTM: managed account retrieval and cleanup.

Fetches via managed_wallet_info, converts account type, frees wallet-info handle on exit. Error propagation uses FFIManagedAccountResult consistently.

Also applies to: 121-137

key-wallet-ffi/src/account_collection.rs (5)

84-106: Good null checks + clear error reporting in wallet_get_account_collection.

Fits the “const wallet” guidance and validates single-network selection via try_into(); error propagation is consistent.


744-786: Count computation looks correct and feature-gated bits are handled.


799-928: String summary API LGTM.

  • Handles null input.
  • Produces user-friendly, sorted summaries.
  • Uses CString::new safely.

1038-1079: Summary free routine is sound.

Correctly reconstructs Vecs from raw parts and drops the Box; no double-free paths observed.


608-636: Ensure stable C ABI by adding an unconditional pointer alias
Add an always-available account_collection_get_provider_operator_keys_ptr(collection: *const FFIAccountCollection) -> *mut c_void that forwards to the typed bls variant and returns null otherwise, preserving the existing API; regenerate cbindgen headers with and without bls to confirm identical prototypes.

Comment on lines +670 to +697
#[no_mangle]
pub unsafe extern "C" fn account_collection_get_provider_platform_keys(
collection: *const FFIAccountCollection,
) -> *mut crate::account::FFIEdDSAAccount {
if collection.is_null() {
return ptr::null_mut();
}

let collection = &*collection;
match &collection.collection.provider_platform_keys {
Some(account) => {
let ffi_account = crate::account::FFIEdDSAAccount::new(account);
Box::into_raw(Box::new(ffi_account))
}
None => ptr::null_mut(),
}
}

/// Get the provider platform keys account if it exists (stub when EdDSA is disabled)
#[cfg(not(feature = "eddsa"))]
#[no_mangle]
pub unsafe extern "C" fn account_collection_get_provider_platform_keys(
_collection: *const FFIAccountCollection,
) -> *mut std::os::raw::c_void {
// EdDSA feature not enabled, always return null
ptr::null_mut()
}

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Mirror the ABI-stable pointer pattern for EdDSA platform keys.

Same rationale as BLS: provide an always-available …_ptr symbol with void* return type.

@@
 #[cfg(feature = "eddsa")]
 #[no_mangle]
 pub unsafe extern "C" fn account_collection_get_provider_platform_keys(
     collection: *const FFIAccountCollection,
 ) -> *mut crate::account::FFIEdDSAAccount {
@@
 }
 
+// Always-available pointer form with a stable signature across feature flags
+#[no_mangle]
+pub unsafe extern "C" fn account_collection_get_provider_platform_keys_ptr(
+    collection: *const FFIAccountCollection,
+) -> *mut std::os::raw::c_void {
+    if collection.is_null() {
+        return ptr::null_mut();
+    }
+    #[cfg(feature = "eddsa")]
+    {
+        let acc = account_collection_get_provider_platform_keys(collection);
+        acc.cast()
+    }
+    #[cfg(not(feature = "eddsa"))]
+    {
+        ptr::null_mut()
+    }
+}
@@
 #[cfg(not(feature = "eddsa"))]
 #[no_mangle]
 pub unsafe extern "C" fn account_collection_get_provider_platform_keys(
     _collection: *const FFIAccountCollection,
 ) -> *mut std::os::raw::c_void {
     // EdDSA feature not enabled, always return null
     ptr::null_mut()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[no_mangle]
pub unsafe extern "C" fn account_collection_get_provider_platform_keys(
collection: *const FFIAccountCollection,
) -> *mut crate::account::FFIEdDSAAccount {
if collection.is_null() {
return ptr::null_mut();
}
let collection = &*collection;
match &collection.collection.provider_platform_keys {
Some(account) => {
let ffi_account = crate::account::FFIEdDSAAccount::new(account);
Box::into_raw(Box::new(ffi_account))
}
None => ptr::null_mut(),
}
}
/// Get the provider platform keys account if it exists (stub when EdDSA is disabled)
#[cfg(not(feature = "eddsa"))]
#[no_mangle]
pub unsafe extern "C" fn account_collection_get_provider_platform_keys(
_collection: *const FFIAccountCollection,
) -> *mut std::os::raw::c_void {
// EdDSA feature not enabled, always return null
ptr::null_mut()
}
#[no_mangle]
pub unsafe extern "C" fn account_collection_get_provider_platform_keys(
collection: *const FFIAccountCollection,
) -> *mut crate::account::FFIEdDSAAccount {
if collection.is_null() {
return ptr::null_mut();
}
let collection = &*collection;
match &collection.collection.provider_platform_keys {
Some(account) => {
let ffi_account = crate::account::FFIEdDSAAccount::new(account);
Box::into_raw(Box::new(ffi_account))
}
None => ptr::null_mut(),
}
}
// Always-available pointer form with a stable signature across feature flags
#[no_mangle]
pub unsafe extern "C" fn account_collection_get_provider_platform_keys_ptr(
collection: *const FFIAccountCollection,
) -> *mut std::os::raw::c_void {
if collection.is_null() {
return ptr::null_mut();
}
#[cfg(feature = "eddsa")]
{
let acc = account_collection_get_provider_platform_keys(collection);
acc.cast()
}
#[cfg(not(feature = "eddsa"))]
{
ptr::null_mut()
}
}
/// Get the provider platform keys account if it exists (stub when EdDSA is disabled)
#[cfg(not(feature = "eddsa"))]
#[no_mangle]
pub unsafe extern "C" fn account_collection_get_provider_platform_keys(
_collection: *const FFIAccountCollection,
) -> *mut std::os::raw::c_void {
// EdDSA feature not enabled, always return null
ptr::null_mut()
}

Comment on lines +596 to +617
pub unsafe extern "C" fn managed_account_get_external_address_pool(
account: *const FFIManagedAccount,
) -> *mut FFIAddressPool {
if account.is_null() {
return std::ptr::null_mut();
}

let account = &*account;
let managed_account = account.inner();

// Get external address pool if this is a standard account
match &managed_account.account_type {
key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard {
external_addresses,
..
} => {
let ffi_pool = FFIAddressPool {
pool: external_addresses as *const AddressPool as *mut AddressPool,
pool_type: FFIAddressPoolType::External,
};
Box::into_raw(Box::new(ffi_pool))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Tie FFIAddressPool to account lifetime to prevent UAF.

Constructors must pass an Arc clone to FFIAddressPool so the underlying ManagedAccount outlives the pool handle.

 // external
-            let ffi_pool = FFIAddressPool {
-                pool: external_addresses as *const AddressPool as *mut AddressPool,
-                pool_type: FFIAddressPoolType::External,
-            };
+            let ffi_pool = FFIAddressPool {
+                pool: external_addresses as *const AddressPool as *mut AddressPool,
+                _account_keepalive: account.account.clone(),
+                pool_type: FFIAddressPoolType::External,
+            };
 // internal
-            let ffi_pool = FFIAddressPool {
-                pool: internal_addresses as *const AddressPool as *mut AddressPool,
-                pool_type: FFIAddressPoolType::Internal,
-            };
+            let ffi_pool = FFIAddressPool {
+                pool: internal_addresses as *const AddressPool as *mut AddressPool,
+                _account_keepalive: account.account.clone(),
+                pool_type: FFIAddressPoolType::Internal,
+            };
 // generic by type
-                    let ffi_pool = FFIAddressPool {
-                        pool: external_addresses as *const AddressPool as *mut AddressPool,
-                        pool_type: FFIAddressPoolType::External,
-                    };
+                    let ffi_pool = FFIAddressPool {
+                        pool: external_addresses as *const AddressPool as *mut AddressPool,
+                        _account_keepalive: account.account.clone(),
+                        pool_type: FFIAddressPoolType::External,
+                    };
 ...
-                    let ffi_pool = FFIAddressPool {
-                        pool: internal_addresses as *const AddressPool as *mut AddressPool,
-                        pool_type: FFIAddressPoolType::Internal,
-                    };
+                    let ffi_pool = FFIAddressPool {
+                        pool: internal_addresses as *const AddressPool as *mut AddressPool,
+                        _account_keepalive: account.account.clone(),
+                        pool_type: FFIAddressPoolType::Internal,
+                    };
 ...
-            let ffi_pool = FFIAddressPool {
-                pool: pool_ref as *const AddressPool as *mut AddressPool,
-                pool_type: FFIAddressPoolType::Single,
-            };
+            let ffi_pool = FFIAddressPool {
+                pool: pool_ref as *const AddressPool as *mut AddressPool,
+                _account_keepalive: account.account.clone(),
+                pool_type: FFIAddressPoolType::Single,
+            };

Also applies to: 632-653, 684-763

🤖 Prompt for AI Agents
In key-wallet-ffi/src/managed_account.rs around lines 596 to 617, the
FFIAddressPool returned currently contains a raw pointer to the account's
AddressPool but no ownership tie to the ManagedAccount, risking use-after-free;
fix by updating FFIAddressPool to hold an Arc<ManagedAccount> (or Arc to the
inner owner that holds AddressPool) and when creating ffi_pool clone and store
account.inner_arc (or wrap managed_account in Arc::clone(&account_arc)) so the
Arc is moved into the FFIAddressPool instance; apply the same pattern to the
other creation sites at lines 632-653 and 684-763 so every FFIAddressPool keeps
an Arc clone that ensures the underlying ManagedAccount outlives the pool
handle.

@QuantumExplorer QuantumExplorer merged commit 3b035d8 into v0.40-dev Aug 29, 2025
19 of 26 checks passed
@QuantumExplorer QuantumExplorer deleted the refactor/cleanUpKeyWallet5 branch August 29, 2025 04:47
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