diff --git a/dash-spv/src/main.rs b/dash-spv/src/main.rs index d53925914..e4090bbd8 100644 --- a/dash-spv/src/main.rs +++ b/dash-spv/src/main.rs @@ -228,11 +228,9 @@ async fn run() -> Result<(), Box> { ManagedWalletInfo, >::new()); spv_wallet.base.create_wallet_from_mnemonic( - WalletId::default(), - "Default".to_string(), "enemy check owner stumble unaware debris suffer peanut good fabric bleak outside", "", - Some(network), + &[network], None, key_wallet::wallet::initialization::WalletAccountCreationOptions::default(), )?; diff --git a/key-wallet-ffi/Cargo.toml b/key-wallet-ffi/Cargo.toml index 5f8501d17..0be44e3ae 100644 --- a/key-wallet-ffi/Cargo.toml +++ b/key-wallet-ffi/Cargo.toml @@ -13,8 +13,11 @@ name = "key_wallet_ffi" crate-type = ["cdylib", "staticlib", "lib"] [features] -default = [] +default = ["bincode", "eddsa", "bls"] bip38 = ["key-wallet/bip38"] +bincode = ["key-wallet-manager/bincode", "key-wallet/bincode"] +eddsa = ["dashcore/eddsa", "key-wallet/eddsa"] +bls = ["dashcore/bls", "key-wallet/bls"] [dependencies] key-wallet = { path = "../key-wallet", default-features = false, features = ["std"] } @@ -22,9 +25,7 @@ key-wallet-manager = { path = "../key-wallet-manager", features = ["std"] } dashcore = { path = "../dash", features = ["std"] } dash-network = { path = "../dash-network" } secp256k1 = { version = "0.30.0", features = ["global-context"] } -thiserror = "2.0.12" libc = "0.2" -sha2 = "0.10" hex = "0.4" [build-dependencies] diff --git a/key-wallet-ffi/IMPORT_WALLET_FFI.md b/key-wallet-ffi/IMPORT_WALLET_FFI.md new file mode 100644 index 000000000..c3fa16b11 --- /dev/null +++ b/key-wallet-ffi/IMPORT_WALLET_FFI.md @@ -0,0 +1,104 @@ +# Wallet Import FFI Binding + +## Overview + +The `wallet_manager_import_wallet_from_bytes` FFI function allows importing a previously serialized wallet from bincode bytes into a wallet manager instance. + +## Function Signature + +```c +bool wallet_manager_import_wallet_from_bytes( + FFIWalletManager *manager, + const uint8_t *wallet_bytes, + size_t wallet_bytes_len, + uint8_t *wallet_id_out, + FFIError *error +); +``` + +## Parameters + +- `manager`: Pointer to an FFIWalletManager instance +- `wallet_bytes`: Pointer to bincode-serialized wallet bytes +- `wallet_bytes_len`: Length of the wallet bytes +- `wallet_id_out`: Pointer to a 32-byte buffer that will receive the wallet ID +- `error`: Pointer to an FFIError structure for error reporting (can be NULL) + +## Return Value + +- `true`: Wallet imported successfully +- `false`: Import failed (check error for details) + +## Error Codes + +The function may set the following error codes: + +- `InvalidInput` (1): Null pointer or invalid length provided +- `SerializationError` (9): Failed to deserialize wallet from bincode +- `InvalidState` (11): Wallet already exists in the manager +- `WalletError` (8): Other wallet-related errors + +## Usage Example + +```c +#include "key_wallet_ffi.h" + +// Load wallet bytes from file or network +uint8_t *wallet_bytes = load_wallet_bytes(); +size_t bytes_len = get_wallet_bytes_length(); + +// Prepare output buffer for wallet ID +uint8_t wallet_id[32]; + +// Import the wallet +FFIError error = {0}; +bool success = wallet_manager_import_wallet_from_bytes( + manager, + wallet_bytes, + bytes_len, + wallet_id, + &error +); + +if (success) { + printf("Wallet imported with ID: "); + for (int i = 0; i < 32; i++) { + printf("%02x", wallet_id[i]); + } + printf("\n"); +} else { + printf("Import failed: %s\n", error.message); + if (error.message) { + error_message_free(error.message); + } +} +``` + +## Building with Bincode Support + +To use this function, the FFI library must be built with the `bincode` feature enabled: + +```bash +cargo build --features bincode +``` + +## Serialization Format + +The wallet bytes must be in bincode format (version 2.0.0-rc.3). The serialization includes: +- Wallet seed and key material +- Account information +- Address pools and indices +- Transaction history +- Other wallet metadata + +## Safety Considerations + +1. The `wallet_bytes` pointer must remain valid for the duration of the function call +2. The `wallet_id_out` buffer must be at least 32 bytes +3. Do not use the wallet_id_out buffer if the function returns false +4. Always free error messages using `error_message_free()` when done +5. The imported wallet must not already exist in the manager (will fail with InvalidState) + +## Thread Safety + +The wallet manager uses internal locking, so this function is thread-safe with respect to other wallet manager operations on the same instance. \ No newline at end of file diff --git a/key-wallet-ffi/cbindgen.toml b/key-wallet-ffi/cbindgen.toml index 76e1aa0b5..941e34a04 100644 --- a/key-wallet-ffi/cbindgen.toml +++ b/key-wallet-ffi/cbindgen.toml @@ -42,6 +42,15 @@ item_types = ["functions", "enums", "structs", "typedefs", "opaque", "constants" "FFIExtendedPublicKey" = "" "FFIManagedWalletInfo" = "" "FFIWalletManager" = "" +"FFIWallet" = "" +"FFIManagedWallet" = "" +"FFIAccount" = "" +"FFIBLSAccount" = "" +"FFIEdDSAAccount" = "" +"FFIManagedAccount" = "" +"FFIAccountCollection" = "" +"FFIManagedAccountCollection" = "" +"FFIAddressPool" = "" [export.rename] # Rename types to match C conventions diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 6d12acc7d..e9211f71f 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -47,7 +47,7 @@ typedef enum { /* Create no accounts at all */ - NONE = 4, + NO_ACCOUNTS = 4, } FFIAccountCreationOptionType; /* @@ -189,13 +189,15 @@ typedef enum { } FFILanguage; /* - FFI Network type + FFI Network type (bit flags for multiple networks) */ typedef enum { - DASH = 0, - TESTNET = 1, - REGTEST = 2, - DEVNET = 3, + NO_NETWORKS = 0, + DASH = 1, + TESTNET = 2, + REGTEST = 4, + DEVNET = 8, + ALL_NETWORKS = 15, } FFINetwork; /* @@ -243,6 +245,29 @@ typedef enum { */ typedef struct FFIAccount FFIAccount; +/* + Opaque handle to an account collection + */ +typedef struct FFIAccountCollection FFIAccountCollection; + +/* + FFI wrapper for an AddressPool from a ManagedAccount + + This is a lightweight wrapper that holds a reference to an AddressPool + from within a ManagedAccount. It allows querying addresses and pool information. + */ +typedef struct FFIAddressPool FFIAddressPool; + +/* + Opaque BLS account handle + */ +typedef struct FFIBLSAccount FFIBLSAccount; + +/* + Opaque EdDSA account handle + */ +typedef struct FFIEdDSAAccount FFIEdDSAAccount; + /* Extended private key structure */ @@ -263,6 +288,16 @@ typedef struct FFIExtendedPubKey FFIExtendedPubKey; */ typedef struct FFIExtendedPublicKey FFIExtendedPublicKey; +/* + Opaque managed account handle that wraps ManagedAccount + */ +typedef struct FFIManagedAccount FFIManagedAccount; + +/* + Opaque handle to a managed account collection + */ +typedef struct FFIManagedAccountCollection FFIManagedAccountCollection; + /* FFI wrapper for ManagedWalletInfo */ @@ -314,11 +349,82 @@ typedef struct { char *message; } FFIError; +/* + C-compatible summary of all accounts in a collection + + This struct provides Swift with structured data about all accounts + that exist in the collection, allowing programmatic access to account + indices and presence information. + */ +typedef struct { + /* + Array of BIP44 account indices + */ + unsigned int *bip44_indices; + /* + Number of BIP44 accounts + */ + size_t bip44_count; + /* + Array of BIP32 account indices + */ + unsigned int *bip32_indices; + /* + Number of BIP32 accounts + */ + size_t bip32_count; + /* + Array of CoinJoin account indices + */ + unsigned int *coinjoin_indices; + /* + Number of CoinJoin accounts + */ + size_t coinjoin_count; + /* + Array of identity top-up registration indices + */ + unsigned int *identity_topup_indices; + /* + Number of identity top-up accounts + */ + size_t identity_topup_count; + /* + Whether identity registration account exists + */ + bool has_identity_registration; + /* + Whether identity invitation account exists + */ + bool has_identity_invitation; + /* + Whether identity top-up not bound account exists + */ + bool has_identity_topup_not_bound; + /* + Whether provider voting keys account exists + */ + bool has_provider_voting_keys; + /* + Whether provider owner keys account exists + */ + bool has_provider_owner_keys; + /* + Whether provider operator keys account exists + */ + bool has_provider_operator_keys; + /* + Whether provider platform keys account exists + */ + bool has_provider_platform_keys; +} FFIAccountCollectionSummary; + /* FFI wrapper for ManagedWalletInfo that includes transaction checking capabilities */ typedef struct { ManagedWalletInfo *inner; + } FFIManagedWallet; /* @@ -352,15 +458,181 @@ typedef struct { } FFIAddressPoolInfo; /* - Balance structure for FFI + FFI-compatible version of AddressInfo + */ +typedef struct { + /* + Address as string + */ + char *address; + /* + Script pubkey bytes + */ + uint8_t *script_pubkey; + /* + Length of script pubkey + */ + size_t script_pubkey_len; + /* + Public key bytes (nullable) + */ + uint8_t *public_key; + /* + Length of public key + */ + size_t public_key_len; + /* + Derivation index + */ + uint32_t index; + /* + Derivation path as string + */ + char *path; + /* + Whether address has been used + */ + bool used; + /* + When generated (timestamp) + */ + uint64_t generated_at; + /* + When first used (0 if never) + */ + uint64_t used_at; + /* + Transaction count + */ + uint32_t tx_count; + /* + Total received + */ + uint64_t total_received; + /* + Total sent + */ + uint64_t total_sent; + /* + Current balance + */ + uint64_t balance; + /* + Custom label (nullable) + */ + char *label; +} FFIAddressInfo; + +/* + FFI Result type for ManagedAccount operations + */ +typedef struct { + /* + The managed account handle if successful, NULL if error + */ + FFIManagedAccount *account; + /* + Error code (0 = success) + */ + int32_t error_code; + /* + Error message (NULL if success, must be freed by caller if not NULL) + */ + char *error_message; +} FFIManagedAccountResult; + +/* + FFI Balance type for representing wallet balances */ typedef struct { + /* + Confirmed balance in duffs + */ uint64_t confirmed; + /* + Unconfirmed balance in duffs + */ uint64_t unconfirmed; + /* + Immature balance in duffs (e.g., mining rewards) + */ uint64_t immature; + /* + Total balance (confirmed + unconfirmed) in duffs + */ uint64_t total; } FFIBalance; +/* + C-compatible summary of all accounts in a managed collection + + This struct provides Swift with structured data about all accounts + that exist in the managed collection, allowing programmatic access to account + indices and presence information. + */ +typedef struct { + /* + Array of BIP44 account indices + */ + unsigned int *bip44_indices; + /* + Number of BIP44 accounts + */ + size_t bip44_count; + /* + Array of BIP32 account indices + */ + unsigned int *bip32_indices; + /* + Number of BIP32 accounts + */ + size_t bip32_count; + /* + Array of CoinJoin account indices + */ + unsigned int *coinjoin_indices; + /* + Number of CoinJoin accounts + */ + size_t coinjoin_count; + /* + Array of identity top-up registration indices + */ + unsigned int *identity_topup_indices; + /* + Number of identity top-up accounts + */ + size_t identity_topup_count; + /* + Whether identity registration account exists + */ + bool has_identity_registration; + /* + Whether identity invitation account exists + */ + bool has_identity_invitation; + /* + Whether identity top-up not bound account exists + */ + bool has_identity_topup_not_bound; + /* + Whether provider voting keys account exists + */ + bool has_provider_voting_keys; + /* + Whether provider owner keys account exists + */ + bool has_provider_owner_keys; + /* + Whether provider operator keys account exists + */ + bool has_provider_operator_keys; + /* + Whether provider platform keys account exists + */ + bool has_provider_platform_keys; +} FFIManagedAccountCollectionSummary; + /* Provider key info */ @@ -524,7 +796,7 @@ extern "C" { FFIAccountResult wallet_get_account(const FFIWallet *wallet, FFINetwork network, unsigned int account_index, - unsigned int account_type) + FFIAccountType account_type) ; /* @@ -554,6 +826,28 @@ FFIAccountResult wallet_get_top_up_account_with_registration_index(const FFIWall */ void account_free(FFIAccount *account) ; +/* + Free a BLS account handle + + # Safety + + - `account` must be a valid pointer to an FFIBLSAccount + - The pointer must not be used after calling this function + - This function must only be called once per allocation + */ + void bls_account_free(FFIBLSAccount *account) ; + +/* + Free an EdDSA account handle + + # Safety + + - `account` must be a valid pointer to an FFIEdDSAAccount + - The pointer must not be used after calling this function + - This function must only be called once per allocation + */ + void eddsa_account_free(FFIEdDSAAccount *account) ; + /* Free an account result's error message (if any) Note: This does NOT free the account handle itself - use account_free for that @@ -567,704 +861,1855 @@ FFIAccountResult wallet_get_top_up_account_with_registration_index(const FFIWall void account_result_free_error(FFIAccountResult *result) ; /* - Get number of accounts + Get the extended public key of an account as a string # Safety - - `wallet` must be a valid pointer to an FFIWallet instance - - `error` must be a valid pointer to an FFIError structure or null - - The caller must ensure both pointers remain valid for the duration of this call + - `account` must be a valid pointer to an FFIAccount instance + - The returned string must be freed by the caller using `string_free` + - Returns NULL if the account is null */ - -unsigned int wallet_get_account_count(const FFIWallet *wallet, - FFINetwork network, - FFIError *error) -; + char *account_get_extended_public_key_as_string(const FFIAccount *account) ; /* - Free address string + Get the network of an account # Safety - - `address` must be a valid pointer created by address functions or null - - After calling this function, the pointer becomes invalid + - `account` must be a valid pointer to an FFIAccount instance + - Returns FFINetwork::NoNetworks if the account is null */ - void address_free(char *address) ; + FFINetwork account_get_network(const FFIAccount *account) ; /* - Free address array + Get the parent wallet ID of an account # Safety - - `addresses` must be a valid pointer to an array of address strings or null - - Each address in the array must be a valid C string pointer - - `count` must be the correct number of addresses in the array - - After calling this function, all pointers become invalid + - `account` must be a valid pointer to an FFIAccount instance + - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null + - The returned pointer is valid only as long as the account exists + - The caller should copy the data if needed for longer use */ - void address_array_free(char **addresses, size_t count) ; + const uint8_t *account_get_parent_wallet_id(const FFIAccount *account) ; /* - Validate an address + Get the account type of an account # Safety - - `address` must be a valid null-terminated C string - - `error` must be a valid pointer to an FFIError + - `account` must be a valid pointer to an FFIAccount instance + - `out_index` must be a valid pointer to a c_uint where the index will be stored + - Returns FFIAccountType::StandardBIP44 with index 0 if the account is null */ - bool address_validate(const char *address, FFINetwork network, FFIError *error) ; + FFIAccountType account_get_account_type(const FFIAccount *account, unsigned int *out_index) ; /* - Get address type - - Returns: - - 0: P2PKH address - - 1: P2SH address - - 2: Other address type - - u8::MAX (255): Error occurred + Check if an account is watch-only # Safety - - `address` must be a valid null-terminated C string - - `error` must be a valid pointer to an FFIError + - `account` must be a valid pointer to an FFIAccount instance + - Returns false if the account is null */ - unsigned char address_get_type(const char *address, FFINetwork network, FFIError *error) ; + bool account_get_is_watch_only(const FFIAccount *account) ; /* - Get address pool information for an account + Get the extended public key of a BLS account as a string # Safety - - `managed_wallet` must be a valid pointer to an FFIManagedWallet - - `info_out` must be a valid pointer to store the pool info - - `error` must be a valid pointer to an FFIError or null + - `account` must be a valid pointer to an FFIBLSAccount instance + - The returned string must be freed by the caller using `string_free` + - Returns NULL if the account is null */ - -bool managed_wallet_get_address_pool_info(const FFIManagedWallet *managed_wallet, - FFINetwork network, - unsigned int account_type, - unsigned int account_index, - unsigned int registration_index, - FFIAddressPoolType pool_type, - FFIAddressPoolInfo *info_out, - FFIError *error) -; + char *bls_account_get_extended_public_key_as_string(const FFIBLSAccount *account) ; /* - Set the gap limit for an address pool - - The gap limit determines how many unused addresses to maintain at the end - of the pool. This is important for wallet recovery and address discovery. + Get the network of a BLS account # Safety - - `managed_wallet` must be a valid pointer to an FFIManagedWallet - - `error` must be a valid pointer to an FFIError or null + - `account` must be a valid pointer to an FFIBLSAccount instance + - Returns FFINetwork::NoNetworks if the account is null */ - -bool managed_wallet_set_gap_limit(FFIManagedWallet *managed_wallet, - FFINetwork network, - unsigned int account_type, - unsigned int account_index, - unsigned int registration_index, - FFIAddressPoolType pool_type, - unsigned int gap_limit, - FFIError *error) -; + FFINetwork bls_account_get_network(const FFIBLSAccount *account) ; /* - Generate addresses up to a specific index in a pool + Get the parent wallet ID of a BLS account - This ensures that addresses up to and including the specified index exist - in the pool. This is useful for wallet recovery or when specific indices - are needed. + # Safety + + - `account` must be a valid pointer to an FFIBLSAccount instance + - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null + - The returned pointer is valid only as long as the account exists + - The caller should copy the data if needed for longer use + */ + const uint8_t *bls_account_get_parent_wallet_id(const FFIBLSAccount *account) ; + +/* + Get the account type of a BLS account # Safety - - `managed_wallet` must be a valid pointer to an FFIManagedWallet - - `wallet` must be a valid pointer to an FFIWallet (for key derivation) - - `error` must be a valid pointer to an FFIError or null + - `account` must be a valid pointer to an FFIBLSAccount instance + - `out_index` must be a valid pointer to a c_uint where the index will be stored + - Returns FFIAccountType::StandardBIP44 with index 0 if the account is null */ -bool managed_wallet_generate_addresses_to_index(FFIManagedWallet *managed_wallet, - const FFIWallet *wallet, - FFINetwork network, - unsigned int account_type, - unsigned int account_index, - unsigned int registration_index, - FFIAddressPoolType pool_type, - unsigned int target_index, - FFIError *error) +FFIAccountType bls_account_get_account_type(const FFIBLSAccount *account, + unsigned int *out_index) ; /* - Mark an address as used in the pool + Check if a BLS account is watch-only - This updates the pool's tracking of which addresses have been used, - which is important for gap limit management and wallet recovery. + # Safety + + - `account` must be a valid pointer to an FFIBLSAccount instance + - Returns false if the account is null + */ + bool bls_account_get_is_watch_only(const FFIBLSAccount *account) ; + +/* + Get the extended public key of an EdDSA account as a string # Safety - - `managed_wallet` must be a valid pointer to an FFIManagedWallet - - `address` must be a valid C string - - `error` must be a valid pointer to an FFIError or null + - `account` must be a valid pointer to an FFIEdDSAAccount instance + - The returned string must be freed by the caller using `string_free` + - Returns NULL if the account is null */ + char *eddsa_account_get_extended_public_key_as_string(const FFIEdDSAAccount *account) ; -bool managed_wallet_mark_address_used(FFIManagedWallet *managed_wallet, - FFINetwork network, - const char *address, - FFIError *error) +/* + Get the network of an EdDSA account + + # Safety + + - `account` must be a valid pointer to an FFIEdDSAAccount instance + - Returns FFINetwork::NoNetworks if the account is null + */ + FFINetwork eddsa_account_get_network(const FFIEdDSAAccount *account) ; + +/* + Get the parent wallet ID of an EdDSA account + + # Safety + + - `account` must be a valid pointer to an FFIEdDSAAccount instance + - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null + - The returned pointer is valid only as long as the account exists + - The caller should copy the data if needed for longer use + */ + const uint8_t *eddsa_account_get_parent_wallet_id(const FFIEdDSAAccount *account) ; + +/* + Get the account type of an EdDSA account + + # Safety + + - `account` must be a valid pointer to an FFIEdDSAAccount instance + - `out_index` must be a valid pointer to a c_uint where the index will be stored + - Returns FFIAccountType::StandardBIP44 with index 0 if the account is null + */ + +FFIAccountType eddsa_account_get_account_type(const FFIEdDSAAccount *account, + unsigned int *out_index) ; /* - Get wallet balance + Check if an EdDSA account is watch-only + + # Safety + + - `account` must be a valid pointer to an FFIEdDSAAccount instance + - Returns false if the account is null + */ + bool eddsa_account_get_is_watch_only(const FFIEdDSAAccount *account) ; + +/* + Get number of accounts # Safety - `wallet` must be a valid pointer to an FFIWallet instance - - `balance_out` must be a valid pointer to an FFIBalance structure - `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 caller must ensure both pointers remain valid for the duration of this call */ -bool wallet_get_balance(const FFIWallet *wallet, - FFINetwork network, - FFIBalance *balance_out, - FFIError *error) +unsigned int wallet_get_account_count(const FFIWallet *wallet, + FFINetwork network, + FFIError *error) ; /* - Get account balance + Get account collection for a specific network from wallet # Safety - `wallet` must be a valid pointer to an FFIWallet instance - - `balance_out` must be a valid pointer to an FFIBalance structure - `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 pointer must be freed with `account_collection_free` when no longer needed */ -bool wallet_get_account_balance(const FFIWallet *wallet, - FFINetwork network, - unsigned int account_index, - FFIBalance *balance_out, - FFIError *error) +FFIAccountCollection *wallet_get_account_collection(const FFIWallet *wallet, + FFINetwork network, + FFIError *error) ; /* - Create a new master extended private key from seed + Free an account collection handle # Safety - - `seed` must be a valid pointer to a byte array of `seed_len` length - - `error` must be a valid pointer to an FFIError structure or null - - The caller must ensure the seed pointer remains valid for the duration of this call + - `collection` must be a valid pointer to an FFIAccountCollection created by this library + - `collection` must not be used after calling this function */ + void account_collection_free(FFIAccountCollection *collection) ; -FFIExtendedPrivKey *derivation_new_master_key(const uint8_t *seed, - size_t seed_len, - FFINetwork network, - FFIError *error) +/* + Get a BIP44 account by index from the collection + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed + */ + +FFIAccount *account_collection_get_bip44_account(const FFIAccountCollection *collection, + unsigned int index) ; /* - Derive a BIP44 account path (m/44'/5'/account') + Get all BIP44 account indices + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - `out_indices` must be a valid pointer to store the indices array + - `out_count` must be a valid pointer to store the count + - The returned array must be freed with `free_u32_array` when no longer needed */ -bool derivation_bip44_account_path(FFINetwork network, - unsigned int account_index, - char *path_out, - size_t path_max_len, - FFIError *error) +bool account_collection_get_bip44_indices(const FFIAccountCollection *collection, + unsigned int **out_indices, + size_t *out_count) ; /* - Derive a BIP44 payment path (m/44'/5'/account'/change/index) + Get a BIP32 account by index from the collection + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed */ -bool derivation_bip44_payment_path(FFINetwork network, - unsigned int account_index, - bool is_change, - unsigned int address_index, - char *path_out, - size_t path_max_len, - FFIError *error) +FFIAccount *account_collection_get_bip32_account(const FFIAccountCollection *collection, + unsigned int index) ; /* - Derive CoinJoin path (m/9'/5'/4'/account') + Get all BIP32 account indices + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - `out_indices` must be a valid pointer to store the indices array + - `out_count` must be a valid pointer to store the count + - The returned array must be freed with `free_u32_array` when no longer needed */ -bool derivation_coinjoin_path(FFINetwork network, - unsigned int account_index, - char *path_out, - size_t path_max_len, - FFIError *error) +bool account_collection_get_bip32_indices(const FFIAccountCollection *collection, + unsigned int **out_indices, + size_t *out_count) ; /* - Derive identity registration path (m/9'/5'/5'/1'/index') + Get a CoinJoin account by index from the collection + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed */ -bool derivation_identity_registration_path(FFINetwork network, - unsigned int identity_index, - char *path_out, - size_t path_max_len, - FFIError *error) +FFIAccount *account_collection_get_coinjoin_account(const FFIAccountCollection *collection, + unsigned int index) ; /* - Derive identity top-up path (m/9'/5'/5'/2'/identity_index'/top_up_index') + Get all CoinJoin account indices + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - `out_indices` must be a valid pointer to store the indices array + - `out_count` must be a valid pointer to store the count + - The returned array must be freed with `free_u32_array` when no longer needed */ -bool derivation_identity_topup_path(FFINetwork network, - unsigned int identity_index, - unsigned int topup_index, - char *path_out, - size_t path_max_len, - FFIError *error) +bool account_collection_get_coinjoin_indices(const FFIAccountCollection *collection, + unsigned int **out_indices, + size_t *out_count) ; /* - Derive identity authentication path (m/9'/5'/5'/0'/identity_index'/key_index') + Get the identity registration account if it exists + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed */ + FFIAccount *account_collection_get_identity_registration(const FFIAccountCollection *collection) ; -bool derivation_identity_authentication_path(FFINetwork network, - unsigned int identity_index, - unsigned int key_index, - char *path_out, - size_t path_max_len, - FFIError *error) +/* + Check if identity registration account exists + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + */ + bool account_collection_has_identity_registration(const FFIAccountCollection *collection) ; + +/* + Get an identity topup account by registration index + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed + */ + +FFIAccount *account_collection_get_identity_topup(const FFIAccountCollection *collection, + unsigned int registration_index) ; /* - Derive private key for a specific path from seed + Get all identity topup registration indices # Safety - - `seed` must be a valid pointer to a byte array of `seed_len` length - - `path` must be a valid pointer to a null-terminated C string - - `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 + - `collection` must be a valid pointer to an FFIAccountCollection + - `out_indices` must be a valid pointer to store the indices array + - `out_count` must be a valid pointer to store the count + - The returned array must be freed with `free_u32_array` when no longer needed */ -FFIExtendedPrivKey *derivation_derive_private_key_from_seed(const uint8_t *seed, - size_t seed_len, - const char *path, - FFINetwork network, - FFIError *error) +bool account_collection_get_identity_topup_indices(const FFIAccountCollection *collection, + unsigned int **out_indices, + size_t *out_count) ; /* - Derive public key from extended private key + Get the identity topup not bound account if it exists # Safety - - `xpriv` must be a valid pointer to an FFIExtendedPrivKey - - `error` must be a valid pointer to an FFIError - - The returned pointer must be freed with `extended_public_key_free` + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed */ - FFIExtendedPubKey *derivation_xpriv_to_xpub(const FFIExtendedPrivKey *xpriv, FFIError *error) ; + +FFIAccount *account_collection_get_identity_topup_not_bound(const FFIAccountCollection *collection) +; /* - Get extended private key as string + Check if identity topup not bound account exists # Safety - - `xpriv` must be a valid pointer to an FFIExtendedPrivKey - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIAccountCollection */ - char *derivation_xpriv_to_string(const FFIExtendedPrivKey *xpriv, FFIError *error) ; + bool account_collection_has_identity_topup_not_bound(const FFIAccountCollection *collection) ; /* - Get extended public key as string + Get the identity invitation account if it exists # Safety - - `xpub` must be a valid pointer to an FFIExtendedPubKey - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed */ - char *derivation_xpub_to_string(const FFIExtendedPubKey *xpub, FFIError *error) ; + FFIAccount *account_collection_get_identity_invitation(const FFIAccountCollection *collection) ; /* - Get fingerprint from extended public key (4 bytes) + Check if identity invitation account exists # Safety - - `xpub` must be a valid pointer to an FFIExtendedPubKey - - `fingerprint_out` must be a valid pointer to a buffer of at least 4 bytes - - `error` must be a valid pointer to an FFIError + - `collection` must be a valid pointer to an FFIAccountCollection */ + bool account_collection_has_identity_invitation(const FFIAccountCollection *collection) ; -bool derivation_xpub_fingerprint(const FFIExtendedPubKey *xpub, - uint8_t *fingerprint_out, - FFIError *error) -; +/* + Get the provider voting keys account if it exists + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed + */ + FFIAccount *account_collection_get_provider_voting_keys(const FFIAccountCollection *collection) ; /* - Free extended private key + Check if provider voting keys account exists # Safety - - `xpriv` must be a valid pointer to an FFIExtendedPrivKey that was allocated by this library - - The pointer must not be used after calling this function - - This function must only be called once per allocation + - `collection` must be a valid pointer to an FFIAccountCollection */ - void derivation_xpriv_free(FFIExtendedPrivKey *xpriv) ; + bool account_collection_has_provider_voting_keys(const FFIAccountCollection *collection) ; /* - Free extended public key + Get the provider owner keys account if it exists # Safety - - `xpub` must be a valid pointer to an FFIExtendedPubKey that was allocated by this library - - The pointer must not be used after calling this function - - This function must only be called once per allocation + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_free` when no longer needed */ - void derivation_xpub_free(FFIExtendedPubKey *xpub) ; + FFIAccount *account_collection_get_provider_owner_keys(const FFIAccountCollection *collection) ; /* - Free derivation path string + Check if provider owner keys account exists # Safety - - `s` must be a valid pointer to a C string that was allocated by this library - - The pointer must not be used after calling this function - - This function must only be called once per allocation + - `collection` must be a valid pointer to an FFIAccountCollection */ - void derivation_string_free(char *s) ; + bool account_collection_has_provider_owner_keys(const FFIAccountCollection *collection) ; /* - Derive key using DIP9 path constants for identity + Get the provider operator keys account if it exists + Note: This function is only available when the `bls` feature is enabled # Safety - - `seed` must be a valid pointer to a byte array of `seed_len` length - - `error` must be a valid pointer to an FFIError structure or null - - The caller must ensure the seed pointer remains valid for the duration of this call + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `bls_account_free` when no longer needed */ -FFIExtendedPrivKey *dip9_derive_identity_key(const uint8_t *seed, - size_t seed_len, - FFINetwork network, - unsigned int identity_index, - unsigned int key_index, - FFIDerivationPathType key_type, - FFIError *error) +FFIBLSAccount *account_collection_get_provider_operator_keys(const FFIAccountCollection *collection) ; /* - Free an error message + Get the provider operator keys account if it exists (stub when BLS is disabled) + */ + void *account_collection_get_provider_operator_keys(const FFIAccountCollection *_collection) ; + +/* + Check if provider operator keys account exists # Safety - - `message` must be a valid pointer to a C string that was allocated by this library - - The pointer must not be used after calling this function - - This function must only be called once per allocation + - `collection` must be a valid pointer to an FFIAccountCollection */ - void error_message_free(char *message) ; + bool account_collection_has_provider_operator_keys(const FFIAccountCollection *collection) ; /* - Get extended private key for account + Get the provider platform keys account if it exists + Note: This function is only available when the `eddsa` feature is enabled + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `eddsa_account_free` when no longer needed + */ + +FFIEdDSAAccount *account_collection_get_provider_platform_keys(const FFIAccountCollection *collection) +; + +/* + Get the provider platform keys account if it exists (stub when EdDSA is disabled) + */ + void *account_collection_get_provider_platform_keys(const FFIAccountCollection *_collection) ; + +/* + Check if provider platform keys account exists + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + */ + bool account_collection_has_provider_platform_keys(const FFIAccountCollection *collection) ; + +/* + Free a u32 array allocated by this library + + # Safety + + - `array` must be a valid pointer to an array allocated by this library + - `array` must not be used after calling this function + */ + void free_u32_array(unsigned int *array, size_t count) ; + +/* + Get the total number of accounts in the collection + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + */ + unsigned int account_collection_count(const FFIAccountCollection *collection) ; + +/* + Get a human-readable summary of all accounts in the collection + + Returns a formatted string showing all account types and their indices. + The format is designed to be clear and readable for end users. + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned string must be freed with `string_free` when no longer needed + - Returns null if the collection pointer is null + */ + char *account_collection_summary(const FFIAccountCollection *collection) ; + +/* + Get structured account collection summary data + + Returns a struct containing arrays of indices for each account type and boolean + flags for special accounts. This provides Swift with programmatic access to + account information. + + # Safety + + - `collection` must be a valid pointer to an FFIAccountCollection + - The returned pointer must be freed with `account_collection_summary_free` when no longer needed + - Returns null if the collection pointer is null + */ + +FFIAccountCollectionSummary *account_collection_summary_data(const FFIAccountCollection *collection) +; + +/* + Free an account collection summary and all its allocated memory + + # Safety + + - `summary` must be a valid pointer to an FFIAccountCollectionSummary created by `account_collection_summary_data` + - `summary` must not be used after calling this function + */ + +void account_collection_summary_free(FFIAccountCollectionSummary *summary) +; + +/* + Free address string + + # Safety + + - `address` must be a valid pointer created by address functions or null + - After calling this function, the pointer becomes invalid + */ + void address_free(char *address) ; + +/* + Free address array + + # Safety + + - `addresses` must be a valid pointer to an array of address strings or null + - Each address in the array must be a valid C string pointer + - `count` must be the correct number of addresses in the array + - After calling this function, all pointers become invalid + */ + void address_array_free(char **addresses, size_t count) ; + +/* + Validate an address + + # Safety + + - `address` must be a valid null-terminated C string + - `error` must be a valid pointer to an FFIError + */ + bool address_validate(const char *address, FFINetwork network, FFIError *error) ; + +/* + Get address type + + Returns: + - 0: P2PKH address + - 1: P2SH address + - 2: Other address type + - u8::MAX (255): Error occurred + + # Safety + + - `address` must be a valid null-terminated C string + - `error` must be a valid pointer to an FFIError + */ + unsigned char address_get_type(const char *address, FFINetwork network, FFIError *error) ; + +/* + Free an address pool handle + + # Safety + + - `pool` must be a valid pointer to an FFIAddressPool that was allocated by this library + - The pointer must not be used after calling this function + - This function must only be called once per allocation + */ + void address_pool_free(FFIAddressPool *pool) ; + +/* + Get address pool information for an account + + # Safety + + - `managed_wallet` must be a valid pointer to an FFIManagedWallet + - `info_out` must be a valid pointer to store the pool info + - `error` must be a valid pointer to an FFIError or null + */ + +bool managed_wallet_get_address_pool_info(const FFIManagedWallet *managed_wallet, + FFINetwork network, + FFIAccountType account_type, + unsigned int account_index, + FFIAddressPoolType pool_type, + FFIAddressPoolInfo *info_out, + FFIError *error) +; + +/* + Set the gap limit for an address pool + + The gap limit determines how many unused addresses to maintain at the end + of the pool. This is important for wallet recovery and address discovery. + + # Safety + + - `managed_wallet` must be a valid pointer to an FFIManagedWallet + - `error` must be a valid pointer to an FFIError or null + */ + +bool managed_wallet_set_gap_limit(FFIManagedWallet *managed_wallet, + FFINetwork network, + FFIAccountType account_type, + unsigned int account_index, + FFIAddressPoolType pool_type, + unsigned int gap_limit, + FFIError *error) +; + +/* + Generate addresses up to a specific index in a pool + + This ensures that addresses up to and including the specified index exist + in the pool. This is useful for wallet recovery or when specific indices + are needed. + + # Safety + + - `managed_wallet` must be a valid pointer to an FFIManagedWallet + - `wallet` must be a valid pointer to an FFIWallet (for key derivation) + - `error` must be a valid pointer to an FFIError or null + */ + +bool managed_wallet_generate_addresses_to_index(FFIManagedWallet *managed_wallet, + const FFIWallet *wallet, + FFINetwork network, + FFIAccountType account_type, + unsigned int account_index, + FFIAddressPoolType pool_type, + unsigned int target_index, + FFIError *error) +; + +/* + Mark an address as used in the pool + + This updates the pool's tracking of which addresses have been used, + which is important for gap limit management and wallet recovery. + + # Safety + + - `managed_wallet` must be a valid pointer to an FFIManagedWallet + - `address` must be a valid C string + - `error` must be a valid pointer to an FFIError or null + */ + +bool managed_wallet_mark_address_used(FFIManagedWallet *managed_wallet, + FFINetwork network, + const char *address, + FFIError *error) +; + +/* + Get a single address info at a specific index from the pool + + Returns detailed information about the address at the given index, or NULL + if the index is out of bounds or not generated yet. + + # Safety + + - `pool` must be a valid pointer to an FFIAddressPool + - `error` must be a valid pointer to an FFIError or null + - The returned FFIAddressInfo must be freed using `address_info_free` + */ + +FFIAddressInfo *address_pool_get_address_at_index(const FFIAddressPool *pool, + uint32_t index, + FFIError *error) +; + +/* + Get a range of addresses from the pool + + Returns an array of FFIAddressInfo structures for addresses in the range [start_index, end_index). + The count_out parameter will be set to the actual number of addresses returned. + + Note: This function only reads existing addresses from the pool. It does not generate new addresses. + Use managed_wallet_generate_addresses_to_index if you need to generate addresses first. + + # Safety + + - `pool` must be a valid pointer to an FFIAddressPool + - `count_out` must be a valid pointer to store the count + - `error` must be a valid pointer to an FFIError or null + - The returned array must be freed using `address_info_array_free` + */ + +FFIAddressInfo **address_pool_get_addresses_in_range(const FFIAddressPool *pool, + uint32_t start_index, + uint32_t end_index, + size_t *count_out, + FFIError *error) +; + +/* + Free a single FFIAddressInfo structure + + # Safety + + - `info` must be a valid pointer to an FFIAddressInfo allocated by this library or null + - The pointer must not be used after calling this function + */ + void address_info_free(FFIAddressInfo *info) ; + +/* + Free an array of FFIAddressInfo structures + + # Safety + + - `infos` must be a valid pointer to an array of FFIAddressInfo pointers allocated by this library or null + - `count` must be the exact number of elements in the array + - The pointers must not be used after calling this function + */ + +void address_info_array_free(FFIAddressInfo **infos, + size_t count) +; + +/* + Create a new master extended private key from seed + + # Safety + + - `seed` must be a valid pointer to a byte array of `seed_len` length + - `error` must be a valid pointer to an FFIError structure or null + - The caller must ensure the seed pointer remains valid for the duration of this call + */ + +FFIExtendedPrivKey *derivation_new_master_key(const uint8_t *seed, + size_t seed_len, + FFINetwork network, + FFIError *error) +; + +/* + Derive a BIP44 account path (m/44'/5'/account') + */ + +bool derivation_bip44_account_path(FFINetwork network, + unsigned int account_index, + char *path_out, + size_t path_max_len, + FFIError *error) +; + +/* + Derive a BIP44 payment path (m/44'/5'/account'/change/index) + */ + +bool derivation_bip44_payment_path(FFINetwork network, + unsigned int account_index, + bool is_change, + unsigned int address_index, + char *path_out, + size_t path_max_len, + FFIError *error) +; + +/* + Derive CoinJoin path (m/9'/5'/4'/account') + */ + +bool derivation_coinjoin_path(FFINetwork network, + unsigned int account_index, + char *path_out, + size_t path_max_len, + FFIError *error) +; + +/* + Derive identity registration path (m/9'/5'/5'/1'/index') + */ + +bool derivation_identity_registration_path(FFINetwork network, + unsigned int identity_index, + char *path_out, + size_t path_max_len, + FFIError *error) +; + +/* + Derive identity top-up path (m/9'/5'/5'/2'/identity_index'/top_up_index') + */ + +bool derivation_identity_topup_path(FFINetwork network, + unsigned int identity_index, + unsigned int topup_index, + char *path_out, + size_t path_max_len, + FFIError *error) +; + +/* + Derive identity authentication path (m/9'/5'/5'/0'/identity_index'/key_index') + */ + +bool derivation_identity_authentication_path(FFINetwork network, + unsigned int identity_index, + unsigned int key_index, + char *path_out, + size_t path_max_len, + FFIError *error) +; + +/* + Derive private key for a specific path from seed + + # Safety + + - `seed` must be a valid pointer to a byte array of `seed_len` length + - `path` must be a valid pointer to a null-terminated C string + - `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 + */ + +FFIExtendedPrivKey *derivation_derive_private_key_from_seed(const uint8_t *seed, + size_t seed_len, + const char *path, + FFINetwork network, + FFIError *error) +; + +/* + Derive public key from extended private key + + # Safety + + - `xpriv` must be a valid pointer to an FFIExtendedPrivKey + - `error` must be a valid pointer to an FFIError + - The returned pointer must be freed with `extended_public_key_free` + */ + FFIExtendedPubKey *derivation_xpriv_to_xpub(const FFIExtendedPrivKey *xpriv, FFIError *error) ; + +/* + Get extended private key as string + + # Safety + + - `xpriv` must be a valid pointer to an FFIExtendedPrivKey + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + char *derivation_xpriv_to_string(const FFIExtendedPrivKey *xpriv, FFIError *error) ; + +/* + Get extended public key as string + + # Safety + + - `xpub` must be a valid pointer to an FFIExtendedPubKey + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + char *derivation_xpub_to_string(const FFIExtendedPubKey *xpub, FFIError *error) ; + +/* + Get fingerprint from extended public key (4 bytes) + + # Safety + + - `xpub` must be a valid pointer to an FFIExtendedPubKey + - `fingerprint_out` must be a valid pointer to a buffer of at least 4 bytes + - `error` must be a valid pointer to an FFIError + */ + +bool derivation_xpub_fingerprint(const FFIExtendedPubKey *xpub, + uint8_t *fingerprint_out, + FFIError *error) +; + +/* + Free extended private key + + # Safety + + - `xpriv` must be a valid pointer to an FFIExtendedPrivKey that was allocated by this library + - The pointer must not be used after calling this function + - This function must only be called once per allocation + */ + void derivation_xpriv_free(FFIExtendedPrivKey *xpriv) ; + +/* + Free extended public key + + # Safety + + - `xpub` must be a valid pointer to an FFIExtendedPubKey that was allocated by this library + - The pointer must not be used after calling this function + - This function must only be called once per allocation + */ + void derivation_xpub_free(FFIExtendedPubKey *xpub) ; + +/* + Free derivation path string + + # Safety + + - `s` must be a valid pointer to a C string that was allocated by this library + - The pointer must not be used after calling this function + - This function must only be called once per allocation + */ + void derivation_string_free(char *s) ; + +/* + Derive key using DIP9 path constants for identity + + # Safety + + - `seed` must be a valid pointer to a byte array of `seed_len` length + - `error` must be a valid pointer to an FFIError structure or null + - The caller must ensure the seed pointer remains valid for the duration of this call + */ + +FFIExtendedPrivKey *dip9_derive_identity_key(const uint8_t *seed, + size_t seed_len, + FFINetwork network, + unsigned int identity_index, + unsigned int key_index, + FFIDerivationPathType key_type, + FFIError *error) +; + +/* + Free an error message + + # Safety + + - `message` must be a valid pointer to a C string that was allocated by this library + - The pointer must not be used after calling this function + - This function must only be called once per allocation + */ + void error_message_free(char *message) ; + +/* + Get extended private key for account + + # Safety + + - `wallet` must be a valid pointer to an FFIWallet + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + +char *wallet_get_account_xpriv(const FFIWallet *wallet, + FFINetwork network, + unsigned int account_index, + FFIError *error) +; + +/* + Get extended public key for account + + # Safety + + - `wallet` must be a valid pointer to an FFIWallet + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + +char *wallet_get_account_xpub(const FFIWallet *wallet, + FFINetwork network, + unsigned int account_index, + FFIError *error) +; + +/* + Derive private key at a specific path + Returns an opaque FFIPrivateKey pointer that must be freed with private_key_free + + # Safety + + - `wallet` must be a valid pointer to an FFIWallet + - `derivation_path` must be a valid null-terminated C string + - `error` must be a valid pointer to an FFIError + - The returned pointer must be freed with `private_key_free` + */ + +FFIPrivateKey *wallet_derive_private_key(const FFIWallet *wallet, + FFINetwork network, + const char *derivation_path, + FFIError *error) +; + +/* + Derive extended private key at a specific path + Returns an opaque FFIExtendedPrivateKey pointer that must be freed with extended_private_key_free + + # Safety + + - `wallet` must be a valid pointer to an FFIWallet + - `derivation_path` must be a valid null-terminated C string + - `error` must be a valid pointer to an FFIError + - The returned pointer must be freed with `extended_private_key_free` + */ + +FFIExtendedPrivateKey *wallet_derive_extended_private_key(const FFIWallet *wallet, + FFINetwork network, + const char *derivation_path, + FFIError *error) +; + +/* + Derive private key at a specific path and return as WIF string + + # Safety + + - `wallet` must be a valid pointer to an FFIWallet + - `derivation_path` must be a valid null-terminated C string + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + +char *wallet_derive_private_key_as_wif(const FFIWallet *wallet, + FFINetwork network, + const char *derivation_path, + FFIError *error) +; + +/* + Free a private key + + # Safety + + - `key` must be a valid pointer created by private key functions or null + - After calling this function, the pointer becomes invalid + */ + void private_key_free(FFIPrivateKey *key) ; + +/* + Free an extended private key + + # Safety + + - `key` must be a valid pointer created by extended private key functions or null + - After calling this function, the pointer becomes invalid + */ + void extended_private_key_free(FFIExtendedPrivateKey *key) ; + +/* + Get extended private key as string (xprv format) + + Returns the extended private key in base58 format (xprv... for mainnet, tprv... for testnet) + + # Safety + + - `key` must be a valid pointer to an FFIExtendedPrivateKey + - `network` is ignored; the network is encoded in the extended key + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + +char *extended_private_key_to_string(const FFIExtendedPrivateKey *key, + FFINetwork network, + FFIError *error) +; + +/* + Get the private key from an extended private key + + Extracts the non-extended private key from an extended private key. + + # Safety + + - `extended_key` must be a valid pointer to an FFIExtendedPrivateKey + - `error` must be a valid pointer to an FFIError + - The returned FFIPrivateKey must be freed with `private_key_free` + */ + +FFIPrivateKey *extended_private_key_get_private_key(const FFIExtendedPrivateKey *extended_key, + FFIError *error) +; + +/* + Get private key as WIF string from FFIPrivateKey + + # Safety + + - `key` must be a valid pointer to an FFIPrivateKey + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + char *private_key_to_wif(const FFIPrivateKey *key, FFINetwork network, FFIError *error) ; + +/* + Derive public key at a specific path + Returns an opaque FFIPublicKey pointer that must be freed with public_key_free + + # Safety + + - `wallet` must be a valid pointer to an FFIWallet + - `derivation_path` must be a valid null-terminated C string + - `error` must be a valid pointer to an FFIError + - The returned pointer must be freed with `public_key_free` + */ + +FFIPublicKey *wallet_derive_public_key(const FFIWallet *wallet, + FFINetwork network, + const char *derivation_path, + FFIError *error) +; + +/* + Derive extended public key at a specific path + Returns an opaque FFIExtendedPublicKey pointer that must be freed with extended_public_key_free + + # Safety + + - `wallet` must be a valid pointer to an FFIWallet + - `derivation_path` must be a valid null-terminated C string + - `error` must be a valid pointer to an FFIError + - The returned pointer must be freed with `extended_public_key_free` + */ + +FFIExtendedPublicKey *wallet_derive_extended_public_key(const FFIWallet *wallet, + FFINetwork network, + const char *derivation_path, + FFIError *error) +; + +/* + Derive public key at a specific path and return as hex string + + # Safety + + - `wallet` must be a valid pointer to an FFIWallet + - `derivation_path` must be a valid null-terminated C string + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + +char *wallet_derive_public_key_as_hex(const FFIWallet *wallet, + FFINetwork network, + const char *derivation_path, + FFIError *error) +; + +/* + Free a public key + + # Safety + + - `key` must be a valid pointer created by public key functions or null + - After calling this function, the pointer becomes invalid + */ + void public_key_free(FFIPublicKey *key) ; + +/* + Free an extended public key + + # Safety + + - `key` must be a valid pointer created by extended public key functions or null + - After calling this function, the pointer becomes invalid + */ + void extended_public_key_free(FFIExtendedPublicKey *key) ; + +/* + Get extended public key as string (xpub format) + + Returns the extended public key in base58 format (xpub... for mainnet, tpub... for testnet) + + # Safety + + - `key` must be a valid pointer to an FFIExtendedPublicKey + - `network` is ignored; the network is encoded in the extended key + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + +char *extended_public_key_to_string(const FFIExtendedPublicKey *key, + FFINetwork network, + FFIError *error) +; + +/* + Get the public key from an extended public key + + Extracts the non-extended public key from an extended public key. + + # Safety + + - `extended_key` must be a valid pointer to an FFIExtendedPublicKey + - `error` must be a valid pointer to an FFIError + - The returned FFIPublicKey must be freed with `public_key_free` + */ + +FFIPublicKey *extended_public_key_get_public_key(const FFIExtendedPublicKey *extended_key, + FFIError *error) +; + +/* + Get public key as hex string from FFIPublicKey + + # Safety + + - `key` must be a valid pointer to an FFIPublicKey + - `error` must be a valid pointer to an FFIError + - The returned string must be freed with `string_free` + */ + char *public_key_to_hex(const FFIPublicKey *key, FFIError *error) ; + +/* + Convert derivation path string to indices + + # Safety + + - `path` must be a valid null-terminated C string or null + - `indices_out` must be a valid pointer to store the indices array pointer + - `hardened_out` must be a valid pointer to store the hardened flags array pointer + - `count_out` must be a valid pointer to store the count + - `error` must be a valid pointer to an FFIError + - The returned arrays must be freed with `derivation_path_free` + */ + +bool derivation_path_parse(const char *path, + uint32_t **indices_out, + bool **hardened_out, + size_t *count_out, + FFIError *error) +; + +/* + Free derivation path arrays + Note: This function expects the count to properly free the slices + + # Safety + + - `indices` must be a valid pointer created by `derivation_path_parse` or null + - `hardened` must be a valid pointer created by `derivation_path_parse` or null + - `count` must match the count from `derivation_path_parse` + - After calling this function, the pointers become invalid + */ + void derivation_path_free(uint32_t *indices, bool *hardened, size_t count) ; + +/* + Get a managed account from a managed wallet + + This function gets a ManagedAccount from the wallet manager's managed wallet info, + returning a managed account handle that wraps the ManagedAccount. + + # Safety + + - `manager` must be a valid pointer to an FFIWalletManager instance + - `wallet_id` must be a valid pointer to a 32-byte wallet ID + - `network` must specify exactly one network + - The caller must ensure all pointers remain valid for the duration of this call + - The returned account must be freed with `managed_account_free` when no longer needed + */ + +FFIManagedAccountResult managed_wallet_get_account(const FFIWalletManager *manager, + const uint8_t *wallet_id, + FFINetwork network, + unsigned int account_index, + FFIAccountType account_type) +; + +/* + Get a managed IdentityTopUp account with a specific registration index + + This is used for top-up accounts that are bound to a specific identity. + Returns a managed account handle that wraps the ManagedAccount. + + # Safety + + - `manager` must be a valid pointer to an FFIWalletManager instance + - `wallet_id` must be a valid pointer to a 32-byte wallet ID + - `network` must specify exactly one network + - The caller must ensure all pointers remain valid for the duration of this call + - The returned account must be freed with `managed_account_free` when no longer needed + */ + +FFIManagedAccountResult managed_wallet_get_top_up_account_with_registration_index(const FFIWalletManager *manager, + const uint8_t *wallet_id, + FFINetwork network, + unsigned int registration_index) +; + +/* + Get the network of a managed account + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + */ + FFINetwork managed_account_get_network(const FFIManagedAccount *account) ; + +/* + Get the parent wallet ID of a managed account + + Note: ManagedAccount doesn't store the parent wallet ID directly. + The wallet ID is typically known from the context (e.g., when getting the account from a managed wallet). + + # Safety + + - `wallet_id` must be a valid pointer to a 32-byte wallet ID buffer that was provided by the caller + - The returned pointer is the same as the input pointer for convenience + - The caller must not free the returned pointer as it's the same as the input + */ + +const uint8_t *managed_account_get_parent_wallet_id(const uint8_t *wallet_id) +; + +/* + Get the account type of a managed account + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + - `index_out` must be a valid pointer to receive the account index (or null) + */ + +FFIAccountType managed_account_get_account_type(const FFIManagedAccount *account, + unsigned int *index_out) +; + +/* + Check if a managed account is watch-only + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + */ + bool managed_account_get_is_watch_only(const FFIManagedAccount *account) ; + +/* + Get the balance of a managed account + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + - `balance_out` must be a valid pointer to an FFIBalance structure + */ + bool managed_account_get_balance(const FFIManagedAccount *account, FFIBalance *balance_out) ; + +/* + Get the number of transactions in a managed account + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + */ + unsigned int managed_account_get_transaction_count(const FFIManagedAccount *account) ; + +/* + Get the number of UTXOs in a managed account + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + */ + unsigned int managed_account_get_utxo_count(const FFIManagedAccount *account) ; + +/* + Free a managed account handle + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount that was allocated by this library + - The pointer must not be used after calling this function + - This function must only be called once per allocation + */ + void managed_account_free(FFIManagedAccount *account) ; + +/* + Free a managed account result's error message (if any) + Note: This does NOT free the account handle itself - use managed_account_free for that + + # Safety + + - `result` must be a valid pointer to an FFIManagedAccountResult + - The error_message field must be either null or a valid CString allocated by this library + - The caller must ensure the result pointer remains valid for the duration of this call + */ + void managed_account_result_free_error(FFIManagedAccountResult *result) ; + +/* + Get number of accounts in a managed wallet + + # Safety + + - `manager` must be a valid pointer to an FFIWalletManager instance + - `wallet_id` must be a valid pointer to a 32-byte wallet ID + - `network` must specify exactly one network + - `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 + */ + +unsigned int managed_wallet_get_account_count(const FFIWalletManager *manager, + const uint8_t *wallet_id, + FFINetwork network, + FFIError *error) +; + +/* + Get the account index from a managed account + + Returns the primary account index for Standard and CoinJoin accounts. + Returns 0 for account types that don't have an index (like Identity or Provider accounts). + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + */ + unsigned int managed_account_get_index(const FFIManagedAccount *account) ; + +/* + Get the external address pool from a managed account + + This function returns the external (receive) address pool for Standard accounts. + Returns NULL for account types that don't have separate external/internal pools. + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + - The returned pool must be freed with `address_pool_free` when no longer needed + */ + FFIAddressPool *managed_account_get_external_address_pool(const FFIManagedAccount *account) ; + +/* + Get the internal address pool from a managed account + + This function returns the internal (change) address pool for Standard accounts. + Returns NULL for account types that don't have separate external/internal pools. + + # Safety + + - `account` must be a valid pointer to an FFIManagedAccount instance + - The returned pool must be freed with `address_pool_free` when no longer needed + */ + FFIAddressPool *managed_account_get_internal_address_pool(const FFIManagedAccount *account) ; + +/* + Get an address pool from a managed account by type + + This function returns the appropriate address pool based on the pool type parameter. + For Standard accounts with External/Internal pool types, returns the corresponding pool. + For non-standard accounts with Single pool type, returns their single address pool. + + # Safety + + - `manager` must be a valid pointer to an FFIWalletManager instance + - `account` must be a valid pointer to an FFIManagedAccount instance + - `wallet_id` must be a valid pointer to a 32-byte wallet ID + - The returned pool must be freed with `address_pool_free` when no longer needed + */ + +FFIAddressPool *managed_account_get_address_pool(const FFIManagedAccount *account, + FFIAddressPoolType pool_type) +; + +/* + Get managed account collection for a specific network from wallet manager + + # Safety + + - `manager` must be a valid pointer to an FFIWalletManager instance + - `wallet_id` must be a valid pointer to a 32-byte wallet ID + - `error` must be a valid pointer to an FFIError structure or null + - The returned pointer must be freed with `managed_account_collection_free` when no longer needed + */ + +FFIManagedAccountCollection *managed_wallet_get_account_collection(const FFIWalletManager *manager, + const uint8_t *wallet_id, + FFINetwork network, + FFIError *error) +; + +/* + Free a managed account collection handle + + # Safety + + - `collection` must be a valid pointer to an FFIManagedAccountCollection created by this library + - `collection` must not be used after calling this function + */ + void managed_account_collection_free(FFIManagedAccountCollection *collection) ; + +/* + Get a BIP44 account by index from the managed collection + + # Safety + + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed + */ + +FFIManagedAccount *managed_account_collection_get_bip44_account(const FFIManagedAccountCollection *collection, + unsigned int index) +; + +/* + Get all BIP44 account indices from managed collection + + # Safety + + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - `out_indices` must be a valid pointer to store the indices array + - `out_count` must be a valid pointer to store the count + - The returned array must be freed with `free_u32_array` when no longer needed + */ + +bool managed_account_collection_get_bip44_indices(const FFIManagedAccountCollection *collection, + unsigned int **out_indices, + size_t *out_count) +; + +/* + Get a BIP32 account by index from the managed collection + + # Safety + + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed + */ + +FFIManagedAccount *managed_account_collection_get_bip32_account(const FFIManagedAccountCollection *collection, + unsigned int index) +; + +/* + Get all BIP32 account indices from managed collection + + # Safety + + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - `out_indices` must be a valid pointer to store the indices array + - `out_count` must be a valid pointer to store the count + - The returned array must be freed with `free_u32_array` when no longer needed + */ + +bool managed_account_collection_get_bip32_indices(const FFIManagedAccountCollection *collection, + unsigned int **out_indices, + size_t *out_count) +; + +/* + Get a CoinJoin account by index from the managed collection + + # Safety + + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed + */ + +FFIManagedAccount *managed_account_collection_get_coinjoin_account(const FFIManagedAccountCollection *collection, + unsigned int index) +; + +/* + Get all CoinJoin account indices from managed collection + + # Safety + + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - `out_indices` must be a valid pointer to store the indices array + - `out_count` must be a valid pointer to store the count + - The returned array must be freed with `free_u32_array` when no longer needed + */ + +bool managed_account_collection_get_coinjoin_indices(const FFIManagedAccountCollection *collection, + unsigned int **out_indices, + size_t *out_count) +; + +/* + Get the identity registration account if it exists in managed collection # Safety - - `wallet` must be a valid pointer to an FFIWallet - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed */ -char *wallet_get_account_xpriv(const FFIWallet *wallet, - FFINetwork network, - unsigned int account_index, - FFIError *error) +FFIManagedAccount *managed_account_collection_get_identity_registration(const FFIManagedAccountCollection *collection) ; /* - Get extended public key for account + Check if identity registration account exists in managed collection # Safety - - `wallet` must be a valid pointer to an FFIWallet - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection */ -char *wallet_get_account_xpub(const FFIWallet *wallet, - FFINetwork network, - unsigned int account_index, - FFIError *error) +bool managed_account_collection_has_identity_registration(const FFIManagedAccountCollection *collection) ; /* - Derive private key at a specific path - Returns an opaque FFIPrivateKey pointer that must be freed with private_key_free + Get an identity topup account by registration index from managed collection # Safety - - `wallet` must be a valid pointer to an FFIWallet - - `derivation_path` must be a valid null-terminated C string - - `error` must be a valid pointer to an FFIError - - The returned pointer must be freed with `private_key_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed */ -FFIPrivateKey *wallet_derive_private_key(const FFIWallet *wallet, - FFINetwork network, - const char *derivation_path, - FFIError *error) +FFIManagedAccount *managed_account_collection_get_identity_topup(const FFIManagedAccountCollection *collection, + unsigned int registration_index) ; /* - Derive extended private key at a specific path - Returns an opaque FFIExtendedPrivateKey pointer that must be freed with extended_private_key_free + Get all identity topup registration indices from managed collection # Safety - - `wallet` must be a valid pointer to an FFIWallet - - `derivation_path` must be a valid null-terminated C string - - `error` must be a valid pointer to an FFIError - - The returned pointer must be freed with `extended_private_key_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - `out_indices` must be a valid pointer to store the indices array + - `out_count` must be a valid pointer to store the count + - The returned array must be freed with `free_u32_array` when no longer needed */ -FFIExtendedPrivateKey *wallet_derive_extended_private_key(const FFIWallet *wallet, - FFINetwork network, - const char *derivation_path, - FFIError *error) +bool managed_account_collection_get_identity_topup_indices(const FFIManagedAccountCollection *collection, + unsigned int **out_indices, + size_t *out_count) ; /* - Derive private key at a specific path and return as WIF string + Get the identity topup not bound account if it exists in managed collection # Safety - - `wallet` must be a valid pointer to an FFIWallet - - `derivation_path` must be a valid null-terminated C string - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - `manager` must be a valid pointer to an FFIWalletManager + - The returned pointer must be freed with `managed_account_free` when no longer needed */ -char *wallet_derive_private_key_as_wif(const FFIWallet *wallet, - FFINetwork network, - const char *derivation_path, - FFIError *error) +FFIManagedAccount *managed_account_collection_get_identity_topup_not_bound(const FFIManagedAccountCollection *collection) ; /* - Free a private key + Check if identity topup not bound account exists in managed collection # Safety - - `key` must be a valid pointer created by private key functions or null - - After calling this function, the pointer becomes invalid + - `collection` must be a valid pointer to an FFIManagedAccountCollection */ - void private_key_free(FFIPrivateKey *key) ; + +bool managed_account_collection_has_identity_topup_not_bound(const FFIManagedAccountCollection *collection) +; /* - Free an extended private key + Get the identity invitation account if it exists in managed collection # Safety - - `key` must be a valid pointer created by extended private key functions or null - - After calling this function, the pointer becomes invalid + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed */ - void extended_private_key_free(FFIExtendedPrivateKey *key) ; -/* - Get extended private key as string (xprv format) +FFIManagedAccount *managed_account_collection_get_identity_invitation(const FFIManagedAccountCollection *collection) +; - Returns the extended private key in base58 format (xprv... for mainnet, tprv... for testnet) +/* + Check if identity invitation account exists in managed collection # Safety - - `key` must be a valid pointer to an FFIExtendedPrivateKey - - `network` is ignored; the network is encoded in the extended key - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection */ -char *extended_private_key_to_string(const FFIExtendedPrivateKey *key, - FFINetwork network, - FFIError *error) +bool managed_account_collection_has_identity_invitation(const FFIManagedAccountCollection *collection) ; /* - Get the private key from an extended private key - - Extracts the non-extended private key from an extended private key. + Get the provider voting keys account if it exists in managed collection # Safety - - `extended_key` must be a valid pointer to an FFIExtendedPrivateKey - - `error` must be a valid pointer to an FFIError - - The returned FFIPrivateKey must be freed with `private_key_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed */ -FFIPrivateKey *extended_private_key_get_private_key(const FFIExtendedPrivateKey *extended_key, - FFIError *error) +FFIManagedAccount *managed_account_collection_get_provider_voting_keys(const FFIManagedAccountCollection *collection) ; /* - Get private key as WIF string from FFIPrivateKey + Check if provider voting keys account exists in managed collection # Safety - - `key` must be a valid pointer to an FFIPrivateKey - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection */ - char *private_key_to_wif(const FFIPrivateKey *key, FFINetwork network, FFIError *error) ; + +bool managed_account_collection_has_provider_voting_keys(const FFIManagedAccountCollection *collection) +; /* - Derive public key at a specific path - Returns an opaque FFIPublicKey pointer that must be freed with public_key_free + Get the provider owner keys account if it exists in managed collection # Safety - - `wallet` must be a valid pointer to an FFIWallet - - `derivation_path` must be a valid null-terminated C string - - `error` must be a valid pointer to an FFIError - - The returned pointer must be freed with `public_key_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed */ -FFIPublicKey *wallet_derive_public_key(const FFIWallet *wallet, - FFINetwork network, - const char *derivation_path, - FFIError *error) +FFIManagedAccount *managed_account_collection_get_provider_owner_keys(const FFIManagedAccountCollection *collection) ; /* - Derive extended public key at a specific path - Returns an opaque FFIExtendedPublicKey pointer that must be freed with extended_public_key_free + Check if provider owner keys account exists in managed collection # Safety - - `wallet` must be a valid pointer to an FFIWallet - - `derivation_path` must be a valid null-terminated C string - - `error` must be a valid pointer to an FFIError - - The returned pointer must be freed with `extended_public_key_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection */ -FFIExtendedPublicKey *wallet_derive_extended_public_key(const FFIWallet *wallet, - FFINetwork network, - const char *derivation_path, - FFIError *error) +bool managed_account_collection_has_provider_owner_keys(const FFIManagedAccountCollection *collection) ; /* - Derive public key at a specific path and return as hex string + Get the provider operator keys account if it exists in managed collection + Note: This function is only available when the `bls` feature is enabled # Safety - - `wallet` must be a valid pointer to an FFIWallet - - `derivation_path` must be a valid null-terminated C string - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_free` when no longer needed */ -char *wallet_derive_public_key_as_hex(const FFIWallet *wallet, - FFINetwork network, - const char *derivation_path, - FFIError *error) +FFIManagedAccount *managed_account_collection_get_provider_operator_keys(const FFIManagedAccountCollection *collection) ; /* - Free a public key + Get the provider operator keys account if it exists (stub when BLS is disabled) + */ + +FFIManagedAccount *managed_account_collection_get_provider_operator_keys(const FFIManagedAccountCollection *_collection) +; + +/* + Check if provider operator keys account exists in managed collection # Safety - - `key` must be a valid pointer created by public key functions or null - - After calling this function, the pointer becomes invalid + - `collection` must be a valid pointer to an FFIManagedAccountCollection */ - void public_key_free(FFIPublicKey *key) ; + +bool managed_account_collection_has_provider_operator_keys(const FFIManagedAccountCollection *collection) +; /* - Free an extended public key + Get the provider platform keys account if it exists in managed collection + Note: This function is only available when the `eddsa` feature is enabled # Safety - - `key` must be a valid pointer created by extended public key functions or null - - After calling this function, the pointer becomes invalid + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - `manager` must be a valid pointer to an FFIWalletManager + - The returned pointer must be freed with `managed_account_free` when no longer needed */ - void extended_public_key_free(FFIExtendedPublicKey *key) ; + +FFIManagedAccount *managed_account_collection_get_provider_platform_keys(const FFIManagedAccountCollection *collection) +; /* - Get extended public key as string (xpub format) + Get the provider platform keys account if it exists (stub when EdDSA is disabled) + */ - Returns the extended public key in base58 format (xpub... for mainnet, tpub... for testnet) +FFIManagedAccount *managed_account_collection_get_provider_platform_keys(const FFIManagedAccountCollection *_collection) +; + +/* + Check if provider platform keys account exists in managed collection # Safety - - `key` must be a valid pointer to an FFIExtendedPublicKey - - `network` is ignored; the network is encoded in the extended key - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection */ -char *extended_public_key_to_string(const FFIExtendedPublicKey *key, - FFINetwork network, - FFIError *error) +bool managed_account_collection_has_provider_platform_keys(const FFIManagedAccountCollection *collection) ; /* - Get the public key from an extended public key - - Extracts the non-extended public key from an extended public key. + Get the total number of accounts in the managed collection # Safety - - `extended_key` must be a valid pointer to an FFIExtendedPublicKey - - `error` must be a valid pointer to an FFIError - - The returned FFIPublicKey must be freed with `public_key_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection */ - -FFIPublicKey *extended_public_key_get_public_key(const FFIExtendedPublicKey *extended_key, - FFIError *error) -; + unsigned int managed_account_collection_count(const FFIManagedAccountCollection *collection) ; /* - Get public key as hex string from FFIPublicKey + Get a human-readable summary of all accounts in the managed collection + + Returns a formatted string showing all account types and their indices. + The format is designed to be clear and readable for end users. # Safety - - `key` must be a valid pointer to an FFIPublicKey - - `error` must be a valid pointer to an FFIError - - The returned string must be freed with `string_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned string must be freed with `string_free` when no longer needed + - Returns null if the collection pointer is null */ - char *public_key_to_hex(const FFIPublicKey *key, FFIError *error) ; + char *managed_account_collection_summary(const FFIManagedAccountCollection *collection) ; /* - Convert derivation path string to indices + Get structured account collection summary data for managed collection + + Returns a struct containing arrays of indices for each account type and boolean + flags for special accounts. This provides Swift with programmatic access to + account information. # Safety - - `path` must be a valid null-terminated C string or null - - `indices_out` must be a valid pointer to store the indices array pointer - - `hardened_out` must be a valid pointer to store the hardened flags array pointer - - `count_out` must be a valid pointer to store the count - - `error` must be a valid pointer to an FFIError - - The returned arrays must be freed with `derivation_path_free` + - `collection` must be a valid pointer to an FFIManagedAccountCollection + - The returned pointer must be freed with `managed_account_collection_summary_free` when no longer needed + - Returns null if the collection pointer is null */ -bool derivation_path_parse(const char *path, - uint32_t **indices_out, - bool **hardened_out, - size_t *count_out, - FFIError *error) +FFIManagedAccountCollectionSummary *managed_account_collection_summary_data(const FFIManagedAccountCollection *collection) ; /* - Free derivation path arrays - Note: This function expects the count to properly free the slices + Free a managed account collection summary and all its allocated memory # Safety - - `indices` must be a valid pointer created by `derivation_path_parse` or null - - `hardened` must be a valid pointer created by `derivation_path_parse` or null - - `count` must match the count from `derivation_path_parse` - - After calling this function, the pointers become invalid + - `summary` must be a valid pointer to an FFIManagedAccountCollectionSummary created by `managed_account_collection_summary_data` + - `summary` must not be used after calling this function */ - void derivation_path_free(uint32_t *indices, bool *hardened, size_t count) ; + +void managed_account_collection_summary_free(FFIManagedAccountCollectionSummary *summary) +; /* Get the next unused receive address @@ -1502,26 +2947,6 @@ bool wallet_generate_provider_key(const FFIWallet *wallet, */ void provider_key_info_free(FFIProviderKeyInfo *info) ; -/* - Get the address for a provider key - - This returns the P2PKH address corresponding to the provider key at - the specified index. This is useful for funding provider accounts. - - # Safety - - - `wallet` must be a valid pointer to an FFIWallet - - `error` must be a valid pointer to an FFIError or null - - The returned string must be freed by the caller - */ - -char *wallet_get_provider_key_address(const FFIWallet *wallet, - FFINetwork network, - FFIProviderKeyType key_type, - unsigned int _key_index, - FFIError *error) -; - /* Sign data with a provider key @@ -1564,7 +2989,7 @@ bool wallet_sign_with_provider_key(const FFIWallet *wallet, */ bool wallet_build_transaction(FFIWallet *wallet, - FFINetwork network, + FFINetwork _network, unsigned int account_index, const FFITxOutput *outputs, size_t outputs_count, @@ -1588,7 +3013,7 @@ bool wallet_build_transaction(FFIWallet *wallet, */ bool wallet_sign_transaction(const FFIWallet *wallet, - FFINetwork network, + FFINetwork _network, const uint8_t *tx_bytes, size_t tx_len, uint8_t **signed_tx_out, @@ -1785,13 +3210,13 @@ bool wallet_get_utxos(const FFIWallet *_wallet, FFIWallet *wallet_create_from_mnemonic_with_options(const char *mnemonic, const char *passphrase, - FFINetwork network, + FFINetwork networks, const FFIWalletAccountCreationOptions *account_options, FFIError *error) ; /* - Create a new wallet from mnemonic (backward compatibility) + Create a new wallet from mnemonic (backward compatibility - single network) # Safety @@ -1821,7 +3246,7 @@ FFIWallet *wallet_create_from_mnemonic(const char *mnemonic, FFIWallet *wallet_create_from_seed_with_options(const uint8_t *seed, size_t seed_len, - FFINetwork network, + FFINetwork networks, const FFIWalletAccountCreationOptions *account_options, FFIError *error) ; @@ -1842,34 +3267,6 @@ FFIWallet *wallet_create_from_seed(const uint8_t *seed, FFIError *error) ; -/* - Create a new wallet from seed bytes - - # Safety - - - `seed_bytes` must be a valid pointer to a byte array of `seed_len` length - - `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 pointer must be freed with `wallet_free` when no longer needed - */ - -FFIWallet *wallet_create_from_seed_bytes(const uint8_t *seed_bytes, - size_t seed_len, - FFINetwork network, - FFIError *error) -; - -/* - Create a watch-only wallet from extended public key - - # Safety - - - `xpub` must be a valid pointer to a null-terminated C string - - `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 - */ - FFIWallet *wallet_create_from_xpub(const char *xpub, FFINetwork network, FFIError *error) ; - /* Create a new random wallet with options @@ -1880,7 +3277,7 @@ FFIWallet *wallet_create_from_seed_bytes(const uint8_t *seed_bytes, - The caller must ensure all pointers remain valid for the duration of this call */ -FFIWallet *wallet_create_random_with_options(FFINetwork network, +FFIWallet *wallet_create_random_with_options(FFINetwork networks, const FFIWalletAccountCreationOptions *account_options, FFIError *error) ; @@ -1986,7 +3383,7 @@ char *wallet_get_xpub(const FFIWallet *wallet, FFIAccountResult wallet_add_account(FFIWallet *wallet, FFINetwork network, - unsigned int account_type, + FFIAccountType account_type, unsigned int account_index) ; @@ -2004,7 +3401,7 @@ FFIAccountResult wallet_add_account(FFIWallet *wallet, FFIAccountResult wallet_add_account_with_xpub_bytes(FFIWallet *wallet, FFINetwork network, - unsigned int account_type, + FFIAccountType account_type, unsigned int account_index, const uint8_t *xpub_bytes, size_t xpub_len) @@ -2024,7 +3421,7 @@ FFIAccountResult wallet_add_account_with_xpub_bytes(FFIWallet *wallet, FFIAccountResult wallet_add_account_with_string_xpub(FFIWallet *wallet, FFINetwork network, - unsigned int account_type, + FFIAccountType account_type, unsigned int account_index, const char *xpub_string) ; @@ -2074,6 +3471,81 @@ bool wallet_manager_add_wallet_from_mnemonic(FFIWalletManager *manager, FFIError *error) ; +/* + Add a wallet from mnemonic to the manager and return serialized bytes + + Creates a wallet from a mnemonic phrase, adds it to the manager, optionally downgrading it + to a pubkey-only wallet (watch-only or externally signable), and returns the serialized wallet bytes. + + # Safety + + - `manager` must be a valid pointer to an FFIWalletManager instance + - `mnemonic` must be a valid pointer to a null-terminated C string + - `passphrase` must be a valid pointer to a null-terminated C string or null + - `birth_height` is optional, pass 0 for default + - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null + - `downgrade_to_pubkey_wallet` if true, creates a watch-only or externally signable wallet + - `allow_external_signing` if true AND downgrade_to_pubkey_wallet is true, creates an externally signable wallet + - `wallet_bytes_out` must be a valid pointer to a pointer that will receive the serialized bytes + - `wallet_bytes_len_out` must be a valid pointer that will receive the byte length + - `wallet_id_out` must be a valid pointer to a 32-byte array that will receive the wallet ID + - `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 caller must free the returned wallet_bytes using wallet_manager_free_wallet_bytes() + */ + +bool wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes(FFIWalletManager *manager, + const char *mnemonic, + const char *passphrase, + FFINetwork network, + unsigned int birth_height, + const FFIWalletAccountCreationOptions *account_options, + bool downgrade_to_pubkey_wallet, + bool allow_external_signing, + uint8_t **wallet_bytes_out, + size_t *wallet_bytes_len_out, + uint8_t *wallet_id_out, + FFIError *error) +; + +/* + Free wallet bytes buffer + + # Safety + + - `wallet_bytes` must be a valid pointer to a buffer allocated by wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes + - `bytes_len` must match the original allocation size + - The pointer must not be used after calling this function + - This function must only be called once per buffer + */ + +void wallet_manager_free_wallet_bytes(uint8_t *wallet_bytes, + size_t bytes_len) +; + +/* + Import a wallet from bincode-serialized bytes + + Deserializes a wallet from bytes and adds it to the manager. + Returns a 32-byte wallet ID on success. + + # Safety + + - `manager` must be a valid pointer to an FFIWalletManager instance + - `wallet_bytes` must be a valid pointer to bincode-serialized wallet bytes + - `wallet_bytes_len` must be the exact length of the wallet bytes + - `wallet_id_out` must be a valid pointer to a 32-byte array that will receive the wallet ID + - `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 + */ + +bool wallet_manager_import_wallet_from_bytes(FFIWalletManager *manager, + const uint8_t *wallet_bytes, + size_t wallet_bytes_len, + uint8_t *wallet_id_out, + FFIError *error) +; + /* Get wallet IDs diff --git a/key-wallet-ffi/src/account.rs b/key-wallet-ffi/src/account.rs index c920c71a4..4dffb9362 100644 --- a/key-wallet-ffi/src/account.rs +++ b/key-wallet-ffi/src/account.rs @@ -1,9 +1,75 @@ //! Account management functions use std::os::raw::c_uint; +use std::sync::Arc; use crate::error::{FFIError, FFIErrorCode}; -use crate::types::{FFIAccount, FFIAccountResult, FFIAccountType, FFINetwork, FFIWallet}; +use crate::types::{FFIAccountResult, FFIAccountType, FFINetwork, FFIWallet}; +#[cfg(feature = "bls")] +use key_wallet::account::BLSAccount; +#[cfg(feature = "eddsa")] +use key_wallet::account::EdDSAAccount; + +/// Opaque account handle +pub struct FFIAccount { + pub(crate) account: Arc, +} + +impl FFIAccount { + /// Create a new FFI account handle + pub fn new(account: &key_wallet::Account) -> Self { + FFIAccount { + account: Arc::new(account.clone()), + } + } + + /// Get a reference to the inner account + pub fn inner(&self) -> &key_wallet::Account { + self.account.as_ref() + } +} + +/// Opaque BLS account handle +#[cfg(feature = "bls")] +pub struct FFIBLSAccount { + pub(crate) account: Arc, +} + +#[cfg(feature = "bls")] +impl FFIBLSAccount { + /// Create a new FFI BLS account handle + pub fn new(account: &BLSAccount) -> Self { + FFIBLSAccount { + account: Arc::new(account.clone()), + } + } + + /// Get a reference to the inner BLS account + pub fn inner(&self) -> &BLSAccount { + self.account.as_ref() + } +} + +/// Opaque EdDSA account handle +#[cfg(feature = "eddsa")] +pub struct FFIEdDSAAccount { + pub(crate) account: Arc, +} + +#[cfg(feature = "eddsa")] +impl FFIEdDSAAccount { + /// Create a new FFI EdDSA account handle + pub fn new(account: &EdDSAAccount) -> Self { + FFIEdDSAAccount { + account: Arc::new(account.clone()), + } + } + + /// Get a reference to the inner EdDSA account + pub fn inner(&self) -> &EdDSAAccount { + self.account.as_ref() + } +} /// Get an account handle for a specific account type /// Returns a result containing either the account handle or an error @@ -17,55 +83,29 @@ pub unsafe extern "C" fn wallet_get_account( wallet: *const FFIWallet, network: FFINetwork, account_index: c_uint, - account_type: c_uint, + account_type: FFIAccountType, ) -> FFIAccountResult { if wallet.is_null() { return FFIAccountResult::error(FFIErrorCode::InvalidInput, "Wallet is null".to_string()); } let wallet = &*wallet; - let network_rust: key_wallet::Network = network.into(); - - let account_type_enum = match account_type { - 0 => FFIAccountType::StandardBIP44, - 1 => FFIAccountType::StandardBIP32, - 2 => FFIAccountType::CoinJoin, - 3 => FFIAccountType::IdentityRegistration, - 4 => { - // IdentityTopUp requires a registration_index - return FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "IdentityTopUp accounts require a registration_index. Use wallet_get_top_up_account_with_registration_index instead".to_string(), - ); - } - 5 => FFIAccountType::IdentityTopUpNotBoundToIdentity, - 6 => FFIAccountType::IdentityInvitation, - 7 => FFIAccountType::ProviderVotingKeys, - 8 => FFIAccountType::ProviderOwnerKeys, - 9 => FFIAccountType::ProviderOperatorKeys, - 10 => FFIAccountType::ProviderPlatformKeys, - _ => { + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { return FFIAccountResult::error( FFIErrorCode::InvalidInput, - format!("Invalid account type: {}", account_type), + "Must specify exactly one network".to_string(), ); } }; - let account_type = match account_type_enum.to_account_type(account_index, None) { - Some(at) => at, - None => { - return FFIAccountResult::error( - FFIErrorCode::InvalidInput, - format!("Missing required parameters for account type {}", account_type), - ); - } - }; + let account_type_rust = account_type.to_account_type(account_index); match wallet .inner() .accounts_on_network(network_rust) - .and_then(|account_collection| account_collection.account_of_type(account_type)) + .and_then(|account_collection| account_collection.account_of_type(account_type_rust)) { Some(account) => { let ffi_account = FFIAccount::new(account); @@ -94,7 +134,15 @@ pub unsafe extern "C" fn wallet_get_top_up_account_with_registration_index( } let wallet = &*wallet; - let network_rust: key_wallet::Network = network.into(); + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + return FFIAccountResult::error( + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + } + }; // This function is specifically for IdentityTopUp accounts let account_type = key_wallet::AccountType::IdentityTopUp { @@ -134,6 +182,36 @@ pub unsafe extern "C" fn account_free(account: *mut FFIAccount) { } } +/// Free a BLS account handle +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIBLSAccount +/// - The pointer must not be used after calling this function +/// - This function must only be called once per allocation +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn bls_account_free(account: *mut FFIBLSAccount) { + if !account.is_null() { + let _ = Box::from_raw(account); + } +} + +/// Free an EdDSA account handle +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIEdDSAAccount +/// - The pointer must not be used after calling this function +/// - This function must only be called once per allocation +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn eddsa_account_free(account: *mut FFIEdDSAAccount) { + if !account.is_null() { + let _ = Box::from_raw(account); + } +} + /// Free an account result's error message (if any) /// Note: This does NOT free the account handle itself - use account_free for that /// @@ -153,6 +231,346 @@ pub unsafe extern "C" fn account_result_free_error(result: *mut FFIAccountResult } } +/// Get the extended public key of an account as a string +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIAccount instance +/// - The returned string must be freed by the caller using `string_free` +/// - Returns NULL if the account is null +#[no_mangle] +pub unsafe extern "C" fn account_get_extended_public_key_as_string( + account: *const FFIAccount, +) -> *mut std::os::raw::c_char { + if account.is_null() { + return std::ptr::null_mut(); + } + + let account = &*account; + let xpub = account.inner().extended_public_key(); + + match std::ffi::CString::new(xpub.to_string()) { + Ok(c_str) => c_str.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Get the network of an account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIAccount instance +/// - Returns FFINetwork::NoNetworks if the account is null +#[no_mangle] +pub unsafe extern "C" fn account_get_network(account: *const FFIAccount) -> FFINetwork { + if account.is_null() { + return FFINetwork::NoNetworks; + } + + let account = &*account; + account.inner().network.into() +} + +/// Get the parent wallet ID of an account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIAccount instance +/// - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null +/// - The returned pointer is valid only as long as the account exists +/// - The caller should copy the data if needed for longer use +#[no_mangle] +pub unsafe extern "C" fn account_get_parent_wallet_id(account: *const FFIAccount) -> *const u8 { + if account.is_null() { + return std::ptr::null(); + } + + let account = &*account; + match account.inner().parent_wallet_id { + Some(ref id) => id.as_ptr(), + None => std::ptr::null(), + } +} + +/// Get the account type of an account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIAccount instance +/// - `out_index` must be a valid pointer to a c_uint where the index will be stored +/// - Returns FFIAccountType::StandardBIP44 with index 0 if the account is null +#[no_mangle] +pub unsafe extern "C" fn account_get_account_type( + account: *const FFIAccount, + out_index: *mut c_uint, +) -> FFIAccountType { + if account.is_null() || out_index.is_null() { + if !out_index.is_null() { + *out_index = 0; + } + return FFIAccountType::StandardBIP44; + } + + let account = &*account; + let (account_type, index, registration_index) = + FFIAccountType::from_account_type(&account.inner().account_type); + + // For IdentityTopUp, the registration_index is the relevant index + *out_index = registration_index.unwrap_or(index); + + account_type +} + +/// Check if an account is watch-only +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIAccount instance +/// - Returns false if the account is null +#[no_mangle] +pub unsafe extern "C" fn account_get_is_watch_only(account: *const FFIAccount) -> bool { + if account.is_null() { + return false; + } + + let account = &*account; + account.inner().is_watch_only +} + +// BLS account getter functions +/// Get the extended public key of a BLS account as a string +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIBLSAccount instance +/// - The returned string must be freed by the caller using `string_free` +/// - Returns NULL if the account is null +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn bls_account_get_extended_public_key_as_string( + account: *const FFIBLSAccount, +) -> *mut std::os::raw::c_char { + if account.is_null() { + return std::ptr::null_mut(); + } + + let account = &*account; + // For BLS accounts, we need to encode the extended public key bytes + // There's no standard string representation for BLS extended keys + let bytes = account.inner().bls_public_key.to_bytes(); + let hex_string = hex::encode(bytes); + + match std::ffi::CString::new(hex_string) { + Ok(c_str) => c_str.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Get the network of a BLS account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIBLSAccount instance +/// - Returns FFINetwork::NoNetworks if the account is null +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn bls_account_get_network(account: *const FFIBLSAccount) -> FFINetwork { + if account.is_null() { + return FFINetwork::NoNetworks; + } + + let account = &*account; + account.inner().network.into() +} + +/// Get the parent wallet ID of a BLS account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIBLSAccount instance +/// - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null +/// - The returned pointer is valid only as long as the account exists +/// - The caller should copy the data if needed for longer use +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn bls_account_get_parent_wallet_id( + account: *const FFIBLSAccount, +) -> *const u8 { + if account.is_null() { + return std::ptr::null(); + } + + let account = &*account; + match &account.inner().parent_wallet_id { + Some(id) => id.as_ptr(), + None => std::ptr::null(), + } +} + +/// Get the account type of a BLS account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIBLSAccount instance +/// - `out_index` must be a valid pointer to a c_uint where the index will be stored +/// - Returns FFIAccountType::StandardBIP44 with index 0 if the account is null +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn bls_account_get_account_type( + account: *const FFIBLSAccount, + out_index: *mut c_uint, +) -> FFIAccountType { + if account.is_null() || out_index.is_null() { + if !out_index.is_null() { + *out_index = 0; + } + return FFIAccountType::StandardBIP44; + } + + let account = &*account; + let (account_type, index, registration_index) = + FFIAccountType::from_account_type(&account.inner().account_type); + + // For IdentityTopUp, the registration_index is the relevant index + *out_index = registration_index.unwrap_or(index); + + account_type +} + +/// Check if a BLS account is watch-only +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIBLSAccount instance +/// - Returns false if the account is null +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn bls_account_get_is_watch_only(account: *const FFIBLSAccount) -> bool { + if account.is_null() { + return false; + } + + let account = &*account; + account.inner().is_watch_only +} + +// EdDSA account getter functions +/// Get the extended public key of an EdDSA account as a string +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIEdDSAAccount instance +/// - The returned string must be freed by the caller using `string_free` +/// - Returns NULL if the account is null +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn eddsa_account_get_extended_public_key_as_string( + account: *const FFIEdDSAAccount, +) -> *mut std::os::raw::c_char { + if account.is_null() { + return std::ptr::null_mut(); + } + + let account = &*account; + // For EdDSA accounts, we need to encode the extended public key + // There's no standard string representation for Ed25519 extended keys + let bytes = account.inner().ed25519_public_key.encode(); + let hex_string = hex::encode(bytes); + + match std::ffi::CString::new(hex_string) { + Ok(c_str) => c_str.into_raw(), + Err(_) => std::ptr::null_mut(), + } +} + +/// Get the network of an EdDSA account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIEdDSAAccount instance +/// - Returns FFINetwork::NoNetworks if the account is null +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn eddsa_account_get_network(account: *const FFIEdDSAAccount) -> FFINetwork { + if account.is_null() { + return FFINetwork::NoNetworks; + } + + let account = &*account; + account.inner().network.into() +} + +/// Get the parent wallet ID of an EdDSA account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIEdDSAAccount instance +/// - Returns a pointer to the 32-byte wallet ID, or NULL if not set or account is null +/// - The returned pointer is valid only as long as the account exists +/// - The caller should copy the data if needed for longer use +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn eddsa_account_get_parent_wallet_id( + account: *const FFIEdDSAAccount, +) -> *const u8 { + if account.is_null() { + return std::ptr::null(); + } + + let account = &*account; + match &account.inner().parent_wallet_id { + Some(id) => id.as_ptr(), + None => std::ptr::null(), + } +} + +/// Get the account type of an EdDSA account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIEdDSAAccount instance +/// - `out_index` must be a valid pointer to a c_uint where the index will be stored +/// - Returns FFIAccountType::StandardBIP44 with index 0 if the account is null +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn eddsa_account_get_account_type( + account: *const FFIEdDSAAccount, + out_index: *mut c_uint, +) -> FFIAccountType { + if account.is_null() || out_index.is_null() { + if !out_index.is_null() { + *out_index = 0; + } + return FFIAccountType::StandardBIP44; + } + + let account = &*account; + let (account_type, index, registration_index) = + FFIAccountType::from_account_type(&account.inner().account_type); + + // For IdentityTopUp, the registration_index is the relevant index + *out_index = registration_index.unwrap_or(index); + + account_type +} + +/// Check if an EdDSA account is watch-only +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIEdDSAAccount instance +/// - Returns false if the account is null +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn eddsa_account_get_is_watch_only(account: *const FFIEdDSAAccount) -> bool { + if account.is_null() { + return false; + } + + let account = &*account; + account.inner().is_watch_only +} + /// Get number of accounts /// /// # Safety @@ -172,7 +590,17 @@ pub unsafe extern "C" fn wallet_get_account_count( } let wallet = &*wallet; - let network: key_wallet::Network = network.into(); + let network: 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) { Some(accounts) => { diff --git a/key-wallet-ffi/src/account_collection.rs b/key-wallet-ffi/src/account_collection.rs new file mode 100644 index 000000000..bbcf931ec --- /dev/null +++ b/key-wallet-ffi/src/account_collection.rs @@ -0,0 +1,1631 @@ +//! FFI bindings for account collections +//! +//! This module provides FFI-compatible account collection functionality that mirrors +//! the AccountCollection structure from key-wallet but uses FFI-safe types. + +use std::ffi::CString; +use std::os::raw::{c_char, c_uint}; +use std::ptr; + +use crate::account::FFIAccount; +use crate::error::{FFIError, FFIErrorCode}; +use crate::types::{FFINetwork, FFIWallet}; + +/// Opaque handle to an account collection +pub struct FFIAccountCollection { + /// The underlying account collection reference + collection: key_wallet::AccountCollection, +} + +impl FFIAccountCollection { + /// Create a new FFI account collection from a key_wallet AccountCollection + pub fn new(collection: &key_wallet::AccountCollection) -> Self { + FFIAccountCollection { + collection: collection.clone(), + } + } +} + +/// C-compatible summary of all accounts in a collection +/// +/// This struct provides Swift with structured data about all accounts +/// that exist in the collection, allowing programmatic access to account +/// indices and presence information. +#[repr(C)] +pub struct FFIAccountCollectionSummary { + /// Array of BIP44 account indices + pub bip44_indices: *mut c_uint, + /// Number of BIP44 accounts + pub bip44_count: usize, + + /// Array of BIP32 account indices + pub bip32_indices: *mut c_uint, + /// Number of BIP32 accounts + pub bip32_count: usize, + + /// Array of CoinJoin account indices + pub coinjoin_indices: *mut c_uint, + /// Number of CoinJoin accounts + pub coinjoin_count: usize, + + /// Array of identity top-up registration indices + pub identity_topup_indices: *mut c_uint, + /// Number of identity top-up accounts + pub identity_topup_count: usize, + + /// Whether identity registration account exists + pub has_identity_registration: bool, + /// Whether identity invitation account exists + pub has_identity_invitation: bool, + /// Whether identity top-up not bound account exists + pub has_identity_topup_not_bound: bool, + /// Whether provider voting keys account exists + pub has_provider_voting_keys: bool, + /// Whether provider owner keys account exists + pub has_provider_owner_keys: bool, + + #[cfg(feature = "bls")] + /// Whether provider operator keys account exists + pub has_provider_operator_keys: bool, + + #[cfg(feature = "eddsa")] + /// Whether provider platform keys account exists + pub has_provider_platform_keys: bool, +} + +/// Get account collection for a specific network from wallet +/// +/// # Safety +/// +/// - `wallet` must be a valid pointer to an FFIWallet instance +/// - `error` must be a valid pointer to an FFIError structure or null +/// - The returned pointer must be freed with `account_collection_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn wallet_get_account_collection( + wallet: *const FFIWallet, + network: FFINetwork, + error: *mut FFIError, +) -> *mut FFIAccountCollection { + if wallet.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Wallet is null".to_string()); + return ptr::null_mut(); + } + + let wallet = &*wallet; + 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 ptr::null_mut(); + } + }; + + match wallet.inner().accounts_on_network(network_rust) { + Some(collection) => { + let ffi_collection = FFIAccountCollection::new(collection); + Box::into_raw(Box::new(ffi_collection)) + } + None => { + FFIError::set_error( + error, + FFIErrorCode::NotFound, + format!("No accounts found for network {:?}", network_rust), + ); + ptr::null_mut() + } + } +} + +/// Free an account collection handle +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection created by this library +/// - `collection` must not be used after calling this function +#[no_mangle] +pub unsafe extern "C" fn account_collection_free(collection: *mut FFIAccountCollection) { + if !collection.is_null() { + let _ = Box::from_raw(collection); + } +} + +// Standard BIP44 accounts functions + +/// Get a BIP44 account by index from the collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_bip44_account( + collection: *const FFIAccountCollection, + index: c_uint, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match collection.collection.standard_bip44_accounts.get(&index) { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get all BIP44 account indices +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - `out_indices` must be a valid pointer to store the indices array +/// - `out_count` must be a valid pointer to store the count +/// - The returned array must be freed with `free_u32_array` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_bip44_indices( + collection: *const FFIAccountCollection, + out_indices: *mut *mut c_uint, + out_count: *mut usize, +) -> bool { + if collection.is_null() || out_indices.is_null() || out_count.is_null() { + return false; + } + + let collection = &*collection; + let mut indices: Vec = + collection.collection.standard_bip44_accounts.keys().copied().collect(); + + if indices.is_empty() { + *out_indices = ptr::null_mut(); + *out_count = 0; + return true; + } + + indices.sort(); + + let mut boxed_slice = indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + let len = boxed_slice.len(); + std::mem::forget(boxed_slice); + + *out_indices = ptr; + *out_count = len; + true +} + +// Standard BIP32 accounts functions + +/// Get a BIP32 account by index from the collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_bip32_account( + collection: *const FFIAccountCollection, + index: c_uint, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match collection.collection.standard_bip32_accounts.get(&index) { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get all BIP32 account indices +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - `out_indices` must be a valid pointer to store the indices array +/// - `out_count` must be a valid pointer to store the count +/// - The returned array must be freed with `free_u32_array` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_bip32_indices( + collection: *const FFIAccountCollection, + out_indices: *mut *mut c_uint, + out_count: *mut usize, +) -> bool { + if collection.is_null() || out_indices.is_null() || out_count.is_null() { + return false; + } + + let collection = &*collection; + let indices: Vec = + collection.collection.standard_bip32_accounts.keys().copied().collect(); + + if indices.is_empty() { + *out_indices = ptr::null_mut(); + *out_count = 0; + return true; + } + + let mut boxed_slice = indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + let len = boxed_slice.len(); + std::mem::forget(boxed_slice); + + *out_indices = ptr; + *out_count = len; + true +} + +// CoinJoin accounts functions + +/// Get a CoinJoin account by index from the collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_coinjoin_account( + collection: *const FFIAccountCollection, + index: c_uint, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match collection.collection.coinjoin_accounts.get(&index) { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get all CoinJoin account indices +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - `out_indices` must be a valid pointer to store the indices array +/// - `out_count` must be a valid pointer to store the count +/// - The returned array must be freed with `free_u32_array` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_coinjoin_indices( + collection: *const FFIAccountCollection, + out_indices: *mut *mut c_uint, + out_count: *mut usize, +) -> bool { + if collection.is_null() || out_indices.is_null() || out_count.is_null() { + return false; + } + + let collection = &*collection; + let mut indices: Vec = + collection.collection.coinjoin_accounts.keys().copied().collect(); + + if indices.is_empty() { + *out_indices = ptr::null_mut(); + *out_count = 0; + return true; + } + + indices.sort(); + + let mut boxed_slice = indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + let len = boxed_slice.len(); + std::mem::forget(boxed_slice); + + *out_indices = ptr; + *out_count = len; + true +} + +// Identity accounts functions + +/// Get the identity registration account if it exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_identity_registration( + collection: *const FFIAccountCollection, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.identity_registration { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if identity registration account exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +#[no_mangle] +pub unsafe extern "C" fn account_collection_has_identity_registration( + collection: *const FFIAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.identity_registration.is_some() +} + +/// Get an identity topup account by registration index +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_identity_topup( + collection: *const FFIAccountCollection, + registration_index: c_uint, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match collection.collection.identity_topup.get(®istration_index) { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get all identity topup registration indices +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - `out_indices` must be a valid pointer to store the indices array +/// - `out_count` must be a valid pointer to store the count +/// - The returned array must be freed with `free_u32_array` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_identity_topup_indices( + collection: *const FFIAccountCollection, + out_indices: *mut *mut c_uint, + out_count: *mut usize, +) -> bool { + if collection.is_null() || out_indices.is_null() || out_count.is_null() { + return false; + } + + let collection = &*collection; + let mut indices: Vec = collection.collection.identity_topup.keys().copied().collect(); + + if indices.is_empty() { + *out_indices = ptr::null_mut(); + *out_count = 0; + return true; + } + + indices.sort(); + + let mut boxed_slice = indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + let len = boxed_slice.len(); + std::mem::forget(boxed_slice); + + *out_indices = ptr; + *out_count = len; + true +} + +/// Get the identity topup not bound account if it exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_identity_topup_not_bound( + collection: *const FFIAccountCollection, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.identity_topup_not_bound { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if identity topup not bound account exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +#[no_mangle] +pub unsafe extern "C" fn account_collection_has_identity_topup_not_bound( + collection: *const FFIAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.identity_topup_not_bound.is_some() +} + +/// Get the identity invitation account if it exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_identity_invitation( + collection: *const FFIAccountCollection, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.identity_invitation { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if identity invitation account exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +#[no_mangle] +pub unsafe extern "C" fn account_collection_has_identity_invitation( + collection: *const FFIAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.identity_invitation.is_some() +} + +// Provider accounts functions + +/// Get the provider voting keys account if it exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_provider_voting_keys( + collection: *const FFIAccountCollection, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.provider_voting_keys { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if provider voting keys account exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +#[no_mangle] +pub unsafe extern "C" fn account_collection_has_provider_voting_keys( + collection: *const FFIAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.provider_voting_keys.is_some() +} + +/// Get the provider owner keys account if it exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_provider_owner_keys( + collection: *const FFIAccountCollection, +) -> *mut FFIAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.provider_owner_keys { + Some(account) => { + let ffi_account = FFIAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if provider owner keys account exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +#[no_mangle] +pub unsafe extern "C" fn account_collection_has_provider_owner_keys( + collection: *const FFIAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.provider_owner_keys.is_some() +} + +/// Get the provider operator keys account if it exists +/// Note: This function is only available when the `bls` feature is enabled +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `bls_account_free` when no longer needed +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_provider_operator_keys( + collection: *const FFIAccountCollection, +) -> *mut crate::account::FFIBLSAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.provider_operator_keys { + Some(account) => { + let ffi_account = crate::account::FFIBLSAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get the provider operator keys account if it exists (stub when BLS is disabled) +#[cfg(not(feature = "bls"))] +#[no_mangle] +pub unsafe extern "C" fn account_collection_get_provider_operator_keys( + _collection: *const FFIAccountCollection, +) -> *mut std::os::raw::c_void { + // BLS feature not enabled, always return null + ptr::null_mut() +} + +/// Check if provider operator keys account exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +#[no_mangle] +pub unsafe extern "C" fn account_collection_has_provider_operator_keys( + collection: *const FFIAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + #[cfg(feature = "bls")] + { + let collection = &*collection; + collection.collection.provider_operator_keys.is_some() + } + + #[cfg(not(feature = "bls"))] + { + false + } +} + +/// Get the provider platform keys account if it exists +/// Note: This function is only available when the `eddsa` feature is enabled +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `eddsa_account_free` when no longer needed +#[cfg(feature = "eddsa")] +#[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() +} + +/// Check if provider platform keys account exists +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +#[no_mangle] +pub unsafe extern "C" fn account_collection_has_provider_platform_keys( + collection: *const FFIAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + #[cfg(feature = "eddsa")] + { + let collection = &*collection; + collection.collection.provider_platform_keys.is_some() + } + + #[cfg(not(feature = "eddsa"))] + { + false + } +} + +// Utility functions + +/// Free a u32 array allocated by this library +/// +/// # Safety +/// +/// - `array` must be a valid pointer to an array allocated by this library +/// - `array` must not be used after calling this function +#[no_mangle] +pub unsafe extern "C" fn free_u32_array(array: *mut c_uint, count: usize) { + if !array.is_null() && count > 0 { + let _ = Vec::from_raw_parts(array, count, count); + } +} + +/// Get the total number of accounts in the collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +#[no_mangle] +pub unsafe extern "C" fn account_collection_count( + collection: *const FFIAccountCollection, +) -> c_uint { + if collection.is_null() { + return 0; + } + + let collection = &*collection; + let mut count = 0u32; + + count += collection.collection.standard_bip44_accounts.len() as u32; + count += collection.collection.standard_bip32_accounts.len() as u32; + count += collection.collection.coinjoin_accounts.len() as u32; + count += collection.collection.identity_topup.len() as u32; + + if collection.collection.identity_registration.is_some() { + count += 1; + } + if collection.collection.identity_topup_not_bound.is_some() { + count += 1; + } + if collection.collection.identity_invitation.is_some() { + count += 1; + } + if collection.collection.provider_voting_keys.is_some() { + count += 1; + } + if collection.collection.provider_owner_keys.is_some() { + count += 1; + } + + #[cfg(feature = "bls")] + if collection.collection.provider_operator_keys.is_some() { + count += 1; + } + + #[cfg(feature = "eddsa")] + if collection.collection.provider_platform_keys.is_some() { + count += 1; + } + + count +} + +/// Get a human-readable summary of all accounts in the collection +/// +/// Returns a formatted string showing all account types and their indices. +/// The format is designed to be clear and readable for end users. +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned string must be freed with `string_free` when no longer needed +/// - Returns null if the collection pointer is null +#[no_mangle] +pub unsafe extern "C" fn account_collection_summary( + collection: *const FFIAccountCollection, +) -> *mut c_char { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + let mut summary_parts = Vec::new(); + + summary_parts.push("Account Summary:".to_string()); + + // BIP44 Accounts + if !collection.collection.standard_bip44_accounts.is_empty() { + let mut indices: Vec = + collection.collection.standard_bip44_accounts.keys().copied().collect(); + indices.sort(); + let count = indices.len(); + let indices_str = format!("{:?}", indices); + summary_parts.push(format!( + "• BIP44 Accounts: {} {} at indices {}", + count, + if count == 1 { + "account" + } else { + "accounts" + }, + indices_str + )); + } + + // BIP32 Accounts + if !collection.collection.standard_bip32_accounts.is_empty() { + let mut indices: Vec = + collection.collection.standard_bip32_accounts.keys().copied().collect(); + indices.sort(); + let count = indices.len(); + let indices_str = format!("{:?}", indices); + summary_parts.push(format!( + "• BIP32 Accounts: {} {} at indices {}", + count, + if count == 1 { + "account" + } else { + "accounts" + }, + indices_str + )); + } + + // CoinJoin Accounts + if !collection.collection.coinjoin_accounts.is_empty() { + let mut indices: Vec = + collection.collection.coinjoin_accounts.keys().copied().collect(); + indices.sort(); + let count = indices.len(); + let indices_str = format!("{:?}", indices); + summary_parts.push(format!( + "• CoinJoin Accounts: {} {} at indices {}", + count, + if count == 1 { + "account" + } else { + "accounts" + }, + indices_str + )); + } + + // Identity TopUp Accounts + if !collection.collection.identity_topup.is_empty() { + let mut indices: Vec = collection.collection.identity_topup.keys().copied().collect(); + indices.sort(); + let count = indices.len(); + let indices_str = format!("{:?}", indices); + summary_parts.push(format!( + "• Identity TopUp: {} {} at indices {}", + count, + if count == 1 { + "account" + } else { + "accounts" + }, + indices_str + )); + } + + // Special accounts (single instances) + if collection.collection.identity_registration.is_some() { + summary_parts.push("• Identity Registration Account".to_string()); + } + + if collection.collection.identity_topup_not_bound.is_some() { + summary_parts.push("• Identity TopUp Not Bound Account".to_string()); + } + + if collection.collection.identity_invitation.is_some() { + summary_parts.push("• Identity Invitation Account".to_string()); + } + + if collection.collection.provider_voting_keys.is_some() { + summary_parts.push("• Provider Voting Keys Account".to_string()); + } + + if collection.collection.provider_owner_keys.is_some() { + summary_parts.push("• Provider Owner Keys Account".to_string()); + } + + #[cfg(feature = "bls")] + if collection.collection.provider_operator_keys.is_some() { + summary_parts.push("• Provider Operator Keys Account (BLS)".to_string()); + } + + #[cfg(feature = "eddsa")] + if collection.collection.provider_platform_keys.is_some() { + summary_parts.push("• Provider Platform Keys Account (EdDSA)".to_string()); + } + + // If there are no accounts at all + if summary_parts.len() == 1 { + summary_parts.push("No accounts configured".to_string()); + } + + let summary = summary_parts.join("\n"); + + match CString::new(summary) { + Ok(c_str) => c_str.into_raw(), + Err(_) => ptr::null_mut(), + } +} + +/// Get structured account collection summary data +/// +/// Returns a struct containing arrays of indices for each account type and boolean +/// flags for special accounts. This provides Swift with programmatic access to +/// account information. +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIAccountCollection +/// - The returned pointer must be freed with `account_collection_summary_free` when no longer needed +/// - Returns null if the collection pointer is null +#[no_mangle] +pub unsafe extern "C" fn account_collection_summary_data( + collection: *const FFIAccountCollection, +) -> *mut FFIAccountCollectionSummary { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + + // Collect BIP44 indices + let mut bip44_indices: Vec = + collection.collection.standard_bip44_accounts.keys().copied().collect(); + bip44_indices.sort(); + let (bip44_ptr, bip44_count) = if bip44_indices.is_empty() { + (ptr::null_mut(), 0) + } else { + let count = bip44_indices.len(); + let mut boxed_slice = bip44_indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + std::mem::forget(boxed_slice); + (ptr, count) + }; + + // Collect BIP32 indices + let mut bip32_indices: Vec = + collection.collection.standard_bip32_accounts.keys().copied().collect(); + bip32_indices.sort(); + let (bip32_ptr, bip32_count) = if bip32_indices.is_empty() { + (ptr::null_mut(), 0) + } else { + let count = bip32_indices.len(); + let mut boxed_slice = bip32_indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + std::mem::forget(boxed_slice); + (ptr, count) + }; + + // Collect CoinJoin indices + let mut coinjoin_indices: Vec = + collection.collection.coinjoin_accounts.keys().copied().collect(); + coinjoin_indices.sort(); + let (coinjoin_ptr, coinjoin_count) = if coinjoin_indices.is_empty() { + (ptr::null_mut(), 0) + } else { + let count = coinjoin_indices.len(); + let mut boxed_slice = coinjoin_indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + std::mem::forget(boxed_slice); + (ptr, count) + }; + + // Collect identity topup indices + let mut topup_indices: Vec = + collection.collection.identity_topup.keys().copied().collect(); + topup_indices.sort(); + let (topup_ptr, topup_count) = if topup_indices.is_empty() { + (ptr::null_mut(), 0) + } else { + let count = topup_indices.len(); + let mut boxed_slice = topup_indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + std::mem::forget(boxed_slice); + (ptr, count) + }; + + // Create the summary struct + let summary = FFIAccountCollectionSummary { + bip44_indices: bip44_ptr, + bip44_count, + bip32_indices: bip32_ptr, + bip32_count, + coinjoin_indices: coinjoin_ptr, + coinjoin_count, + identity_topup_indices: topup_ptr, + identity_topup_count: topup_count, + has_identity_registration: collection.collection.identity_registration.is_some(), + has_identity_invitation: collection.collection.identity_invitation.is_some(), + has_identity_topup_not_bound: collection.collection.identity_topup_not_bound.is_some(), + has_provider_voting_keys: collection.collection.provider_voting_keys.is_some(), + has_provider_owner_keys: collection.collection.provider_owner_keys.is_some(), + #[cfg(feature = "bls")] + has_provider_operator_keys: collection.collection.provider_operator_keys.is_some(), + #[cfg(feature = "eddsa")] + has_provider_platform_keys: collection.collection.provider_platform_keys.is_some(), + }; + + Box::into_raw(Box::new(summary)) +} + +/// Free an account collection summary and all its allocated memory +/// +/// # Safety +/// +/// - `summary` must be a valid pointer to an FFIAccountCollectionSummary created by `account_collection_summary_data` +/// - `summary` must not be used after calling this function +#[no_mangle] +pub unsafe extern "C" fn account_collection_summary_free( + summary: *mut FFIAccountCollectionSummary, +) { + if !summary.is_null() { + let summary = Box::from_raw(summary); + + // Free all the allocated arrays + if !summary.bip44_indices.is_null() && summary.bip44_count > 0 { + let _ = Vec::from_raw_parts( + summary.bip44_indices, + summary.bip44_count, + summary.bip44_count, + ); + } + + if !summary.bip32_indices.is_null() && summary.bip32_count > 0 { + let _ = Vec::from_raw_parts( + summary.bip32_indices, + summary.bip32_count, + summary.bip32_count, + ); + } + + if !summary.coinjoin_indices.is_null() && summary.coinjoin_count > 0 { + let _ = Vec::from_raw_parts( + summary.coinjoin_indices, + summary.coinjoin_count, + summary.coinjoin_count, + ); + } + + if !summary.identity_topup_indices.is_null() && summary.identity_topup_count > 0 { + let _ = Vec::from_raw_parts( + summary.identity_topup_indices, + summary.identity_topup_count, + summary.identity_topup_count, + ); + } + + // The summary struct itself is dropped automatically when the Box is dropped + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::wallet::wallet_create_from_mnemonic_with_options; + use std::ffi::CString; + + #[test] + fn test_account_collection_basic() { + unsafe { + let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); + + // Create wallet with default accounts + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + crate::types::FFINetwork::Testnet, + ptr::null(), + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = wallet_get_account_collection( + wallet, + crate::types::FFINetwork::Testnet, + ptr::null_mut(), + ); + assert!(!collection.is_null()); + + // Check that we have some accounts + let count = account_collection_count(collection); + assert!(count > 0); + + // Check BIP44 accounts + let mut indices: *mut c_uint = ptr::null_mut(); + let mut indices_count: usize = 0; + let success = + account_collection_get_bip44_indices(collection, &mut indices, &mut indices_count); + assert!(success); + assert!(indices_count > 0); + + // Get first BIP44 account + let account = account_collection_get_bip44_account(collection, 0); + assert!(!account.is_null()); + + // Clean up + crate::account::account_free(account); + if !indices.is_null() { + free_u32_array(indices, indices_count); + } + account_collection_free(collection); + crate::wallet::wallet_free(wallet); + } + } + + #[test] + #[cfg(feature = "bls")] + fn test_bls_account() { + unsafe { + let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); + + // Create wallet with provider accounts + let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); + options.option_type = crate::types::FFIAccountCreationOptionType::AllAccounts; + + // Add provider operator keys account type + let special_types = vec![crate::types::FFIAccountType::ProviderOperatorKeys]; + options.special_account_types = special_types.as_ptr(); + options.special_account_types_count = special_types.len(); + + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + crate::types::FFINetwork::Testnet, + &options, + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = wallet_get_account_collection( + wallet, + crate::types::FFINetwork::Testnet, + ptr::null_mut(), + ); + assert!(!collection.is_null()); + + // Check for provider operator keys account (BLS) + let has_operator = account_collection_has_provider_operator_keys(collection); + if has_operator { + let operator_account = account_collection_get_provider_operator_keys(collection); + assert!(!operator_account.is_null()); + + // Free the BLS account + crate::account::bls_account_free(operator_account); + } + + // Clean up + account_collection_free(collection); + crate::wallet::wallet_free(wallet); + } + } + + #[test] + #[cfg(feature = "eddsa")] + fn test_eddsa_account() { + unsafe { + let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); + + // Create wallet with provider accounts + let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); + options.option_type = crate::types::FFIAccountCreationOptionType::AllAccounts; + + // Add provider platform keys account type + let special_types = vec![crate::types::FFIAccountType::ProviderPlatformKeys]; + options.special_account_types = special_types.as_ptr(); + options.special_account_types_count = special_types.len(); + + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + crate::types::FFINetwork::Testnet, + &options, + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = wallet_get_account_collection( + wallet, + crate::types::FFINetwork::Testnet, + ptr::null_mut(), + ); + assert!(!collection.is_null()); + + // Check for provider platform keys account (EdDSA) + let has_platform = account_collection_has_provider_platform_keys(collection); + if has_platform { + let platform_account = account_collection_get_provider_platform_keys(collection); + assert!(!platform_account.is_null()); + + // Free the EdDSA account + crate::account::eddsa_account_free(platform_account); + } + + // Clean up + account_collection_free(collection); + crate::wallet::wallet_free(wallet); + } + } + + #[test] + fn test_account_collection_summary() { + unsafe { + use std::ffi::CStr; + + let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); + + // Create wallet with multiple account types + let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); + options.option_type = crate::types::FFIAccountCreationOptionType::AllAccounts; + + // Add various special accounts + let special_types = vec![ + crate::types::FFIAccountType::ProviderVotingKeys, + crate::types::FFIAccountType::ProviderOwnerKeys, + crate::types::FFIAccountType::IdentityRegistration, + crate::types::FFIAccountType::IdentityInvitation, + ]; + options.special_account_types = special_types.as_ptr(); + options.special_account_types_count = special_types.len(); + + // Configure standard accounts - store vectors in variables to keep them alive + let bip44_indices = vec![0, 4, 5, 8]; + let bip32_indices = vec![0]; + let coinjoin_indices = vec![0, 1]; + let topup_indices = vec![0, 1, 2]; + + options.bip44_indices = bip44_indices.as_ptr(); + options.bip44_count = bip44_indices.len(); + + options.bip32_indices = bip32_indices.as_ptr(); + options.bip32_count = bip32_indices.len(); + + options.coinjoin_indices = coinjoin_indices.as_ptr(); + options.coinjoin_count = coinjoin_indices.len(); + + options.topup_indices = topup_indices.as_ptr(); + options.topup_count = topup_indices.len(); + + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + crate::types::FFINetwork::Testnet, + &options, + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = wallet_get_account_collection( + wallet, + crate::types::FFINetwork::Testnet, + ptr::null_mut(), + ); + assert!(!collection.is_null()); + + // Get the summary + let summary_ptr = account_collection_summary(collection); + assert!(!summary_ptr.is_null()); + + // Convert to Rust string to verify content + let summary_cstr = CStr::from_ptr(summary_ptr); + let summary = summary_cstr.to_str().unwrap(); + + // Verify the summary contains expected content + assert!(summary.contains("Account Summary:")); + // The indices might not be in that exact format, so check more flexibly + assert!(summary.contains("BIP44 Accounts")); + assert!(summary.contains("BIP32 Accounts")); + assert!(summary.contains("CoinJoin Accounts")); + assert!(summary.contains("Identity TopUp")); + assert!(summary.contains("Identity Registration Account")); + assert!(summary.contains("Identity Invitation Account")); + assert!(summary.contains("Provider Voting Keys Account")); + assert!(summary.contains("Provider Owner Keys Account")); + + // Clean up + crate::utils::string_free(summary_ptr); + account_collection_free(collection); + crate::wallet::wallet_free(wallet); + } + } + + #[test] + fn test_account_collection_summary_empty() { + unsafe { + use std::ffi::CStr; + + let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); + + // Create wallet with no accounts using SpecificAccounts with empty lists + let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); + options.option_type = crate::types::FFIAccountCreationOptionType::SpecificAccounts; + // All arrays are already null/0 from default_options() + + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + crate::types::FFINetwork::Testnet, + &options, + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = wallet_get_account_collection( + wallet, + crate::types::FFINetwork::Testnet, + ptr::null_mut(), + ); + + // With SpecificAccounts and empty lists, collection might be null or empty + if collection.is_null() { + // If the collection doesn't exist, that's OK for this test - just clean up and return + crate::wallet::wallet_free(wallet); + return; + } + + // Get the summary + let summary_ptr = account_collection_summary(collection); + assert!(!summary_ptr.is_null()); + + // Convert to Rust string to verify content + let summary_cstr = CStr::from_ptr(summary_ptr); + let summary = summary_cstr.to_str().unwrap(); + + // Verify the summary shows no accounts + assert!(summary.contains("Account Summary:")); + assert!(summary.contains("No accounts configured")); + + // Clean up + crate::utils::string_free(summary_ptr); + account_collection_free(collection); + crate::wallet::wallet_free(wallet); + } + } + + #[test] + fn test_account_collection_summary_null_safety() { + unsafe { + // Test with null collection + let summary_ptr = account_collection_summary(ptr::null()); + assert!(summary_ptr.is_null()); + } + } + + #[test] + fn test_account_collection_summary_data() { + unsafe { + let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); + + // Create wallet with various account types + let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); + options.option_type = crate::types::FFIAccountCreationOptionType::AllAccounts; + + // Add various special accounts + let special_types = vec![ + crate::types::FFIAccountType::ProviderVotingKeys, + crate::types::FFIAccountType::ProviderOwnerKeys, + crate::types::FFIAccountType::IdentityRegistration, + crate::types::FFIAccountType::IdentityInvitation, + ]; + options.special_account_types = special_types.as_ptr(); + options.special_account_types_count = special_types.len(); + + // Configure standard accounts + let bip44_indices = vec![0, 4, 5, 8]; + let bip32_indices = vec![0]; + let coinjoin_indices = vec![0, 1]; + let topup_indices = vec![0, 1, 2]; + + options.bip44_indices = bip44_indices.as_ptr(); + options.bip44_count = bip44_indices.len(); + + options.bip32_indices = bip32_indices.as_ptr(); + options.bip32_count = bip32_indices.len(); + + options.coinjoin_indices = coinjoin_indices.as_ptr(); + options.coinjoin_count = coinjoin_indices.len(); + + options.topup_indices = topup_indices.as_ptr(); + options.topup_count = topup_indices.len(); + + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + crate::types::FFINetwork::Testnet, + &options, + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = wallet_get_account_collection( + wallet, + crate::types::FFINetwork::Testnet, + ptr::null_mut(), + ); + assert!(!collection.is_null()); + + // Get the summary data + let summary = account_collection_summary_data(collection); + assert!(!summary.is_null()); + + let summary_ref = &*summary; + + // Verify BIP44 indices + assert_eq!(summary_ref.bip44_count, 4); + assert!(!summary_ref.bip44_indices.is_null()); + let bip44_slice = + std::slice::from_raw_parts(summary_ref.bip44_indices, summary_ref.bip44_count); + assert_eq!(bip44_slice, &[0, 4, 5, 8]); + + // Verify BIP32 indices + assert_eq!(summary_ref.bip32_count, 1); + assert!(!summary_ref.bip32_indices.is_null()); + let bip32_slice = + std::slice::from_raw_parts(summary_ref.bip32_indices, summary_ref.bip32_count); + assert_eq!(bip32_slice, &[0]); + + // Verify CoinJoin indices + assert_eq!(summary_ref.coinjoin_count, 2); + assert!(!summary_ref.coinjoin_indices.is_null()); + let coinjoin_slice = std::slice::from_raw_parts( + summary_ref.coinjoin_indices, + summary_ref.coinjoin_count, + ); + assert_eq!(coinjoin_slice, &[0, 1]); + + // Verify identity topup indices + assert_eq!(summary_ref.identity_topup_count, 3); + assert!(!summary_ref.identity_topup_indices.is_null()); + let topup_slice = std::slice::from_raw_parts( + summary_ref.identity_topup_indices, + summary_ref.identity_topup_count, + ); + assert_eq!(topup_slice, &[0, 1, 2]); + + // Verify boolean flags + assert!(summary_ref.has_identity_registration); + assert!(summary_ref.has_identity_invitation); + assert!(summary_ref.has_provider_voting_keys); + assert!(summary_ref.has_provider_owner_keys); + + // Clean up + account_collection_summary_free(summary); + account_collection_free(collection); + crate::wallet::wallet_free(wallet); + } + } + + #[test] + fn test_account_collection_summary_data_empty() { + unsafe { + let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); + + // Create wallet with no accounts - but still create a collection on the network + // Use SpecificAccounts with empty lists to get truly empty collections + let mut options = crate::types::FFIWalletAccountCreationOptions::default_options(); + options.option_type = crate::types::FFIAccountCreationOptionType::SpecificAccounts; + + // Set empty arrays for all account types + options.bip44_indices = ptr::null(); + options.bip44_count = 0; + options.bip32_indices = ptr::null(); + options.bip32_count = 0; + options.coinjoin_indices = ptr::null(); + options.coinjoin_count = 0; + options.topup_indices = ptr::null(); + options.topup_count = 0; + options.special_account_types = ptr::null(); + options.special_account_types_count = 0; + + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + crate::types::FFINetwork::Testnet, + &options, + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = wallet_get_account_collection( + wallet, + crate::types::FFINetwork::Testnet, + ptr::null_mut(), + ); + + // With AllAccounts but empty lists, collection should still exist + if collection.is_null() { + // If the collection doesn't exist, that's OK for this test - just clean up and return + crate::wallet::wallet_free(wallet); + return; + } + + // Get the summary data + let summary = account_collection_summary_data(collection); + assert!(!summary.is_null()); + + let summary_ref = &*summary; + + // Verify all arrays are empty + assert_eq!(summary_ref.bip44_count, 0); + assert!(summary_ref.bip44_indices.is_null()); + + assert_eq!(summary_ref.bip32_count, 0); + assert!(summary_ref.bip32_indices.is_null()); + + assert_eq!(summary_ref.coinjoin_count, 0); + assert!(summary_ref.coinjoin_indices.is_null()); + + assert_eq!(summary_ref.identity_topup_count, 0); + assert!(summary_ref.identity_topup_indices.is_null()); + + // Verify all boolean flags are false + assert!(!summary_ref.has_identity_registration); + assert!(!summary_ref.has_identity_invitation); + assert!(!summary_ref.has_identity_topup_not_bound); + assert!(!summary_ref.has_provider_voting_keys); + assert!(!summary_ref.has_provider_owner_keys); + + // Clean up + account_collection_summary_free(summary); + account_collection_free(collection); + crate::wallet::wallet_free(wallet); + } + } + + #[test] + fn test_account_collection_summary_data_null_safety() { + unsafe { + // Test with null collection + let summary = account_collection_summary_data(ptr::null()); + assert!(summary.is_null()); + + // Test freeing null summary (should not crash) + account_collection_summary_free(ptr::null_mut()); + } + } + + #[test] + fn test_account_collection_summary_memory_management() { + unsafe { + let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); + + // Create wallet with default accounts (which should have at least BIP44 account 0) + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + crate::types::FFINetwork::Testnet, + ptr::null(), + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = wallet_get_account_collection( + wallet, + crate::types::FFINetwork::Testnet, + ptr::null_mut(), + ); + assert!(!collection.is_null()); + + // Get multiple summaries to test memory management + let summary1 = account_collection_summary_data(collection); + assert!(!summary1.is_null()); + + let summary2 = account_collection_summary_data(collection); + assert!(!summary2.is_null()); + + // The two summaries should be different pointers + assert_ne!(summary1, summary2); + + // But they should contain the same data + let summary1_ref = &*summary1; + let summary2_ref = &*summary2; + assert_eq!(summary1_ref.bip44_count, summary2_ref.bip44_count); + assert_eq!( + summary1_ref.has_identity_registration, + summary2_ref.has_identity_registration + ); + + // Clean up both summaries + account_collection_summary_free(summary1); + account_collection_summary_free(summary2); + + // Clean up + account_collection_free(collection); + crate::wallet::wallet_free(wallet); + } + } +} diff --git a/key-wallet-ffi/src/account_tests.rs b/key-wallet-ffi/src/account_tests.rs index fd0d10d7c..93ebe7ec7 100644 --- a/key-wallet-ffi/src/account_tests.rs +++ b/key-wallet-ffi/src/account_tests.rs @@ -2,7 +2,7 @@ mod tests { use super::super::*; use crate::error::{FFIError, FFIErrorCode}; - use crate::types::FFINetwork; + use crate::types::{FFIAccountType, FFINetwork}; use crate::wallet; use std::ffi::CString; use std::ptr; @@ -10,12 +10,7 @@ mod tests { #[test] fn test_wallet_get_account_null_wallet() { let result = unsafe { - wallet_get_account( - ptr::null(), - FFINetwork::Testnet, - 0, - 0, // StandardBIP44 - ) + wallet_get_account(ptr::null(), FFINetwork::Testnet, 0, FFIAccountType::StandardBIP44) }; assert!(result.account.is_null()); @@ -30,49 +25,6 @@ mod tests { } } - #[test] - fn test_wallet_get_account_invalid_type() { - let mut error = FFIError::success(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let passphrase = CString::new("").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic( - mnemonic.as_ptr(), - passphrase.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - - let result = unsafe { - wallet_get_account( - wallet, - FFINetwork::Testnet, - 0, - 99, // Invalid account type - ) - }; - - assert!(result.account.is_null()); - assert_ne!(result.error_code, 0); - assert_eq!(result.error_code, FFIErrorCode::InvalidInput as i32); - - // Clean up error message if present - if !result.error_message.is_null() { - unsafe { - let _ = CString::from_raw(result.error_message); - } - } - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - #[test] fn test_wallet_get_account_existing() { let mut error = FFIError::success(); @@ -92,12 +44,7 @@ mod tests { // Try to get the default account (should exist) let result = unsafe { - wallet_get_account( - wallet, - FFINetwork::Testnet, - 0, - 0, // StandardBIP44 - ) + wallet_get_account(wallet, FFINetwork::Testnet, 0, FFIAccountType::StandardBIP44) }; // Note: Since the account may not exist yet (depends on wallet creation logic), @@ -201,7 +148,23 @@ mod tests { } #[test] - fn test_wallet_get_account_identity_topup_error() { + fn test_account_type_values() { + // Test FFIAccountType enum values + assert_eq!(FFIAccountType::StandardBIP44 as u32, 0); + assert_eq!(FFIAccountType::StandardBIP32 as u32, 1); + assert_eq!(FFIAccountType::CoinJoin as u32, 2); + assert_eq!(FFIAccountType::IdentityRegistration as u32, 3); + assert_eq!(FFIAccountType::IdentityTopUp as u32, 4); + assert_eq!(FFIAccountType::IdentityTopUpNotBoundToIdentity as u32, 5); + assert_eq!(FFIAccountType::IdentityInvitation as u32, 6); + assert_eq!(FFIAccountType::ProviderVotingKeys as u32, 7); + assert_eq!(FFIAccountType::ProviderOwnerKeys as u32, 8); + assert_eq!(FFIAccountType::ProviderOperatorKeys as u32, 9); + assert_eq!(FFIAccountType::ProviderPlatformKeys as u32, 10); + } + + #[test] + fn test_account_getters() { let mut error = FFIError::success(); // Create a wallet @@ -217,26 +180,50 @@ mod tests { ) }; - // Try to get an IdentityTopUp account (should fail with helpful error) + assert!(!wallet.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Get an account let result = unsafe { - wallet_get_account( - wallet, - FFINetwork::Testnet, - 0, - 4, // IdentityTopUp - ) + wallet_get_account(wallet, FFINetwork::Testnet, 0, FFIAccountType::StandardBIP44) }; - assert!(result.account.is_null()); - assert_ne!(result.error_code, 0); - assert_eq!(result.error_code, FFIErrorCode::InvalidInput as i32); + if !result.account.is_null() { + // Test all the getter functions + unsafe { + // Test get xpub + let xpub_str = account_get_extended_public_key_as_string(result.account); + assert!(!xpub_str.is_null()); + let xpub = CString::from_raw(xpub_str); + let xpub_string = xpub.to_string_lossy(); + assert!(xpub_string.starts_with("tpub")); // Testnet xpub should start with tpub + + // Test get network + let network = account_get_network(result.account); + assert_eq!(network as u32, FFINetwork::Testnet as u32); + + // Test get parent wallet id (may be null) + let _wallet_id = account_get_parent_wallet_id(result.account); + // Just check it doesn't crash - may be null + + // Test get account type + let mut index = 999u32; + let account_type = account_get_account_type(result.account, &mut index); + assert_eq!(account_type as u32, FFIAccountType::StandardBIP44 as u32); + assert_eq!(index, 0); // Account index should be 0 + + // Test is watch only - should be false for a wallet created from mnemonic + let is_watch_only = account_get_is_watch_only(result.account); + assert!(!is_watch_only); + + // Clean up + account_free(result.account); + } + } - // Check that error message contains helpful guidance + // Clean up error message if present if !result.error_message.is_null() { unsafe { - let c_str = std::ffi::CStr::from_ptr(result.error_message); - let msg = c_str.to_string_lossy(); - assert!(msg.contains("wallet_get_top_up_account_with_registration_index")); let _ = CString::from_raw(result.error_message); } } @@ -248,18 +235,29 @@ mod tests { } #[test] - fn test_account_type_values() { - // Test FFIAccountType enum values - assert_eq!(FFIAccountType::StandardBIP44 as u32, 0); - assert_eq!(FFIAccountType::StandardBIP32 as u32, 1); - assert_eq!(FFIAccountType::CoinJoin as u32, 2); - assert_eq!(FFIAccountType::IdentityRegistration as u32, 3); - assert_eq!(FFIAccountType::IdentityTopUp as u32, 4); - assert_eq!(FFIAccountType::IdentityTopUpNotBoundToIdentity as u32, 5); - assert_eq!(FFIAccountType::IdentityInvitation as u32, 6); - assert_eq!(FFIAccountType::ProviderVotingKeys as u32, 7); - assert_eq!(FFIAccountType::ProviderOwnerKeys as u32, 8); - assert_eq!(FFIAccountType::ProviderOperatorKeys as u32, 9); - assert_eq!(FFIAccountType::ProviderPlatformKeys as u32, 10); + fn test_account_getters_null_safety() { + unsafe { + // Test all getter functions with null pointers + let xpub = account_get_extended_public_key_as_string(ptr::null()); + assert!(xpub.is_null()); + + let network = account_get_network(ptr::null()); + assert_eq!(network as u32, FFINetwork::NoNetworks as u32); + + let wallet_id = account_get_parent_wallet_id(ptr::null()); + assert!(wallet_id.is_null()); + + let mut index = 0u32; + let account_type = account_get_account_type(ptr::null(), &mut index); + assert_eq!(account_type as u32, FFIAccountType::StandardBIP44 as u32); + assert_eq!(index, 0); + + // Test with null out_index + let account_type = account_get_account_type(ptr::null(), ptr::null_mut()); + assert_eq!(account_type as u32, FFIAccountType::StandardBIP44 as u32); + + let is_watch_only = account_get_is_watch_only(ptr::null()); + assert!(!is_watch_only); + } } } diff --git a/key-wallet-ffi/src/address.rs b/key-wallet-ffi/src/address.rs index 0fec4a946..3e06b5f5f 100644 --- a/key-wallet-ffi/src/address.rs +++ b/key-wallet-ffi/src/address.rs @@ -80,7 +80,17 @@ pub unsafe extern "C" fn address_validate( } }; - let network_rust: key_wallet::Network = network.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 false; + } + }; use std::str::FromStr; match key_wallet::Address::from_str(address_str) { @@ -150,7 +160,17 @@ pub unsafe extern "C" fn address_get_type( } }; - let network_rust: key_wallet::Network = network.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 u8::MAX; + } + }; use std::str::FromStr; match key_wallet::Address::from_str(address_str) { diff --git a/key-wallet-ffi/src/address_pool.rs b/key-wallet-ffi/src/address_pool.rs index 2475704e0..a8c6f4d59 100644 --- a/key-wallet-ffi/src/address_pool.rs +++ b/key-wallet-ffi/src/address_pool.rs @@ -3,13 +3,17 @@ //! This module provides FFI bindings for managing address pools within //! managed accounts, including gap limit management and address generation. +use std::ffi::CString; use std::os::raw::{c_char, c_uint}; use crate::error::{FFIError, FFIErrorCode}; use crate::transaction_checking::FFIManagedWallet; use crate::types::{FFIAccountType, FFINetwork, FFIWallet}; +use crate::utils::rust_string_to_c; use key_wallet::account::ManagedAccountCollection; -use key_wallet::managed_account::address_pool::KeySource; +use key_wallet::managed_account::address_pool::{ + AddressInfo, AddressPool, KeySource, PublicKeyType, +}; use key_wallet::managed_account::ManagedAccount; use key_wallet::AccountType; @@ -93,6 +97,128 @@ pub enum FFIAddressPoolType { Single = 2, } +/// FFI wrapper for an AddressPool from a ManagedAccount +/// +/// This is a lightweight wrapper that holds a reference to an AddressPool +/// from within a ManagedAccount. It allows querying addresses and pool information. +pub struct FFIAddressPool { + /// Reference to the address pool (mutable for internal consistency even if not modified) + pub(crate) pool: *mut AddressPool, + /// Pool type to track what kind of pool this is + #[allow(dead_code)] + pub(crate) pool_type: FFIAddressPoolType, +} + +/// FFI-compatible version of AddressInfo +#[repr(C)] +pub struct FFIAddressInfo { + /// Address as string + pub address: *mut c_char, + /// Script pubkey bytes + pub script_pubkey: *mut u8, + /// Length of script pubkey + pub script_pubkey_len: usize, + /// Public key bytes (nullable) + pub public_key: *mut u8, + /// Length of public key + pub public_key_len: usize, + /// Derivation index + pub index: u32, + /// Derivation path as string + pub path: *mut c_char, + /// Whether address has been used + pub used: bool, + /// When generated (timestamp) + pub generated_at: u64, + /// When first used (0 if never) + pub used_at: u64, + /// Transaction count + pub tx_count: u32, + /// Total received + pub total_received: u64, + /// Total sent + pub total_sent: u64, + /// Current balance + pub balance: u64, + /// Custom label (nullable) + pub label: *mut c_char, +} + +/// Convert from AddressInfo to FFIAddressInfo +fn address_info_to_ffi(info: &AddressInfo) -> FFIAddressInfo { + // Convert address to string + let address_str = rust_string_to_c(info.address.to_string()); + + // Convert script pubkey to bytes + let script_bytes = info.script_pubkey.as_bytes(); + let script_pubkey_len = script_bytes.len(); + let script_pubkey = if script_pubkey_len > 0 { + let mut bytes = Vec::with_capacity(script_pubkey_len); + bytes.extend_from_slice(script_bytes); + Box::into_raw(bytes.into_boxed_slice()) as *mut u8 + } else { + std::ptr::null_mut() + }; + + // Convert public key to bytes if present + let (public_key, public_key_len) = match &info.public_key { + Some(pk) => match pk { + PublicKeyType::ECDSA(bytes) + | PublicKeyType::EdDSA(bytes) + | PublicKeyType::BLS(bytes) => { + let len = bytes.len(); + if len > 0 { + let mut key_bytes = Vec::with_capacity(len); + key_bytes.extend_from_slice(bytes); + (Box::into_raw(key_bytes.into_boxed_slice()) as *mut u8, len) + } else { + (std::ptr::null_mut(), 0) + } + } + }, + None => (std::ptr::null_mut(), 0), + }; + + // Convert derivation path to string + let path_str = rust_string_to_c(info.path.to_string()); + + // Convert label if present + let label = + info.label.as_ref().map(|l| rust_string_to_c(l.clone())).unwrap_or(std::ptr::null_mut()); + + FFIAddressInfo { + address: address_str, + script_pubkey, + script_pubkey_len, + public_key, + public_key_len, + index: info.index, + path: path_str, + used: info.used, + generated_at: info.generated_at, + used_at: info.used_at.unwrap_or(0), + tx_count: info.tx_count, + total_received: info.total_received, + total_sent: info.total_sent, + balance: info.balance, + label, + } +} + +/// Free an address pool handle +/// +/// # Safety +/// +/// - `pool` must be a valid pointer to an FFIAddressPool that was allocated by this library +/// - The pointer must not be used after calling this function +/// - This function must only be called once per allocation +#[no_mangle] +pub unsafe extern "C" fn address_pool_free(pool: *mut FFIAddressPool) { + if !pool.is_null() { + let _ = Box::from_raw(pool); + } +} + /// Address pool info #[repr(C)] pub struct FFIAddressPoolInfo { @@ -121,9 +247,8 @@ pub struct FFIAddressPoolInfo { pub unsafe extern "C" fn managed_wallet_get_address_pool_info( managed_wallet: *const FFIManagedWallet, network: FFINetwork, - account_type: c_uint, + account_type: FFIAccountType, account_index: c_uint, - registration_index: c_uint, pool_type: FFIAddressPoolType, info_out: *mut FFIAddressPoolInfo, error: *mut FFIError, @@ -134,49 +259,19 @@ pub unsafe extern "C" fn managed_wallet_get_address_pool_info( } let managed_wallet = &*(*managed_wallet).inner; - let network_rust: key_wallet::Network = network.into(); - - // Convert FFI account type to AccountType - let account_type_enum = match account_type { - 0 => FFIAccountType::StandardBIP44, - 1 => FFIAccountType::StandardBIP32, - 2 => FFIAccountType::CoinJoin, - 3 => FFIAccountType::IdentityRegistration, - 4 => FFIAccountType::IdentityTopUp, - 5 => FFIAccountType::IdentityTopUpNotBoundToIdentity, - 6 => FFIAccountType::IdentityInvitation, - 7 => FFIAccountType::ProviderVotingKeys, - 8 => FFIAccountType::ProviderOwnerKeys, - 9 => FFIAccountType::ProviderOperatorKeys, - 10 => FFIAccountType::ProviderPlatformKeys, - _ => { + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { FFIError::set_error( error, FFIErrorCode::InvalidInput, - format!("Invalid account type: {}", account_type), + "Must specify exactly one network".to_string(), ); return false; } }; - let registration_index_opt = if account_type == 4 { - Some(registration_index) - } else { - None - }; - - let account_type_rust = - match account_type_enum.to_account_type(account_index, registration_index_opt) { - Some(at) => at, - None => { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - "Invalid account type parameters".to_string(), - ); - return false; - } - }; + let account_type_rust = account_type.to_account_type(account_index); // Get the account collection let collection = match managed_wallet.accounts.get(&network_rust) { @@ -282,9 +377,8 @@ pub unsafe extern "C" fn managed_wallet_get_address_pool_info( pub unsafe extern "C" fn managed_wallet_set_gap_limit( managed_wallet: *mut FFIManagedWallet, network: FFINetwork, - account_type: c_uint, + account_type: FFIAccountType, account_index: c_uint, - registration_index: c_uint, pool_type: FFIAddressPoolType, gap_limit: c_uint, error: *mut FFIError, @@ -295,49 +389,19 @@ pub unsafe extern "C" fn managed_wallet_set_gap_limit( } let managed_wallet = &mut *(*managed_wallet).inner; - let network_rust: key_wallet::Network = network.into(); - - // Convert FFI account type to AccountType - let account_type_enum = match account_type { - 0 => FFIAccountType::StandardBIP44, - 1 => FFIAccountType::StandardBIP32, - 2 => FFIAccountType::CoinJoin, - 3 => FFIAccountType::IdentityRegistration, - 4 => FFIAccountType::IdentityTopUp, - 5 => FFIAccountType::IdentityTopUpNotBoundToIdentity, - 6 => FFIAccountType::IdentityInvitation, - 7 => FFIAccountType::ProviderVotingKeys, - 8 => FFIAccountType::ProviderOwnerKeys, - 9 => FFIAccountType::ProviderOperatorKeys, - 10 => FFIAccountType::ProviderPlatformKeys, - _ => { + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { FFIError::set_error( error, FFIErrorCode::InvalidInput, - format!("Invalid account type: {}", account_type), + "Must specify exactly one network".to_string(), ); return false; } }; - let registration_index_opt = if account_type == 4 { - Some(registration_index) - } else { - None - }; - - let account_type_rust = - match account_type_enum.to_account_type(account_index, registration_index_opt) { - Some(at) => at, - None => { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - "Invalid account type parameters".to_string(), - ); - return false; - } - }; + let account_type_rust = account_type.to_account_type(account_index); // Get the account collection let collection = match managed_wallet.accounts.get_mut(&network_rust) { @@ -433,9 +497,8 @@ pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( managed_wallet: *mut FFIManagedWallet, wallet: *const FFIWallet, network: FFINetwork, - account_type: c_uint, + account_type: FFIAccountType, account_index: c_uint, - registration_index: c_uint, pool_type: FFIAddressPoolType, target_index: c_uint, error: *mut FFIError, @@ -447,79 +510,21 @@ pub unsafe extern "C" fn managed_wallet_generate_addresses_to_index( let managed_wallet = &mut *(*managed_wallet).inner; let wallet = &*wallet; - let network_rust: key_wallet::Network = network.into(); - - // Convert FFI account type to AccountType - let account_type_enum = match account_type { - 0 => FFIAccountType::StandardBIP44, - 1 => FFIAccountType::StandardBIP32, - 2 => FFIAccountType::CoinJoin, - 3 => FFIAccountType::IdentityRegistration, - 4 => FFIAccountType::IdentityTopUp, - 5 => FFIAccountType::IdentityTopUpNotBoundToIdentity, - 6 => FFIAccountType::IdentityInvitation, - 7 => FFIAccountType::ProviderVotingKeys, - 8 => FFIAccountType::ProviderOwnerKeys, - 9 => FFIAccountType::ProviderOperatorKeys, - 10 => FFIAccountType::ProviderPlatformKeys, - _ => { + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { FFIError::set_error( error, FFIErrorCode::InvalidInput, - format!("Invalid account type: {}", account_type), + "Must specify exactly one network".to_string(), ); return false; } }; - let registration_index_opt = if account_type == 4 { - Some(registration_index) - } else { - None - }; + let account_type_rust = account_type.to_account_type(account_index); - let account_type_rust = - match account_type_enum.to_account_type(account_index, registration_index_opt) { - Some(at) => at, - None => { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - "Invalid account type parameters".to_string(), - ); - return false; - } - }; - - // Get the wallet account for the xpub - // Convert AccountType to AccountTypeToCheck - use key_wallet::transaction_checking::transaction_router::AccountTypeToCheck; - let account_type_to_check = match &account_type_rust { - AccountType::Standard { - standard_account_type, - .. - } => match standard_account_type { - key_wallet::account::StandardAccountType::BIP44Account => { - AccountTypeToCheck::StandardBIP44 - } - key_wallet::account::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, - }; + let account_type_to_check = account_type_rust.into(); let xpub_opt = wallet.inner().extended_public_key_for_account_type( &account_type_to_check, @@ -676,7 +681,17 @@ pub unsafe extern "C" fn managed_wallet_mark_address_used( } let managed_wallet = &mut *(*managed_wallet).inner; - let network_rust: key_wallet::Network = network.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 false; + } + }; // Parse the address string let address_str = match std::ffi::CStr::from_ptr(address).to_str() { @@ -822,6 +837,167 @@ pub unsafe extern "C" fn managed_wallet_mark_address_used( } } +/// Get a single address info at a specific index from the pool +/// +/// Returns detailed information about the address at the given index, or NULL +/// if the index is out of bounds or not generated yet. +/// +/// # Safety +/// +/// - `pool` must be a valid pointer to an FFIAddressPool +/// - `error` must be a valid pointer to an FFIError or null +/// - The returned FFIAddressInfo must be freed using `address_info_free` +#[no_mangle] +pub unsafe extern "C" fn address_pool_get_address_at_index( + pool: *const FFIAddressPool, + index: u32, + error: *mut FFIError, +) -> *mut FFIAddressInfo { + if pool.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return std::ptr::null_mut(); + } + + let pool = &*pool; + let address_pool = &*pool.pool; + + // Get the address info at the specified index + match address_pool.info_at_index(index) { + Some(info) => { + let ffi_info = address_info_to_ffi(info); + FFIError::set_success(error); + Box::into_raw(Box::new(ffi_info)) + } + None => { + FFIError::set_error( + error, + FFIErrorCode::NotFound, + format!("No address at index {}", index), + ); + std::ptr::null_mut() + } + } +} + +/// Get a range of addresses from the pool +/// +/// Returns an array of FFIAddressInfo structures for addresses in the range [start_index, end_index). +/// The count_out parameter will be set to the actual number of addresses returned. +/// +/// Note: This function only reads existing addresses from the pool. It does not generate new addresses. +/// Use managed_wallet_generate_addresses_to_index if you need to generate addresses first. +/// +/// # Safety +/// +/// - `pool` must be a valid pointer to an FFIAddressPool +/// - `count_out` must be a valid pointer to store the count +/// - `error` must be a valid pointer to an FFIError or null +/// - The returned array must be freed using `address_info_array_free` +#[no_mangle] +pub unsafe extern "C" fn address_pool_get_addresses_in_range( + pool: *const FFIAddressPool, + start_index: u32, + end_index: u32, + count_out: *mut usize, + error: *mut FFIError, +) -> *mut *mut FFIAddressInfo { + if pool.is_null() || count_out.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return std::ptr::null_mut(); + } + + *count_out = 0; + + if end_index <= start_index { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "End index must be greater than start index".to_string(), + ); + return std::ptr::null_mut(); + } + + let pool = &*pool; + let address_pool = &*pool.pool; + + // Collect address infos in the range + let mut infos = Vec::new(); + + for idx in start_index..end_index { + if let Some(info) = address_pool.info_at_index(idx) { + infos.push(Box::into_raw(Box::new(address_info_to_ffi(info)))); + } + } + + if infos.is_empty() { + FFIError::set_error( + error, + FFIErrorCode::NotFound, + "No addresses found in the specified range".to_string(), + ); + return std::ptr::null_mut(); + } + + *count_out = infos.len(); + let array_ptr = Box::into_raw(infos.into_boxed_slice()) as *mut *mut FFIAddressInfo; + + FFIError::set_success(error); + array_ptr +} + +/// Free a single FFIAddressInfo structure +/// +/// # Safety +/// +/// - `info` must be a valid pointer to an FFIAddressInfo allocated by this library or null +/// - The pointer must not be used after calling this function +#[no_mangle] +pub unsafe extern "C" fn address_info_free(info: *mut FFIAddressInfo) { + if !info.is_null() { + let info = Box::from_raw(info); + + // Free the C strings + if !info.address.is_null() { + let _ = CString::from_raw(info.address); + } + if !info.path.is_null() { + let _ = CString::from_raw(info.path); + } + if !info.label.is_null() { + let _ = CString::from_raw(info.label); + } + + // Free the byte arrays + if !info.script_pubkey.is_null() && info.script_pubkey_len > 0 { + let _ = Box::from_raw(std::slice::from_raw_parts_mut( + info.script_pubkey, + info.script_pubkey_len, + )); + } + if !info.public_key.is_null() && info.public_key_len > 0 { + let _ = + Box::from_raw(std::slice::from_raw_parts_mut(info.public_key, info.public_key_len)); + } + } +} + +/// Free an array of FFIAddressInfo structures +/// +/// # Safety +/// +/// - `infos` must be a valid pointer to an array of FFIAddressInfo pointers allocated by this library or null +/// - `count` must be the exact number of elements in the array +/// - The pointers must not be used after calling this function +#[no_mangle] +pub unsafe extern "C" fn address_info_array_free(infos: *mut *mut FFIAddressInfo, count: usize) { + if !infos.is_null() && count > 0 { + let array = Box::from_raw(std::slice::from_raw_parts_mut(infos, count)); + for info_ptr in array.iter() { + address_info_free(*info_ptr); + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -832,4 +1008,301 @@ mod tests { assert_eq!(FFIAddressPoolType::Internal as u32, 1); assert_eq!(FFIAddressPoolType::Single as u32, 2); } + + #[test] + fn test_address_info_conversion() { + // Test the FFI conversion function with a mock AddressInfo + use key_wallet::bip32::DerivationPath; + use std::str::FromStr; + + // Create a test address programmatically + use dashcore::PublicKey; + + // Use a valid compressed public key (this is a well-known test key) + let pubkey_bytes = [ + 0x02, // Compressed pubkey prefix + 0x50, 0x86, 0x3a, 0xd6, 0x4a, 0x87, 0xae, 0x8a, 0x2f, 0xe8, 0x3c, 0x1a, 0xf1, 0xa8, + 0x40, 0x3c, 0xb5, 0x3f, 0x53, 0xe4, 0x86, 0xd8, 0x51, 0x1d, 0xad, 0x8a, 0x04, 0x88, + 0x7e, 0x5b, 0x23, 0x52, + ]; + let pubkey = PublicKey::from_slice(&pubkey_bytes).unwrap(); + let test_address = dashcore::Address::p2pkh(&pubkey, key_wallet::Network::Testnet); + + let test_path = DerivationPath::from_str("m/44'/5'/0'/0/0").unwrap(); + + let info = AddressInfo { + address: test_address.clone(), + script_pubkey: test_address.script_pubkey(), + public_key: Some(PublicKeyType::ECDSA(vec![0x02, 0x03, 0x04])), + index: 0, + path: test_path, + used: false, + generated_at: 1234567890, + used_at: None, + tx_count: 0, + total_received: 0, + total_sent: 0, + balance: 0, + label: Some("Test Label".to_string()), + metadata: std::collections::BTreeMap::new(), + }; + + // Convert to FFI + let ffi_info = address_info_to_ffi(&info); + + // Verify basic fields + assert_eq!(ffi_info.index, 0); + assert!(!ffi_info.used); + assert_eq!(ffi_info.generated_at, 1234567890); + assert_eq!(ffi_info.used_at, 0); + assert_eq!(ffi_info.public_key_len, 3); + assert!(ffi_info.script_pubkey_len > 0); + + // Clean up the FFI structure + unsafe { + let boxed = Box::new(ffi_info); + address_info_free(Box::into_raw(boxed)); + } + } + + #[test] + fn test_address_info_free() { + // Test that free functions handle NULL gracefully + unsafe { + address_info_free(std::ptr::null_mut()); + address_info_array_free(std::ptr::null_mut(), 0); + address_info_array_free(std::ptr::null_mut(), 10); + } + } + + #[test] + fn test_address_pool_get_address_at_index() { + // Test the simplified address_pool_get_address_at_index function + unsafe { + use crate::managed_account::{ + managed_account_free, managed_account_get_external_address_pool, + }; + use crate::wallet_manager::{ + wallet_manager_add_wallet_from_mnemonic_with_options, wallet_manager_create, + wallet_manager_free, wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, + }; + use std::ffi::CString; + use std::ptr; + + let test_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Add a wallet with default accounts + let mnemonic = CString::new(test_mnemonic).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), + &mut error, + ); + assert!(success); + assert_eq!(error.code, FFIErrorCode::Success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + assert_eq!(count_out, 1); + assert!(!wallet_ids_out.is_null()); + + // Get a standard BIP44 managed account + let result = crate::managed_account::managed_wallet_get_account( + manager, + wallet_ids_out, + FFINetwork::Testnet, + 0, + FFIAccountType::StandardBIP44, + ); + + assert!(!result.account.is_null()); + assert_eq!(result.error_code, 0); + + let account = result.account; + + // Get external address pool + let external_pool = managed_account_get_external_address_pool(account); + assert!(!external_pool.is_null()); + + // Test getting address at index 0 (should exist by default) + let address_info = address_pool_get_address_at_index(external_pool, 0, &mut error); + + if !address_info.is_null() { + // Verify the address info + let info = &*address_info; + assert_eq!(info.index, 0); + assert!(!info.address.is_null()); + assert!(!info.path.is_null()); + + // Clean up address info + address_info_free(address_info); + } + + // Test getting address at an out-of-bounds index + let invalid_info = address_pool_get_address_at_index(external_pool, 10000, &mut error); + assert!(invalid_info.is_null()); + assert_eq!(error.code, FFIErrorCode::NotFound); + + // Test null pool + let null_info = address_pool_get_address_at_index(ptr::null(), 0, &mut error); + assert!(null_info.is_null()); + assert_eq!(error.code, FFIErrorCode::InvalidInput); + + // Clean up + address_pool_free(external_pool); + managed_account_free(account); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } + } + + #[test] + fn test_address_pool_get_addresses_in_range() { + // Test the simplified address_pool_get_addresses_in_range function + unsafe { + use crate::managed_account::{ + managed_account_free, managed_account_get_external_address_pool, + }; + use crate::wallet_manager::{ + wallet_manager_add_wallet_from_mnemonic_with_options, wallet_manager_create, + wallet_manager_free, wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, + }; + use std::ffi::CString; + use std::ptr; + + let test_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Add a wallet with default accounts + let mnemonic = CString::new(test_mnemonic).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), + &mut error, + ); + assert!(success); + assert_eq!(error.code, FFIErrorCode::Success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + assert_eq!(count_out, 1); + assert!(!wallet_ids_out.is_null()); + + // Get a standard BIP44 managed account + let result = crate::managed_account::managed_wallet_get_account( + manager, + wallet_ids_out, + FFINetwork::Testnet, + 0, + FFIAccountType::StandardBIP44, + ); + + assert!(!result.account.is_null()); + assert_eq!(result.error_code, 0); + + let account = result.account; + + // Get external address pool + let external_pool = managed_account_get_external_address_pool(account); + assert!(!external_pool.is_null()); + + // Test getting a range of addresses + let mut addresses_count: usize = 0; + let addresses = address_pool_get_addresses_in_range( + external_pool, + 0, + 5, + &mut addresses_count, + &mut error, + ); + + // The pool might not have 5 addresses generated yet, but should have at least 1 + if !addresses.is_null() && addresses_count > 0 { + // Verify we got some addresses + assert!(addresses_count <= 5); + assert_eq!(error.code, FFIErrorCode::Success); + + // Clean up addresses + address_info_array_free(addresses, addresses_count); + } + + // Test invalid range (end <= start) + let invalid_addresses = address_pool_get_addresses_in_range( + external_pool, + 5, + 5, + &mut addresses_count, + &mut error, + ); + assert!(invalid_addresses.is_null()); + assert_eq!(error.code, FFIErrorCode::InvalidInput); + + // Test null pool + let null_addresses = address_pool_get_addresses_in_range( + ptr::null(), + 0, + 5, + &mut addresses_count, + &mut error, + ); + assert!(null_addresses.is_null()); + assert_eq!(error.code, FFIErrorCode::InvalidInput); + + // Test null count_out + let null_count_addresses = address_pool_get_addresses_in_range( + external_pool, + 0, + 5, + ptr::null_mut(), + &mut error, + ); + assert!(null_count_addresses.is_null()); + assert_eq!(error.code, FFIErrorCode::InvalidInput); + + // Clean up + address_pool_free(external_pool); + managed_account_free(account); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } + } } diff --git a/key-wallet-ffi/src/balance.rs b/key-wallet-ffi/src/balance.rs deleted file mode 100644 index d47669acd..000000000 --- a/key-wallet-ffi/src/balance.rs +++ /dev/null @@ -1,107 +0,0 @@ -//! Balance tracking - -#[cfg(test)] -#[path = "balance_tests.rs"] -mod tests; - -use std::os::raw::c_uint; - -use crate::error::{FFIError, FFIErrorCode}; -use crate::types::{FFINetwork, FFIWallet}; - -/// Balance structure for FFI -#[repr(C)] -#[derive(Default)] -pub struct FFIBalance { - pub confirmed: u64, - pub unconfirmed: u64, - pub immature: u64, - pub total: u64, -} - -impl From for FFIBalance { - fn from(balance: key_wallet::WalletBalance) -> Self { - FFIBalance { - confirmed: balance.confirmed, - unconfirmed: balance.unconfirmed, - immature: 0, // key_wallet doesn't have immature field - total: balance.confirmed + balance.unconfirmed, - } - } -} - -/// Get wallet balance -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - `balance_out` must be a valid pointer to an FFIBalance structure -/// - `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 -#[no_mangle] -pub unsafe extern "C" fn wallet_get_balance( - wallet: *const FFIWallet, - network: FFINetwork, - balance_out: *mut FFIBalance, - error: *mut FFIError, -) -> bool { - if wallet.is_null() || balance_out.is_null() { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); - return false; - } - - let _wallet = &*wallet; - let _network_rust: key_wallet::Network = network.into(); - - // Note: get_balance is not directly available on Wallet - // Would need to aggregate from accounts - *balance_out = FFIBalance::default(); - - FFIError::set_success(error); - true -} - -/// Get account balance -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet instance -/// - `balance_out` must be a valid pointer to an FFIBalance structure -/// - `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 -#[no_mangle] -pub unsafe extern "C" fn wallet_get_account_balance( - wallet: *const FFIWallet, - network: FFINetwork, - account_index: c_uint, - balance_out: *mut FFIBalance, - error: *mut FFIError, -) -> bool { - if wallet.is_null() || balance_out.is_null() { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); - return false; - } - - let wallet = &*wallet; - let network_rust: key_wallet::Network = network.into(); - - use key_wallet::account::account_type::{AccountType, StandardAccountType}; - let _account_type = AccountType::Standard { - index: account_index, - standard_account_type: StandardAccountType::BIP44Account, - }; - - match wallet.inner().get_bip44_account(network_rust, account_index) { - Some(_account) => { - // Note: get_balance is not directly available on Account - // Would need to implement balance tracking - *balance_out = FFIBalance::default(); - FFIError::set_success(error); - true - } - None => { - FFIError::set_error(error, FFIErrorCode::NotFound, "Account not found".to_string()); - false - } - } -} diff --git a/key-wallet-ffi/src/balance_tests.rs b/key-wallet-ffi/src/balance_tests.rs deleted file mode 100644 index 8ce36b83d..000000000 --- a/key-wallet-ffi/src/balance_tests.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Unit tests for balance FFI module - -#[cfg(test)] -mod tests { - use crate::balance::{self, FFIBalance}; - use crate::error::{FFIError, FFIErrorCode}; - use crate::types::FFINetwork; - use crate::wallet; - use std::ptr; - - unsafe fn create_test_wallet() -> (*mut crate::types::FFIWallet, *mut FFIError) { - let mut error = FFIError::success(); - let error_ptr = &mut error as *mut FFIError; - - let wallet = wallet::wallet_create_random(FFINetwork::Testnet, error_ptr); - - (wallet, error_ptr) - } - - #[test] - fn test_balance_retrieval() { - let (wallet, error) = unsafe { create_test_wallet() }; - assert!(!wallet.is_null()); - - let mut balance = FFIBalance::default(); - - let success = unsafe { - balance::wallet_get_balance(wallet, FFINetwork::Testnet, &mut balance, error) - }; - - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Balance should be zero for new wallet - assert_eq!(balance.confirmed, 0); - assert_eq!(balance.unconfirmed, 0); - assert_eq!(balance.total, 0); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_account_balance() { - let (wallet, error) = unsafe { create_test_wallet() }; - assert!(!wallet.is_null()); - - let mut balance = FFIBalance::default(); - - let success = unsafe { - balance::wallet_get_account_balance( - wallet, - FFINetwork::Testnet, - 0, // account_index - &mut balance, - error, - ) - }; - - assert!(success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // Balance should be zero for new account - assert_eq!(balance.confirmed, 0); - assert_eq!(balance.unconfirmed, 0); - - // Test non-existent account - let success = unsafe { - balance::wallet_get_account_balance( - wallet, - FFINetwork::Testnet, - 999, // non-existent account - &mut balance, - error, - ) - }; - - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::NotFound); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_balance_for_multiple_networks() { - let (wallet, error) = unsafe { create_test_wallet() }; - assert!(!wallet.is_null()); - - let networks = [FFINetwork::Dash, FFINetwork::Testnet, FFINetwork::Devnet]; - - unsafe { - for network in networks.iter() { - let mut balance = FFIBalance::default(); - - let success = balance::wallet_get_balance(wallet, *network, &mut balance, error); - - assert!(success); - assert_eq!(balance.confirmed, 0); - assert_eq!(balance.unconfirmed, 0); - } - } - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } - - #[test] - fn test_balance_with_null_wallet() { - let mut error = FFIError::success(); - let error = &mut error as *mut FFIError; - let mut balance = FFIBalance::default(); - - let success = unsafe { - balance::wallet_get_balance(ptr::null(), FFINetwork::Testnet, &mut balance, error) - }; - - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - } - - #[test] - fn test_balance_null_checks() { - let (wallet, _) = unsafe { create_test_wallet() }; - assert!(!wallet.is_null()); - - let mut error = FFIError::success(); - let error = &mut error as *mut FFIError; - - // Test with null balance output - let success = unsafe { - balance::wallet_get_balance(wallet, FFINetwork::Testnet, ptr::null_mut(), error) - }; - - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Test account balance with null output - let success = unsafe { - balance::wallet_get_account_balance( - wallet, - FFINetwork::Testnet, - 0, - ptr::null_mut(), - error, - ) - }; - - assert!(!success); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - } - } -} diff --git a/key-wallet-ffi/src/derivation.rs b/key-wallet-ffi/src/derivation.rs index 6df61734b..31b36edd5 100644 --- a/key-wallet-ffi/src/derivation.rs +++ b/key-wallet-ffi/src/derivation.rs @@ -1,13 +1,13 @@ //! BIP32 and DIP9 derivation path functions +use crate::error::{FFIError, FFIErrorCode}; +use crate::types::FFINetwork; +use dash_network::Network; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_uint}; use std::ptr; use std::slice; -use crate::error::{FFIError, FFIErrorCode}; -use crate::types::FFINetwork; - /// Derivation path type for DIP9 #[repr(C)] #[derive(Clone, Copy)] @@ -61,7 +61,17 @@ pub unsafe extern "C" fn derivation_new_master_key( } let seed_slice = slice::from_raw_parts(seed, seed_len); - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; match key_wallet::bip32::ExtendedPrivKey::new_master(network_rust, seed_slice) { Ok(xpriv) => { @@ -99,7 +109,17 @@ pub extern "C" fn derivation_bip44_account_path( return false; } - let network_rust: key_wallet::Network = network.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 false; + } + }; use key_wallet::bip32::DerivationPath; let derivation = DerivationPath::bip_44_account(network_rust, account_index); @@ -156,7 +176,17 @@ pub extern "C" fn derivation_bip44_payment_path( return false; } - let network_rust: key_wallet::Network = network.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 false; + } + }; use key_wallet::bip32::DerivationPath; let derivation = @@ -212,7 +242,17 @@ pub extern "C" fn derivation_coinjoin_path( return false; } - let network_rust: key_wallet::Network = network.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 false; + } + }; use key_wallet::bip32::DerivationPath; let derivation = DerivationPath::coinjoin_path(network_rust, account_index); @@ -267,7 +307,17 @@ pub extern "C" fn derivation_identity_registration_path( return false; } - let network_rust: key_wallet::Network = network.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 false; + } + }; use key_wallet::bip32::DerivationPath; let derivation = DerivationPath::identity_registration_path(network_rust, identity_index); @@ -323,7 +373,17 @@ pub extern "C" fn derivation_identity_topup_path( return false; } - let network_rust: key_wallet::Network = network.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 false; + } + }; use key_wallet::bip32::DerivationPath; let derivation = @@ -380,7 +440,17 @@ pub extern "C" fn derivation_identity_authentication_path( return false; } - let network_rust: key_wallet::Network = network.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 false; + } + }; use key_wallet::bip32::{DerivationPath, KeyDerivationType}; let derivation = DerivationPath::identity_authentication_path( @@ -444,7 +514,7 @@ pub unsafe extern "C" fn derivation_derive_private_key_from_seed( } let seed_slice = slice::from_raw_parts(seed, seed_len); - let network_rust: key_wallet::Network = network.into(); + let network_rust: Network = network.try_into().unwrap_or(Network::Dash); let path_str = match CStr::from_ptr(path).to_str() { Ok(s) => s, @@ -720,7 +790,7 @@ pub unsafe extern "C" fn dip9_derive_identity_key( } let seed_slice = slice::from_raw_parts(seed, seed_len); - let network_rust: key_wallet::Network = network.into(); + let network_rust: Network = network.try_into().unwrap_or(Network::Dash); use key_wallet::bip32::{ChildNumber, DerivationPath}; use key_wallet::dip9::{ diff --git a/key-wallet-ffi/src/keys.rs b/key-wallet-ffi/src/keys.rs index 5efe97a3e..08eb3290a 100644 --- a/key-wallet-ffi/src/keys.rs +++ b/key-wallet-ffi/src/keys.rs @@ -46,7 +46,17 @@ pub unsafe extern "C" fn wallet_get_account_xpriv( } let wallet = unsafe { &*wallet }; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; match wallet.inner().get_bip44_account(network_rust, account_index) { Some(account) => { @@ -96,7 +106,17 @@ pub unsafe extern "C" fn wallet_get_account_xpub( } let wallet = unsafe { &*wallet }; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; match wallet.inner().get_bip44_account(network_rust, account_index) { Some(account) => { @@ -170,7 +190,17 @@ pub unsafe extern "C" fn wallet_derive_private_key( }; let wallet = unsafe { &*wallet }; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; // Use the new wallet method to derive the private key match wallet.inner().derive_private_key(network_rust, &path) { @@ -240,7 +270,17 @@ pub unsafe extern "C" fn wallet_derive_extended_private_key( }; let wallet = unsafe { &*wallet }; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; // Use the new wallet method to derive the extended private key match wallet.inner().derive_extended_private_key(network_rust, &path) { @@ -309,7 +349,17 @@ pub unsafe extern "C" fn wallet_derive_private_key_as_wif( }; let wallet = unsafe { &*wallet }; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; // Use the new wallet method to derive the private key as WIF match wallet.inner().derive_private_key_as_wif(network_rust, &path) { @@ -462,7 +512,17 @@ pub unsafe extern "C" fn private_key_to_wif( } let key = unsafe { &*key }; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; // Convert to WIF format use dashcore::PrivateKey as DashPrivateKey; @@ -537,7 +597,17 @@ pub unsafe extern "C" fn wallet_derive_public_key( unsafe { let wallet = &*wallet; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; // Use the new wallet method to derive the public key match wallet.inner().derive_public_key(network_rust, &path) { @@ -609,7 +679,17 @@ pub unsafe extern "C" fn wallet_derive_extended_public_key( unsafe { let wallet = &*wallet; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; // Use the new wallet method to derive the extended public key match wallet.inner().derive_extended_public_key(network_rust, &path) { @@ -680,7 +760,17 @@ pub unsafe extern "C" fn wallet_derive_public_key_as_hex( unsafe { let wallet = &*wallet; - let network_rust: key_wallet::Network = network.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 ptr::null_mut(); + } + }; // Use the new wallet method to derive the public key as hex match wallet.inner().derive_public_key_as_hex(network_rust, &path) { diff --git a/key-wallet-ffi/src/lib.rs b/key-wallet-ffi/src/lib.rs index 4eaebb12f..727437bc5 100644 --- a/key-wallet-ffi/src/lib.rs +++ b/key-wallet-ffi/src/lib.rs @@ -5,12 +5,14 @@ // Module declarations pub mod account; +pub mod account_collection; pub mod address; pub mod address_pool; -pub mod balance; pub mod derivation; pub mod error; pub mod keys; +pub mod managed_account; +pub mod managed_account_collection; pub mod managed_wallet; pub mod mnemonic; pub mod provider_keys; @@ -28,9 +30,8 @@ pub mod bip38; // Test modules are now included in each source file // Re-export main types for convenience -pub use balance::FFIBalance; pub use error::{FFIError, FFIErrorCode}; -pub use types::{FFINetwork, FFIWallet}; +pub use types::{FFIBalance, FFINetwork, FFIWallet}; pub use utxo::FFIUTXO; // ============================================================================ diff --git a/key-wallet-ffi/src/managed_account.rs b/key-wallet-ffi/src/managed_account.rs new file mode 100644 index 000000000..c991b3427 --- /dev/null +++ b/key-wallet-ffi/src/managed_account.rs @@ -0,0 +1,1329 @@ +//! Managed account FFI bindings +//! +//! This module provides FFI-compatible managed account functionality that wraps +//! ManagedAccount instances from the key-wallet crate. FFIManagedAccount is a +//! simple wrapper around Arc without additional fields. + +use std::os::raw::c_uint; +use std::sync::Arc; + +use crate::address_pool::{FFIAddressPool, FFIAddressPoolType}; +use crate::error::{FFIError, FFIErrorCode}; +use crate::types::{FFIAccountType, FFINetwork}; +use crate::wallet_manager::FFIWalletManager; +use key_wallet::managed_account::address_pool::AddressPool; +use key_wallet::managed_account::ManagedAccount; +use key_wallet::AccountType; + +/// Opaque managed account handle that wraps ManagedAccount +pub struct FFIManagedAccount { + /// The underlying managed account + pub(crate) account: Arc, +} + +impl FFIManagedAccount { + /// Create a new FFI managed account handle + pub fn new(account: &ManagedAccount) -> Self { + FFIManagedAccount { + account: Arc::new(account.clone()), + } + } + + /// Get a reference to the inner managed account + pub fn inner(&self) -> &ManagedAccount { + self.account.as_ref() + } +} + +/// FFI Result type for ManagedAccount operations +#[repr(C)] +pub struct FFIManagedAccountResult { + /// The managed account handle if successful, NULL if error + pub account: *mut FFIManagedAccount, + /// Error code (0 = success) + pub error_code: i32, + /// Error message (NULL if success, must be freed by caller if not NULL) + pub error_message: *mut std::os::raw::c_char, +} + +impl FFIManagedAccountResult { + /// Create a success result + pub fn success(account: *mut FFIManagedAccount) -> Self { + FFIManagedAccountResult { + account, + error_code: 0, + error_message: std::ptr::null_mut(), + } + } + + /// Create an error result + pub fn error(code: FFIErrorCode, message: String) -> Self { + use std::ffi::CString; + let c_message = CString::new(message).unwrap_or_else(|_| { + CString::new("Unknown error").expect("Hardcoded string should never fail") + }); + FFIManagedAccountResult { + account: std::ptr::null_mut(), + error_code: code as i32, + error_message: c_message.into_raw(), + } + } +} + +/// Get a managed account from a managed wallet +/// +/// This function gets a ManagedAccount from the wallet manager's managed wallet info, +/// returning a managed account handle that wraps the ManagedAccount. +/// +/// # Safety +/// +/// - `manager` must be a valid pointer to an FFIWalletManager instance +/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID +/// - `network` must specify exactly one network +/// - The caller must ensure all pointers remain valid for the duration of this call +/// - The returned account must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_wallet_get_account( + manager: *const FFIWalletManager, + wallet_id: *const u8, + network: FFINetwork, + account_index: c_uint, + account_type: FFIAccountType, +) -> FFIManagedAccountResult { + if manager.is_null() { + return FFIManagedAccountResult::error( + FFIErrorCode::InvalidInput, + "Manager is null".to_string(), + ); + } + + if wallet_id.is_null() { + return FFIManagedAccountResult::error( + FFIErrorCode::InvalidInput, + "Wallet ID is null".to_string(), + ); + } + + // Convert wallet_id to array + let mut wallet_id_array = [0u8; 32]; + std::ptr::copy_nonoverlapping(wallet_id, wallet_id_array.as_mut_ptr(), 32); + + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + return FFIManagedAccountResult::error( + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + } + }; + + // Get the managed wallet info from the manager + let mut error = FFIError::success(); + let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( + manager, wallet_id, &mut error, + ); + + if managed_wallet_ptr.is_null() { + return FFIManagedAccountResult::error( + error.code, + if error.message.is_null() { + "Failed to get managed wallet info".to_string() + } else { + let c_str = std::ffi::CStr::from_ptr(error.message); + c_str.to_string_lossy().to_string() + }, + ); + } + + let managed_wallet = &*managed_wallet_ptr; + let account_type_rust = account_type.to_account_type(account_index); + + // Get the managed account from the managed wallet info + let result = match managed_wallet.inner().accounts.get(&network_rust) { + Some(managed_collection) => { + use key_wallet::account::StandardAccountType; + + let managed_account = match account_type_rust { + AccountType::Standard { + index, + standard_account_type, + } => match standard_account_type { + StandardAccountType::BIP44Account => { + managed_collection.standard_bip44_accounts.get(&index) + } + StandardAccountType::BIP32Account => { + managed_collection.standard_bip32_accounts.get(&index) + } + }, + AccountType::CoinJoin { + index, + } => managed_collection.coinjoin_accounts.get(&index), + AccountType::IdentityRegistration => { + managed_collection.identity_registration.as_ref() + } + AccountType::IdentityTopUp { + registration_index, + } => managed_collection.identity_topup.get(®istration_index), + AccountType::IdentityTopUpNotBoundToIdentity => { + managed_collection.identity_topup_not_bound.as_ref() + } + AccountType::IdentityInvitation => managed_collection.identity_invitation.as_ref(), + AccountType::ProviderVotingKeys => managed_collection.provider_voting_keys.as_ref(), + AccountType::ProviderOwnerKeys => managed_collection.provider_owner_keys.as_ref(), + AccountType::ProviderOperatorKeys => { + managed_collection.provider_operator_keys.as_ref() + } + AccountType::ProviderPlatformKeys => { + managed_collection.provider_platform_keys.as_ref() + } + }; + + match managed_account { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + FFIManagedAccountResult::success(Box::into_raw(Box::new(ffi_account))) + } + None => FFIManagedAccountResult::error( + FFIErrorCode::NotFound, + "Account not found".to_string(), + ), + } + } + None => FFIManagedAccountResult::error( + FFIErrorCode::NotFound, + format!("No accounts found for network {:?}", network_rust), + ), + }; + + // Clean up the managed wallet pointer + crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); + + result +} + +/// Get a managed IdentityTopUp account with a specific registration index +/// +/// This is used for top-up accounts that are bound to a specific identity. +/// Returns a managed account handle that wraps the ManagedAccount. +/// +/// # Safety +/// +/// - `manager` must be a valid pointer to an FFIWalletManager instance +/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID +/// - `network` must specify exactly one network +/// - The caller must ensure all pointers remain valid for the duration of this call +/// - The returned account must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_wallet_get_top_up_account_with_registration_index( + manager: *const FFIWalletManager, + wallet_id: *const u8, + network: FFINetwork, + registration_index: c_uint, +) -> FFIManagedAccountResult { + if manager.is_null() { + return FFIManagedAccountResult::error( + FFIErrorCode::InvalidInput, + "Manager is null".to_string(), + ); + } + + if wallet_id.is_null() { + return FFIManagedAccountResult::error( + FFIErrorCode::InvalidInput, + "Wallet ID is null".to_string(), + ); + } + + // Convert wallet_id to array + let mut wallet_id_array = [0u8; 32]; + std::ptr::copy_nonoverlapping(wallet_id, wallet_id_array.as_mut_ptr(), 32); + + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + return FFIManagedAccountResult::error( + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + } + }; + + // Get the managed wallet info from the manager + let mut error = FFIError::success(); + let managed_wallet_ptr = crate::wallet_manager::wallet_manager_get_managed_wallet_info( + manager, wallet_id, &mut error, + ); + + if managed_wallet_ptr.is_null() { + return FFIManagedAccountResult::error( + error.code, + if error.message.is_null() { + "Failed to get managed wallet info".to_string() + } else { + let c_str = std::ffi::CStr::from_ptr(error.message); + c_str.to_string_lossy().to_string() + }, + ); + } + + let managed_wallet = &*managed_wallet_ptr; + + // Get the IdentityTopUp account from the managed collection + let result = match managed_wallet.inner().accounts.get(&network_rust) { + Some(managed_collection) => { + match managed_collection.identity_topup.get(®istration_index) { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + FFIManagedAccountResult::success(Box::into_raw(Box::new(ffi_account))) + } + None => FFIManagedAccountResult::error( + FFIErrorCode::NotFound, + format!( + "IdentityTopUp account for registration index {} not found", + registration_index + ), + ), + } + } + None => FFIManagedAccountResult::error( + FFIErrorCode::NotFound, + format!("No accounts found for network {:?}", network_rust), + ), + }; + + // Clean up the managed wallet pointer + crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); + + result +} + +/// Get the network of a managed account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_network( + account: *const FFIManagedAccount, +) -> FFINetwork { + if account.is_null() { + return FFINetwork::NoNetworks; + } + + let account = &*account; + account.inner().network.into() +} + +/// Get the parent wallet ID of a managed account +/// +/// Note: ManagedAccount doesn't store the parent wallet ID directly. +/// The wallet ID is typically known from the context (e.g., when getting the account from a managed wallet). +/// +/// # Safety +/// +/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID buffer that was provided by the caller +/// - The returned pointer is the same as the input pointer for convenience +/// - The caller must not free the returned pointer as it's the same as the input +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_parent_wallet_id(wallet_id: *const u8) -> *const u8 { + // Simply return the wallet_id that was passed in + // This function exists for API consistency but ManagedAccount doesn't store parent wallet ID + wallet_id +} + +/// Get the account type of a managed account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +/// - `index_out` must be a valid pointer to receive the account index (or null) +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_account_type( + account: *const FFIManagedAccount, + index_out: *mut c_uint, +) -> FFIAccountType { + if account.is_null() { + return FFIAccountType::StandardBIP44; // Default type + } + + let account = &*account; + let managed_account = account.inner(); + let account_type_rust = managed_account.account_type.to_account_type(); + + // Set the index if output pointer is provided + if !index_out.is_null() { + *index_out = account_type_rust.index().unwrap_or(0); + } + + // Convert to FFI account type + match account_type_rust { + AccountType::Standard { + standard_account_type, + .. + } => { + use key_wallet::account::StandardAccountType; + match standard_account_type { + StandardAccountType::BIP44Account => FFIAccountType::StandardBIP44, + StandardAccountType::BIP32Account => FFIAccountType::StandardBIP32, + } + } + AccountType::CoinJoin { + .. + } => FFIAccountType::CoinJoin, + AccountType::IdentityRegistration => FFIAccountType::IdentityRegistration, + AccountType::IdentityTopUp { + .. + } => FFIAccountType::IdentityTopUp, + AccountType::IdentityTopUpNotBoundToIdentity => { + FFIAccountType::IdentityTopUpNotBoundToIdentity + } + AccountType::IdentityInvitation => FFIAccountType::IdentityInvitation, + AccountType::ProviderVotingKeys => FFIAccountType::ProviderVotingKeys, + AccountType::ProviderOwnerKeys => FFIAccountType::ProviderOwnerKeys, + AccountType::ProviderOperatorKeys => FFIAccountType::ProviderOperatorKeys, + AccountType::ProviderPlatformKeys => FFIAccountType::ProviderPlatformKeys, + } +} + +/// Check if a managed account is watch-only +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_is_watch_only( + account: *const FFIManagedAccount, +) -> bool { + if account.is_null() { + return false; + } + + let account = &*account; + account.inner().is_watch_only +} + +/// Get the balance of a managed account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +/// - `balance_out` must be a valid pointer to an FFIBalance structure +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_balance( + account: *const FFIManagedAccount, + balance_out: *mut crate::types::FFIBalance, +) -> bool { + if account.is_null() || balance_out.is_null() { + return false; + } + + let account = &*account; + let balance = &account.inner().balance; + + *balance_out = crate::types::FFIBalance { + confirmed: balance.confirmed, + unconfirmed: balance.unconfirmed, + immature: 0, // WalletBalance doesn't have immature field + total: balance.total, + }; + + true +} + +/// Get the number of transactions in a managed account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_transaction_count( + account: *const FFIManagedAccount, +) -> c_uint { + if account.is_null() { + return 0; + } + + let account = &*account; + account.inner().transactions.len() as c_uint +} + +/// Get the number of UTXOs in a managed account +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_utxo_count( + account: *const FFIManagedAccount, +) -> c_uint { + if account.is_null() { + return 0; + } + + let account = &*account; + account.inner().utxos.len() as c_uint +} + +/// Free a managed account handle +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount that was allocated by this library +/// - The pointer must not be used after calling this function +/// - This function must only be called once per allocation +#[no_mangle] +pub unsafe extern "C" fn managed_account_free(account: *mut FFIManagedAccount) { + if !account.is_null() { + let _ = Box::from_raw(account); + } +} + +/// Free a managed account result's error message (if any) +/// Note: This does NOT free the account handle itself - use managed_account_free for that +/// +/// # Safety +/// +/// - `result` must be a valid pointer to an FFIManagedAccountResult +/// - The error_message field must be either null or a valid CString allocated by this library +/// - The caller must ensure the result pointer remains valid for the duration of this call +#[no_mangle] +pub unsafe extern "C" fn managed_account_result_free_error(result: *mut FFIManagedAccountResult) { + if !result.is_null() { + let result = &mut *result; + if !result.error_message.is_null() { + let _ = std::ffi::CString::from_raw(result.error_message); + result.error_message = std::ptr::null_mut(); + } + } +} + +/// Get number of accounts in a managed wallet +/// +/// # Safety +/// +/// - `manager` must be a valid pointer to an FFIWalletManager instance +/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID +/// - `network` must specify exactly one network +/// - `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 +#[no_mangle] +pub unsafe extern "C" fn managed_wallet_get_account_count( + manager: *const FFIWalletManager, + wallet_id: *const u8, + network: FFINetwork, + error: *mut FFIError, +) -> c_uint { + if manager.is_null() || wallet_id.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return 0; + } + + 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; + } + }; + + // Get the wallet from the manager + let wallet_ptr = crate::wallet_manager::wallet_manager_get_wallet(manager, wallet_id, error); + + if wallet_ptr.is_null() { + // Error already set by wallet_manager_get_wallet + return 0; + } + + let wallet = &*wallet_ptr; + let count = match wallet.inner().accounts.get(&network_rust) { + Some(accounts) => { + FFIError::set_success(error); + 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(); + count as c_uint + } + None => { + FFIError::set_success(error); + 0 + } + }; + + // Clean up the wallet pointer + crate::wallet::wallet_free_const(wallet_ptr); + + count +} + +// Note: BLS and EdDSA accounts are handled through regular FFIManagedAccount +// since ManagedAccountCollection stores all accounts as ManagedAccount type + +/// Get the account index from a managed account +/// +/// Returns the primary account index for Standard and CoinJoin accounts. +/// Returns 0 for account types that don't have an index (like Identity or Provider accounts). +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_index(account: *const FFIManagedAccount) -> c_uint { + if account.is_null() { + return 0; + } + + let account = &*account; + account.inner().account_type.index_or_default() +} + +/// Get the external address pool from a managed account +/// +/// This function returns the external (receive) address pool for Standard accounts. +/// Returns NULL for account types that don't have separate external/internal pools. +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +/// - The returned pool must be freed with `address_pool_free` when no longer needed +#[no_mangle] +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)) + } + _ => std::ptr::null_mut(), + } +} + +/// Get the internal address pool from a managed account +/// +/// This function returns the internal (change) address pool for Standard accounts. +/// Returns NULL for account types that don't have separate external/internal pools. +/// +/// # Safety +/// +/// - `account` must be a valid pointer to an FFIManagedAccount instance +/// - The returned pool must be freed with `address_pool_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_internal_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 internal address pool if this is a standard account + match &managed_account.account_type { + key_wallet::managed_account::managed_account_type::ManagedAccountType::Standard { + internal_addresses, + .. + } => { + let ffi_pool = FFIAddressPool { + pool: internal_addresses as *const AddressPool as *mut AddressPool, + pool_type: FFIAddressPoolType::Internal, + }; + Box::into_raw(Box::new(ffi_pool)) + } + _ => std::ptr::null_mut(), + } +} + +/// Get an address pool from a managed account by type +/// +/// This function returns the appropriate address pool based on the pool type parameter. +/// For Standard accounts with External/Internal pool types, returns the corresponding pool. +/// For non-standard accounts with Single pool type, returns their single address pool. +/// +/// # Safety +/// +/// - `manager` must be a valid pointer to an FFIWalletManager instance +/// - `account` must be a valid pointer to an FFIManagedAccount instance +/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID +/// - The returned pool must be freed with `address_pool_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_get_address_pool( + account: *const FFIManagedAccount, + pool_type: FFIAddressPoolType, +) -> *mut FFIAddressPool { + if account.is_null() { + return std::ptr::null_mut(); + } + + let account = &*account; + let managed_account = account.inner(); + + use key_wallet::managed_account::managed_account_type::ManagedAccountType; + + match pool_type { + FFIAddressPoolType::External => { + // Only standard accounts have external pools + match &managed_account.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)) + } + _ => std::ptr::null_mut(), + } + } + FFIAddressPoolType::Internal => { + // Only standard accounts have internal pools + match &managed_account.account_type { + ManagedAccountType::Standard { + internal_addresses, + .. + } => { + let ffi_pool = FFIAddressPool { + pool: internal_addresses as *const AddressPool as *mut AddressPool, + pool_type: FFIAddressPoolType::Internal, + }; + Box::into_raw(Box::new(ffi_pool)) + } + _ => std::ptr::null_mut(), + } + } + FFIAddressPoolType::Single => { + // Get the single address pool for non-standard accounts + let pool_ref = match &managed_account.account_type { + ManagedAccountType::Standard { + .. + } => { + // Standard accounts don't have a "single" pool + return std::ptr::null_mut(); + } + ManagedAccountType::CoinJoin { + addresses, + .. + } => addresses, + ManagedAccountType::IdentityRegistration { + addresses, + } => addresses, + ManagedAccountType::IdentityTopUp { + addresses, + .. + } => addresses, + ManagedAccountType::IdentityTopUpNotBoundToIdentity { + addresses, + } => addresses, + ManagedAccountType::IdentityInvitation { + addresses, + } => addresses, + ManagedAccountType::ProviderVotingKeys { + addresses, + } => addresses, + ManagedAccountType::ProviderOwnerKeys { + addresses, + } => addresses, + ManagedAccountType::ProviderOperatorKeys { + addresses, + } => addresses, + ManagedAccountType::ProviderPlatformKeys { + addresses, + } => addresses, + }; + + let ffi_pool = FFIAddressPool { + pool: pool_ref as *const AddressPool as *mut AddressPool, + pool_type: FFIAddressPoolType::Single, + }; + Box::into_raw(Box::new(ffi_pool)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::address_pool::address_pool_free; + use crate::types::{FFIAccountCreationOptionType, FFIWalletAccountCreationOptions}; + use crate::wallet_manager::{ + wallet_manager_add_wallet_from_mnemonic_with_options, wallet_manager_create, + wallet_manager_free, wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, + }; + use std::ffi::CString; + use std::ptr; + + const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + #[test] + fn test_managed_account_basic() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Add a wallet with default accounts + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), + &mut error, + ); + assert!(success); + assert_eq!(error.code, FFIErrorCode::Success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + assert_eq!(count_out, 1); + assert!(!wallet_ids_out.is_null()); + + // Get a managed account + let result = managed_wallet_get_account( + manager, + wallet_ids_out, + FFINetwork::Testnet, + 0, + FFIAccountType::StandardBIP44, + ); + + assert!(!result.account.is_null()); + assert_eq!(result.error_code, 0); + assert!(result.error_message.is_null()); + + // Verify the account was created successfully + let account = &*result.account; + // Account should exist and be valid + assert!(!account.inner().is_watch_only); + + // Clean up + managed_account_free(result.account); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } + } + + #[test] + fn test_managed_account_not_found() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Add a wallet with minimal accounts + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut options = FFIWalletAccountCreationOptions::default_options(); + options.option_type = FFIAccountCreationOptionType::BIP44AccountsOnly; + let bip44_indices = vec![0]; + options.bip44_indices = bip44_indices.as_ptr(); + options.bip44_count = bip44_indices.len(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + &options, + &mut error, + ); + assert!(success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + assert_eq!(count_out, 1); + + // Try to get a non-existent CoinJoin account + let mut result = managed_wallet_get_account( + manager, + wallet_ids_out, + FFINetwork::Testnet, + 0, + FFIAccountType::CoinJoin, + ); + + assert!(result.account.is_null()); + assert_ne!(result.error_code, 0); + assert!(!result.error_message.is_null()); + + // Clean up error message + managed_account_result_free_error(&mut result as *mut _); + + // Clean up + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } + } + + #[test] + fn test_managed_account_free_null() { + unsafe { + // Should not crash when freeing null + managed_account_free(ptr::null_mut()); + } + } + + #[test] + fn test_managed_wallet_get_account_count() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Add a wallet with multiple accounts + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut options = FFIWalletAccountCreationOptions::default_options(); + options.option_type = FFIAccountCreationOptionType::AllAccounts; + + let bip44_indices = vec![0, 1, 2]; + let bip32_indices = vec![0]; + let coinjoin_indices = vec![0]; + + options.bip44_indices = bip44_indices.as_ptr(); + options.bip44_count = bip44_indices.len(); + options.bip32_indices = bip32_indices.as_ptr(); + options.bip32_count = bip32_indices.len(); + options.coinjoin_indices = coinjoin_indices.as_ptr(); + options.coinjoin_count = coinjoin_indices.len(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + &options, + &mut error, + ); + assert!(success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + + // Get account count + let count = managed_wallet_get_account_count( + manager, + wallet_ids_out, + FFINetwork::Testnet, + &mut error, + ); + + // Should have at least the accounts we created + assert!(count >= 5); // 3 BIP44 + 1 BIP32 + 1 CoinJoin + assert_eq!(error.code, FFIErrorCode::Success); + + // Clean up + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } + } + + #[test] + fn test_managed_account_getters() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Add a wallet with default accounts + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), + &mut error, + ); + assert!(success); + assert_eq!(error.code, FFIErrorCode::Success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + assert_eq!(count_out, 1); + assert!(!wallet_ids_out.is_null()); + + // Get a managed account + let result = managed_wallet_get_account( + manager, + wallet_ids_out, + FFINetwork::Testnet, + 0, + FFIAccountType::StandardBIP44, + ); + + assert!(!result.account.is_null()); + assert_eq!(result.error_code, 0); + assert!(result.error_message.is_null()); + + let account = result.account; + + // Test get_network + let network = managed_account_get_network(account); + assert_eq!(network, FFINetwork::Testnet); + + // Test get_account_type + let mut index_out: c_uint = 999; // Initialize with unexpected value + let account_type = managed_account_get_account_type(account, &mut index_out); + assert_eq!(account_type, FFIAccountType::StandardBIP44); + assert_eq!(index_out, 0); + + // Test get_is_watch_only + let is_watch_only = managed_account_get_is_watch_only(account); + assert_eq!(is_watch_only, false); + + // Test get_balance + let mut balance_out = crate::types::FFIBalance { + confirmed: 999, + unconfirmed: 999, + immature: 999, + total: 999, + }; + let success = managed_account_get_balance(account, &mut balance_out); + assert!(success); + // Initially, balance should be 0 + assert_eq!(balance_out.confirmed, 0); + assert_eq!(balance_out.unconfirmed, 0); + assert_eq!(balance_out.immature, 0); + assert_eq!(balance_out.total, 0); + + // Test get_transaction_count + let tx_count = managed_account_get_transaction_count(account); + assert_eq!(tx_count, 0); // Initially no transactions + + // Test get_utxo_count + let utxo_count = managed_account_get_utxo_count(account); + assert_eq!(utxo_count, 0); // Initially no UTXOs + + // Test get_parent_wallet_id + let parent_id = managed_account_get_parent_wallet_id(wallet_ids_out); + assert_eq!(parent_id, wallet_ids_out); // Should return the same pointer + + // Clean up + managed_account_free(account); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } + } + + #[test] + fn test_managed_account_getter_edge_cases() { + unsafe { + // Test null account + let network = managed_account_get_network(ptr::null()); + assert_eq!(network, FFINetwork::NoNetworks); + + let mut index_out: c_uint = 0; + let account_type = managed_account_get_account_type(ptr::null(), &mut index_out); + assert_eq!(account_type, FFIAccountType::StandardBIP44); // Default type + + let is_watch_only = managed_account_get_is_watch_only(ptr::null()); + assert_eq!(is_watch_only, false); + + let tx_count = managed_account_get_transaction_count(ptr::null()); + assert_eq!(tx_count, 0); + + let utxo_count = managed_account_get_utxo_count(ptr::null()); + assert_eq!(utxo_count, 0); + + // Test new getters with null account + let index = managed_account_get_index(ptr::null()); + assert_eq!(index, 0); + + // Test null balance_out + let mut error = FFIError::success(); + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Add a wallet + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), + &mut error, + ); + assert!(success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + + // Get an account + let result = managed_wallet_get_account( + manager, + wallet_ids_out, + FFINetwork::Testnet, + 0, + FFIAccountType::StandardBIP44, + ); + assert!(!result.account.is_null()); + + // Test balance with null output + let success = managed_account_get_balance(result.account, ptr::null_mut()); + assert!(!success); + + // Clean up + managed_account_free(result.account); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } + } + + #[test] + fn test_managed_account_address_pools() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let mut manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Add a wallet with default accounts + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), + &mut error, + ); + assert!(success); + assert_eq!(error.code, FFIErrorCode::Success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + assert_eq!(count_out, 1); + assert!(!wallet_ids_out.is_null()); + + // Get a standard BIP44 managed account + let result = managed_wallet_get_account( + manager, + wallet_ids_out, + FFINetwork::Testnet, + 0, + FFIAccountType::StandardBIP44, + ); + + assert!(!result.account.is_null()); + assert_eq!(result.error_code, 0); + + let account = result.account; + + // Test get_index + let index = managed_account_get_index(account); + assert_eq!(index, 0); + + // Test get_external_address_pool + let external_pool = managed_account_get_external_address_pool(account); + assert!(!external_pool.is_null()); + + // Test get_internal_address_pool + let internal_pool = managed_account_get_internal_address_pool(account); + assert!(!internal_pool.is_null()); + + // Test get_address_pool with External type + let external_pool2 = + managed_account_get_address_pool(account, FFIAddressPoolType::External); + assert!(!external_pool2.is_null()); + + // Test get_address_pool with Internal type + let internal_pool2 = + managed_account_get_address_pool(account, FFIAddressPoolType::Internal); + assert!(!internal_pool2.is_null()); + + // Test get_address_pool with Single type (should return null for Standard account) + let single_pool = managed_account_get_address_pool(account, FFIAddressPoolType::Single); + assert!(single_pool.is_null()); + + // Clean up address pools + address_pool_free(external_pool); + address_pool_free(internal_pool); + address_pool_free(external_pool2); + address_pool_free(internal_pool2); + + // Clean up account + managed_account_free(account); + + // Now test with different account types from the same wallet + // The default wallet should have been created with StandardBIP44 index 0 + // Let's try creating a wallet with CoinJoin accounts first + + // Clean up and start fresh for the second test + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + + // Create a new manager + manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Create wallet with CoinJoin account + let mut options = FFIWalletAccountCreationOptions::default_options(); + options.option_type = FFIAccountCreationOptionType::SpecificAccounts; + let coinjoin_indices = vec![0]; + options.coinjoin_indices = coinjoin_indices.as_ptr(); + options.coinjoin_count = coinjoin_indices.len(); + + let mnemonic2 = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase2 = CString::new("").unwrap(); + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic2.as_ptr(), + passphrase2.as_ptr(), + FFINetwork::Testnet, + &options, + &mut error, + ); + assert!(success); + + // Get wallet IDs + let success = wallet_manager_get_wallet_ids( + manager, + &mut wallet_ids_out, + &mut count_out, + &mut error, + ); + assert!(success); + assert_eq!(count_out, 1); + + // Get CoinJoin account + let cj_result = managed_wallet_get_account( + manager, + wallet_ids_out, + FFINetwork::Testnet, + 0, + FFIAccountType::CoinJoin, + ); + assert!(!cj_result.account.is_null()); + + let cj_account = cj_result.account; + + // Test that external/internal return null for CoinJoin account + let cj_external = managed_account_get_external_address_pool(cj_account); + assert!(cj_external.is_null()); + + let cj_internal = managed_account_get_internal_address_pool(cj_account); + assert!(cj_internal.is_null()); + + // Test that Single pool works for CoinJoin account + let cj_single = + managed_account_get_address_pool(cj_account, FFIAddressPoolType::Single); + assert!(!cj_single.is_null()); + + // Clean up + address_pool_free(cj_single); + managed_account_free(cj_account); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } + } + + #[test] + fn test_address_pool_free_null() { + unsafe { + // Should not crash when freeing null + address_pool_free(ptr::null_mut()); + } + } +} diff --git a/key-wallet-ffi/src/managed_account_collection.rs b/key-wallet-ffi/src/managed_account_collection.rs new file mode 100644 index 000000000..07f92dfa3 --- /dev/null +++ b/key-wallet-ffi/src/managed_account_collection.rs @@ -0,0 +1,1102 @@ +//! FFI bindings for managed account collections +//! +//! This module provides FFI-compatible account collection functionality that works +//! with managed wallets through the wallet manager. It mirrors the functionality +//! of account_collection.rs but accesses accounts through the wallet manager's +//! wallet reference. + +use std::ffi::CString; +use std::os::raw::{c_char, c_uint}; +use std::ptr; + +use crate::error::{FFIError, FFIErrorCode}; +use crate::managed_account::FFIManagedAccount; +use crate::types::FFINetwork; +use crate::wallet_manager::FFIWalletManager; + +/// Opaque handle to a managed account collection +pub struct FFIManagedAccountCollection { + /// The underlying managed account collection + collection: key_wallet::managed_account::managed_account_collection::ManagedAccountCollection, +} + +impl FFIManagedAccountCollection { + /// Create a new FFI managed account collection + pub fn new( + collection: &key_wallet::managed_account::managed_account_collection::ManagedAccountCollection, + ) -> Self { + FFIManagedAccountCollection { + collection: collection.clone(), + } + } +} + +/// C-compatible summary of all accounts in a managed collection +/// +/// This struct provides Swift with structured data about all accounts +/// that exist in the managed collection, allowing programmatic access to account +/// indices and presence information. +#[repr(C)] +pub struct FFIManagedAccountCollectionSummary { + /// Array of BIP44 account indices + pub bip44_indices: *mut c_uint, + /// Number of BIP44 accounts + pub bip44_count: usize, + + /// Array of BIP32 account indices + pub bip32_indices: *mut c_uint, + /// Number of BIP32 accounts + pub bip32_count: usize, + + /// Array of CoinJoin account indices + pub coinjoin_indices: *mut c_uint, + /// Number of CoinJoin accounts + pub coinjoin_count: usize, + + /// Array of identity top-up registration indices + pub identity_topup_indices: *mut c_uint, + /// Number of identity top-up accounts + pub identity_topup_count: usize, + + /// Whether identity registration account exists + pub has_identity_registration: bool, + /// Whether identity invitation account exists + pub has_identity_invitation: bool, + /// Whether identity top-up not bound account exists + pub has_identity_topup_not_bound: bool, + /// Whether provider voting keys account exists + pub has_provider_voting_keys: bool, + /// Whether provider owner keys account exists + pub has_provider_owner_keys: bool, + + #[cfg(feature = "bls")] + /// Whether provider operator keys account exists + pub has_provider_operator_keys: bool, + + #[cfg(feature = "eddsa")] + /// Whether provider platform keys account exists + pub has_provider_platform_keys: bool, +} + +/// Get managed account collection for a specific network from wallet manager +/// +/// # Safety +/// +/// - `manager` must be a valid pointer to an FFIWalletManager instance +/// - `wallet_id` must be a valid pointer to a 32-byte wallet ID +/// - `error` must be a valid pointer to an FFIError structure or null +/// - The returned pointer must be freed with `managed_account_collection_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_wallet_get_account_collection( + manager: *const FFIWalletManager, + wallet_id: *const u8, + network: FFINetwork, + error: *mut FFIError, +) -> *mut FFIManagedAccountCollection { + if manager.is_null() || wallet_id.is_null() { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return ptr::null_mut(); + } + + 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 ptr::null_mut(); + } + }; + + // Get the managed wallet info from the manager + let managed_wallet_ptr = + crate::wallet_manager::wallet_manager_get_managed_wallet_info(manager, wallet_id, error); + + if managed_wallet_ptr.is_null() { + // Error already set by wallet_manager_get_managed_wallet_info + return ptr::null_mut(); + } + + // Get the managed account collection from the managed wallet info + let managed_wallet = &*managed_wallet_ptr; + match managed_wallet.inner().accounts.get(&network_rust) { + Some(collection) => { + let ffi_collection = FFIManagedAccountCollection::new(collection); + + // Clean up the managed wallet pointer since we've extracted what we need + crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); + + Box::into_raw(Box::new(ffi_collection)) + } + None => { + // Clean up the managed wallet pointer + crate::managed_wallet::managed_wallet_info_free(managed_wallet_ptr); + + FFIError::set_error( + error, + FFIErrorCode::NotFound, + format!("No accounts found for network {:?}", network_rust), + ); + ptr::null_mut() + } + } +} + +/// Free a managed account collection handle +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection created by this library +/// - `collection` must not be used after calling this function +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_free( + collection: *mut FFIManagedAccountCollection, +) { + if !collection.is_null() { + let _ = Box::from_raw(collection); + } +} + +// Standard BIP44 accounts functions + +/// Get a BIP44 account by index from the managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_bip44_account( + collection: *const FFIManagedAccountCollection, + index: c_uint, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match collection.collection.standard_bip44_accounts.get(&index) { + Some(account) => { + // Get the network from the account + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get all BIP44 account indices from managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - `out_indices` must be a valid pointer to store the indices array +/// - `out_count` must be a valid pointer to store the count +/// - The returned array must be freed with `free_u32_array` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_bip44_indices( + collection: *const FFIManagedAccountCollection, + out_indices: *mut *mut c_uint, + out_count: *mut usize, +) -> bool { + if collection.is_null() || out_indices.is_null() || out_count.is_null() { + return false; + } + + let collection = &*collection; + let mut indices: Vec = + collection.collection.standard_bip44_accounts.keys().copied().collect(); + + if indices.is_empty() { + *out_indices = ptr::null_mut(); + *out_count = 0; + return true; + } + + indices.sort(); + + let mut boxed_slice = indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + let len = boxed_slice.len(); + std::mem::forget(boxed_slice); + + *out_indices = ptr; + *out_count = len; + true +} + +// Standard BIP32 accounts functions + +/// Get a BIP32 account by index from the managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_bip32_account( + collection: *const FFIManagedAccountCollection, + index: c_uint, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match collection.collection.standard_bip32_accounts.get(&index) { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get all BIP32 account indices from managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - `out_indices` must be a valid pointer to store the indices array +/// - `out_count` must be a valid pointer to store the count +/// - The returned array must be freed with `free_u32_array` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_bip32_indices( + collection: *const FFIManagedAccountCollection, + out_indices: *mut *mut c_uint, + out_count: *mut usize, +) -> bool { + if collection.is_null() || out_indices.is_null() || out_count.is_null() { + return false; + } + + let collection = &*collection; + let indices: Vec = + collection.collection.standard_bip32_accounts.keys().copied().collect(); + + if indices.is_empty() { + *out_indices = ptr::null_mut(); + *out_count = 0; + return true; + } + + let mut boxed_slice = indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + let len = boxed_slice.len(); + std::mem::forget(boxed_slice); + + *out_indices = ptr; + *out_count = len; + true +} + +// CoinJoin accounts functions + +/// Get a CoinJoin account by index from the managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_coinjoin_account( + collection: *const FFIManagedAccountCollection, + index: c_uint, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match collection.collection.coinjoin_accounts.get(&index) { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get all CoinJoin account indices from managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - `out_indices` must be a valid pointer to store the indices array +/// - `out_count` must be a valid pointer to store the count +/// - The returned array must be freed with `free_u32_array` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_coinjoin_indices( + collection: *const FFIManagedAccountCollection, + out_indices: *mut *mut c_uint, + out_count: *mut usize, +) -> bool { + if collection.is_null() || out_indices.is_null() || out_count.is_null() { + return false; + } + + let collection = &*collection; + let mut indices: Vec = + collection.collection.coinjoin_accounts.keys().copied().collect(); + + if indices.is_empty() { + *out_indices = ptr::null_mut(); + *out_count = 0; + return true; + } + + indices.sort(); + + let mut boxed_slice = indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + let len = boxed_slice.len(); + std::mem::forget(boxed_slice); + + *out_indices = ptr; + *out_count = len; + true +} + +// Identity accounts functions + +/// Get the identity registration account if it exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_identity_registration( + collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.identity_registration { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if identity registration account exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_has_identity_registration( + collection: *const FFIManagedAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.identity_registration.is_some() +} + +/// Get an identity topup account by registration index from managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_identity_topup( + collection: *const FFIManagedAccountCollection, + registration_index: c_uint, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match collection.collection.identity_topup.get(®istration_index) { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get all identity topup registration indices from managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - `out_indices` must be a valid pointer to store the indices array +/// - `out_count` must be a valid pointer to store the count +/// - The returned array must be freed with `free_u32_array` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_identity_topup_indices( + collection: *const FFIManagedAccountCollection, + out_indices: *mut *mut c_uint, + out_count: *mut usize, +) -> bool { + if collection.is_null() || out_indices.is_null() || out_count.is_null() { + return false; + } + + let collection = &*collection; + let mut indices: Vec = collection.collection.identity_topup.keys().copied().collect(); + + if indices.is_empty() { + *out_indices = ptr::null_mut(); + *out_count = 0; + return true; + } + + indices.sort(); + + let mut boxed_slice = indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + let len = boxed_slice.len(); + std::mem::forget(boxed_slice); + + *out_indices = ptr; + *out_count = len; + true +} + +/// Get the identity topup not bound account if it exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - `manager` must be a valid pointer to an FFIWalletManager +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_identity_topup_not_bound( + collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.identity_topup_not_bound { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if identity topup not bound account exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_has_identity_topup_not_bound( + collection: *const FFIManagedAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.identity_topup_not_bound.is_some() +} + +/// Get the identity invitation account if it exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_identity_invitation( + collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.identity_invitation { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if identity invitation account exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_has_identity_invitation( + collection: *const FFIManagedAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.identity_invitation.is_some() +} + +// Provider accounts functions + +/// Get the provider voting keys account if it exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_provider_voting_keys( + collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.provider_voting_keys { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if provider voting keys account exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_has_provider_voting_keys( + collection: *const FFIManagedAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.provider_voting_keys.is_some() +} + +/// Get the provider owner keys account if it exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_provider_owner_keys( + collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.provider_owner_keys { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Check if provider owner keys account exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_has_provider_owner_keys( + collection: *const FFIManagedAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + let collection = &*collection; + collection.collection.provider_owner_keys.is_some() +} + +/// Get the provider operator keys account if it exists in managed collection +/// Note: This function is only available when the `bls` feature is enabled +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[cfg(feature = "bls")] +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_provider_operator_keys( + collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.provider_operator_keys { + Some(account) => { + let ffi_account = FFIManagedAccount::new(account); + Box::into_raw(Box::new(ffi_account)) + } + None => ptr::null_mut(), + } +} + +/// Get the provider operator keys account if it exists (stub when BLS is disabled) +#[cfg(not(feature = "bls"))] +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_provider_operator_keys( + _collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + // BLS feature not enabled, always return null + ptr::null_mut() +} + +/// Check if provider operator keys account exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_has_provider_operator_keys( + collection: *const FFIManagedAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + #[cfg(feature = "bls")] + { + let collection = &*collection; + collection.collection.provider_operator_keys.is_some() + } + + #[cfg(not(feature = "bls"))] + { + false + } +} + +/// Get the provider platform keys account if it exists in managed collection +/// Note: This function is only available when the `eddsa` feature is enabled +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - `manager` must be a valid pointer to an FFIWalletManager +/// - The returned pointer must be freed with `managed_account_free` when no longer needed +#[cfg(feature = "eddsa")] +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_get_provider_platform_keys( + collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + match &collection.collection.provider_platform_keys { + Some(account) => { + let ffi_account = FFIManagedAccount::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 managed_account_collection_get_provider_platform_keys( + _collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccount { + // EdDSA feature not enabled, always return null + ptr::null_mut() +} + +/// Check if provider platform keys account exists in managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_has_provider_platform_keys( + collection: *const FFIManagedAccountCollection, +) -> bool { + if collection.is_null() { + return false; + } + + #[cfg(feature = "eddsa")] + { + let collection = &*collection; + collection.collection.provider_platform_keys.is_some() + } + + #[cfg(not(feature = "eddsa"))] + { + false + } +} + +// Utility functions + +/// Get the total number of accounts in the managed collection +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_count( + collection: *const FFIManagedAccountCollection, +) -> c_uint { + if collection.is_null() { + return 0; + } + + let collection = &*collection; + let mut count = 0u32; + + count += collection.collection.standard_bip44_accounts.len() as u32; + count += collection.collection.standard_bip32_accounts.len() as u32; + count += collection.collection.coinjoin_accounts.len() as u32; + count += collection.collection.identity_topup.len() as u32; + + if collection.collection.identity_registration.is_some() { + count += 1; + } + if collection.collection.identity_topup_not_bound.is_some() { + count += 1; + } + if collection.collection.identity_invitation.is_some() { + count += 1; + } + if collection.collection.provider_voting_keys.is_some() { + count += 1; + } + if collection.collection.provider_owner_keys.is_some() { + count += 1; + } + + #[cfg(feature = "bls")] + if collection.collection.provider_operator_keys.is_some() { + count += 1; + } + + #[cfg(feature = "eddsa")] + if collection.collection.provider_platform_keys.is_some() { + count += 1; + } + + count +} + +/// Get a human-readable summary of all accounts in the managed collection +/// +/// Returns a formatted string showing all account types and their indices. +/// The format is designed to be clear and readable for end users. +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned string must be freed with `string_free` when no longer needed +/// - Returns null if the collection pointer is null +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_summary( + collection: *const FFIManagedAccountCollection, +) -> *mut c_char { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + let mut summary_parts = Vec::new(); + + summary_parts.push("Managed Account Summary:".to_string()); + + // BIP44 Accounts + if !collection.collection.standard_bip44_accounts.is_empty() { + let mut indices: Vec = + collection.collection.standard_bip44_accounts.keys().copied().collect(); + indices.sort(); + let count = indices.len(); + let indices_str = format!("{:?}", indices); + summary_parts.push(format!( + "• BIP44 Accounts: {} {} at indices {}", + count, + if count == 1 { + "account" + } else { + "accounts" + }, + indices_str + )); + } + + // BIP32 Accounts + if !collection.collection.standard_bip32_accounts.is_empty() { + let mut indices: Vec = + collection.collection.standard_bip32_accounts.keys().copied().collect(); + indices.sort(); + let count = indices.len(); + let indices_str = format!("{:?}", indices); + summary_parts.push(format!( + "• BIP32 Accounts: {} {} at indices {}", + count, + if count == 1 { + "account" + } else { + "accounts" + }, + indices_str + )); + } + + // CoinJoin Accounts + if !collection.collection.coinjoin_accounts.is_empty() { + let mut indices: Vec = + collection.collection.coinjoin_accounts.keys().copied().collect(); + indices.sort(); + let count = indices.len(); + let indices_str = format!("{:?}", indices); + summary_parts.push(format!( + "• CoinJoin Accounts: {} {} at indices {}", + count, + if count == 1 { + "account" + } else { + "accounts" + }, + indices_str + )); + } + + // Identity TopUp Accounts + if !collection.collection.identity_topup.is_empty() { + let mut indices: Vec = collection.collection.identity_topup.keys().copied().collect(); + indices.sort(); + let count = indices.len(); + let indices_str = format!("{:?}", indices); + summary_parts.push(format!( + "• Identity TopUp: {} {} at indices {}", + count, + if count == 1 { + "account" + } else { + "accounts" + }, + indices_str + )); + } + + // Special accounts (single instances) + if collection.collection.identity_registration.is_some() { + summary_parts.push("• Identity Registration Account".to_string()); + } + + if collection.collection.identity_topup_not_bound.is_some() { + summary_parts.push("• Identity TopUp Not Bound Account".to_string()); + } + + if collection.collection.identity_invitation.is_some() { + summary_parts.push("• Identity Invitation Account".to_string()); + } + + if collection.collection.provider_voting_keys.is_some() { + summary_parts.push("• Provider Voting Keys Account".to_string()); + } + + if collection.collection.provider_owner_keys.is_some() { + summary_parts.push("• Provider Owner Keys Account".to_string()); + } + + #[cfg(feature = "bls")] + if collection.collection.provider_operator_keys.is_some() { + summary_parts.push("• Provider Operator Keys Account (BLS)".to_string()); + } + + #[cfg(feature = "eddsa")] + if collection.collection.provider_platform_keys.is_some() { + summary_parts.push("• Provider Platform Keys Account (EdDSA)".to_string()); + } + + // If there are no accounts at all + if summary_parts.len() == 1 { + summary_parts.push("No accounts configured".to_string()); + } + + let summary = summary_parts.join("\n"); + + match CString::new(summary) { + Ok(c_str) => c_str.into_raw(), + Err(_) => ptr::null_mut(), + } +} + +/// Get structured account collection summary data for managed collection +/// +/// Returns a struct containing arrays of indices for each account type and boolean +/// flags for special accounts. This provides Swift with programmatic access to +/// account information. +/// +/// # Safety +/// +/// - `collection` must be a valid pointer to an FFIManagedAccountCollection +/// - The returned pointer must be freed with `managed_account_collection_summary_free` when no longer needed +/// - Returns null if the collection pointer is null +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_summary_data( + collection: *const FFIManagedAccountCollection, +) -> *mut FFIManagedAccountCollectionSummary { + if collection.is_null() { + return ptr::null_mut(); + } + + let collection = &*collection; + + // Collect BIP44 indices + let mut bip44_indices: Vec = + collection.collection.standard_bip44_accounts.keys().copied().collect(); + bip44_indices.sort(); + let (bip44_ptr, bip44_count) = if bip44_indices.is_empty() { + (ptr::null_mut(), 0) + } else { + let count = bip44_indices.len(); + let mut boxed_slice = bip44_indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + std::mem::forget(boxed_slice); + (ptr, count) + }; + + // Collect BIP32 indices + let mut bip32_indices: Vec = + collection.collection.standard_bip32_accounts.keys().copied().collect(); + bip32_indices.sort(); + let (bip32_ptr, bip32_count) = if bip32_indices.is_empty() { + (ptr::null_mut(), 0) + } else { + let count = bip32_indices.len(); + let mut boxed_slice = bip32_indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + std::mem::forget(boxed_slice); + (ptr, count) + }; + + // Collect CoinJoin indices + let mut coinjoin_indices: Vec = + collection.collection.coinjoin_accounts.keys().copied().collect(); + coinjoin_indices.sort(); + let (coinjoin_ptr, coinjoin_count) = if coinjoin_indices.is_empty() { + (ptr::null_mut(), 0) + } else { + let count = coinjoin_indices.len(); + let mut boxed_slice = coinjoin_indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + std::mem::forget(boxed_slice); + (ptr, count) + }; + + // Collect identity topup indices + let mut topup_indices: Vec = + collection.collection.identity_topup.keys().copied().collect(); + topup_indices.sort(); + let (topup_ptr, topup_count) = if topup_indices.is_empty() { + (ptr::null_mut(), 0) + } else { + let count = topup_indices.len(); + let mut boxed_slice = topup_indices.into_boxed_slice(); + let ptr = boxed_slice.as_mut_ptr(); + std::mem::forget(boxed_slice); + (ptr, count) + }; + + // Create the summary struct + let summary = FFIManagedAccountCollectionSummary { + bip44_indices: bip44_ptr, + bip44_count, + bip32_indices: bip32_ptr, + bip32_count, + coinjoin_indices: coinjoin_ptr, + coinjoin_count, + identity_topup_indices: topup_ptr, + identity_topup_count: topup_count, + has_identity_registration: collection.collection.identity_registration.is_some(), + has_identity_invitation: collection.collection.identity_invitation.is_some(), + has_identity_topup_not_bound: collection.collection.identity_topup_not_bound.is_some(), + has_provider_voting_keys: collection.collection.provider_voting_keys.is_some(), + has_provider_owner_keys: collection.collection.provider_owner_keys.is_some(), + #[cfg(feature = "bls")] + has_provider_operator_keys: collection.collection.provider_operator_keys.is_some(), + #[cfg(feature = "eddsa")] + has_provider_platform_keys: collection.collection.provider_platform_keys.is_some(), + }; + + Box::into_raw(Box::new(summary)) +} + +/// Free a managed account collection summary and all its allocated memory +/// +/// # Safety +/// +/// - `summary` must be a valid pointer to an FFIManagedAccountCollectionSummary created by `managed_account_collection_summary_data` +/// - `summary` must not be used after calling this function +#[no_mangle] +pub unsafe extern "C" fn managed_account_collection_summary_free( + summary: *mut FFIManagedAccountCollectionSummary, +) { + if !summary.is_null() { + let summary = Box::from_raw(summary); + + // Free all the allocated arrays + if !summary.bip44_indices.is_null() && summary.bip44_count > 0 { + let _ = Vec::from_raw_parts( + summary.bip44_indices, + summary.bip44_count, + summary.bip44_count, + ); + } + + if !summary.bip32_indices.is_null() && summary.bip32_count > 0 { + let _ = Vec::from_raw_parts( + summary.bip32_indices, + summary.bip32_count, + summary.bip32_count, + ); + } + + if !summary.coinjoin_indices.is_null() && summary.coinjoin_count > 0 { + let _ = Vec::from_raw_parts( + summary.coinjoin_indices, + summary.coinjoin_count, + summary.coinjoin_count, + ); + } + + if !summary.identity_topup_indices.is_null() && summary.identity_topup_count > 0 { + let _ = Vec::from_raw_parts( + summary.identity_topup_indices, + summary.identity_topup_count, + summary.identity_topup_count, + ); + } + + // The summary struct itself is dropped automatically when the Box is dropped + } +} diff --git a/key-wallet-ffi/src/managed_wallet.rs b/key-wallet-ffi/src/managed_wallet.rs index af382cb37..9cf1475cf 100644 --- a/key-wallet-ffi/src/managed_wallet.rs +++ b/key-wallet-ffi/src/managed_wallet.rs @@ -70,7 +70,17 @@ pub unsafe extern "C" fn managed_wallet_get_next_bip44_receive_address( let managed_wallet = unsafe { &mut *managed_wallet }; let wallet = unsafe { &*wallet }; - let network = network.into(); + let network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return ptr::null_mut(); + } + }; // Get the account collection for the network let account_collection = match managed_wallet.inner.accounts.get_mut(&network) { @@ -190,7 +200,17 @@ pub unsafe extern "C" fn managed_wallet_get_next_bip44_change_address( let managed_wallet = unsafe { &mut *managed_wallet }; let wallet = unsafe { &*wallet }; - let network = network.into(); + let network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return ptr::null_mut(); + } + }; // Get the account collection for the network let account_collection = match managed_wallet.inner.accounts.get_mut(&network) { @@ -329,7 +349,19 @@ pub unsafe extern "C" fn managed_wallet_get_bip_44_external_address_range( let managed_wallet = unsafe { &mut *managed_wallet }; let wallet = unsafe { &*wallet }; - let network = network.into(); + let network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + *count_out = 0; + *addresses_out = ptr::null_mut(); + return false; + } + }; // Get the account collection for the network let account_collection = match managed_wallet.inner.accounts.get_mut(&network) { @@ -511,7 +543,19 @@ pub unsafe extern "C" fn managed_wallet_get_bip_44_internal_address_range( let managed_wallet = unsafe { &mut *managed_wallet }; let wallet = unsafe { &*wallet }; - let network = network.into(); + let network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + *count_out = 0; + *addresses_out = ptr::null_mut(); + return false; + } + }; // Get the account collection for the network let account_collection = match managed_wallet.inner.accounts.get_mut(&network) { diff --git a/key-wallet-ffi/src/provider_keys.rs b/key-wallet-ffi/src/provider_keys.rs index 81e653582..cae961a6e 100644 --- a/key-wallet-ffi/src/provider_keys.rs +++ b/key-wallet-ffi/src/provider_keys.rs @@ -71,7 +71,17 @@ pub unsafe extern "C" fn wallet_generate_provider_key( } let wallet = &*wallet; - let network_rust: key_wallet::Network = network.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 false; + } + }; // Determine the account type based on key type let account_type = match key_type { @@ -238,107 +248,6 @@ pub unsafe extern "C" fn provider_key_info_free(info: *mut FFIProviderKeyInfo) { } } -/// Get the address for a provider key -/// -/// This returns the P2PKH address corresponding to the provider key at -/// the specified index. This is useful for funding provider accounts. -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `error` must be a valid pointer to an FFIError or null -/// - The returned string must be freed by the caller -#[no_mangle] -pub unsafe extern "C" fn wallet_get_provider_key_address( - wallet: *const FFIWallet, - network: FFINetwork, - key_type: FFIProviderKeyType, - _key_index: c_uint, - error: *mut FFIError, -) -> *mut c_char { - if wallet.is_null() { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Wallet is null".to_string()); - return ptr::null_mut(); - } - - let wallet = &*wallet; - let network_rust: key_wallet::Network = network.into(); - - // Determine the account type based on key type - let account_type = match key_type { - FFIProviderKeyType::VotingKeys => AccountType::ProviderVotingKeys, - FFIProviderKeyType::OwnerKeys => AccountType::ProviderOwnerKeys, - FFIProviderKeyType::OperatorKeys => AccountType::ProviderOperatorKeys, - FFIProviderKeyType::PlatformKeys => AccountType::ProviderPlatformKeys, - }; - - // Get the account - let accounts = match wallet.inner().accounts.get(&network_rust) { - Some(accounts) => accounts, - None => { - FFIError::set_error( - error, - FFIErrorCode::NotFound, - "No accounts for network".to_string(), - ); - return ptr::null_mut(); - } - }; - - let account = match &account_type { - AccountType::ProviderVotingKeys => accounts.provider_voting_keys.as_ref(), - AccountType::ProviderOwnerKeys => accounts.provider_owner_keys.as_ref(), - AccountType::ProviderOperatorKeys => None, // BLSAccount not yet supported - AccountType::ProviderPlatformKeys => None, // EdDSAAccount not yet supported - _ => None, - }; - - let _account = match account { - Some(acc) => acc, - None => { - FFIError::set_error( - error, - FFIErrorCode::NotFound, - format!("Provider account type {:?} not found", account_type), - ); - return ptr::null_mut(); - } - }; - - // Get the address at the specified index - // For now, return a placeholder address until proper implementation is available - // TODO: Implement proper address derivation for provider keys - let address_str = "XunknownProviderAddress"; - let result: Result = Ok(address_str.to_string()); - match result { - Ok(address) => { - let address_str = address.to_string(); - match CString::new(address_str) { - Ok(c_str) => { - FFIError::set_success(error); - c_str.into_raw() - } - Err(_) => { - FFIError::set_error( - error, - FFIErrorCode::InternalError, - "Failed to convert address to C string".to_string(), - ); - ptr::null_mut() - } - } - } - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - format!("Failed to get provider key address: {}", e), - ); - ptr::null_mut() - } - } -} - /// Sign data with a provider key /// /// This signs arbitrary data with the provider key at the specified index. @@ -372,7 +281,17 @@ pub unsafe extern "C" fn wallet_sign_with_provider_key( } let wallet = &*wallet; - let network_rust: key_wallet::Network = network.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 false; + } + }; let _data_slice = slice::from_raw_parts(data, data_len); // Determine the account type based on key type diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index 3cd971a30..c8985a913 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -26,7 +26,7 @@ pub struct FFITxOutput { #[no_mangle] pub unsafe extern "C" fn wallet_build_transaction( wallet: *mut FFIWallet, - network: FFINetwork, + _network: FFINetwork, account_index: c_uint, outputs: *const FFITxOutput, outputs_count: usize, @@ -42,7 +42,6 @@ pub unsafe extern "C" fn wallet_build_transaction( unsafe { let _wallet = &mut *wallet; - let _network_rust: key_wallet::Network = network.into(); let _outputs_slice = slice::from_raw_parts(outputs, outputs_count); let _account_index = account_index; let _fee_per_kb = fee_per_kb; @@ -71,7 +70,7 @@ pub unsafe extern "C" fn wallet_build_transaction( #[no_mangle] pub unsafe extern "C" fn wallet_sign_transaction( wallet: *const FFIWallet, - network: FFINetwork, + _network: FFINetwork, tx_bytes: *const u8, tx_len: usize, signed_tx_out: *mut *mut u8, @@ -86,7 +85,6 @@ pub unsafe extern "C" fn wallet_sign_transaction( unsafe { let _wallet = &*wallet; - let _network_rust: key_wallet::Network = network.into(); let _tx_slice = slice::from_raw_parts(tx_bytes, tx_len); // Note: Transaction signing would require implementing wallet signing logic @@ -156,7 +154,17 @@ pub unsafe extern "C" fn wallet_check_transaction( unsafe { let wallet = &mut *wallet; - let network_rust: key_wallet::Network = network.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 false; + } + }; let tx_slice = slice::from_raw_parts(tx_bytes, tx_len); // Parse the transaction diff --git a/key-wallet-ffi/src/transaction_checking.rs b/key-wallet-ffi/src/transaction_checking.rs index 9f7facfe6..9a235bd1e 100644 --- a/key-wallet-ffi/src/transaction_checking.rs +++ b/key-wallet-ffi/src/transaction_checking.rs @@ -142,7 +142,17 @@ pub unsafe extern "C" fn managed_wallet_check_transaction( } let managed_wallet = &mut *(*managed_wallet).inner; - let network_rust: key_wallet::Network = network.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 false; + } + }; let tx_slice = slice::from_raw_parts(tx_bytes, tx_len); // Parse the transaction diff --git a/key-wallet-ffi/src/types.rs b/key-wallet-ffi/src/types.rs index 8465999e3..3f984763f 100644 --- a/key-wallet-ffi/src/types.rs +++ b/key-wallet-ffi/src/types.rs @@ -4,23 +4,73 @@ use key_wallet::{Network, Wallet}; use std::os::raw::c_uint; use std::sync::Arc; -/// FFI Network type +/// FFI Network type (bit flags for multiple networks) #[repr(C)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] 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 } -impl From for Network { - fn from(n: FFINetwork) -> Self { - match n { - FFINetwork::Dash => Network::Dash, - FFINetwork::Testnet => Network::Testnet, - FFINetwork::Regtest => Network::Regtest, - FFINetwork::Devnet => Network::Devnet, +impl FFINetwork { + /// Parse bit flags into a vector of networks + pub fn parse_networks(&self) -> Vec { + // Handle special cases + if self == &FFINetwork::NoNetworks { + return vec![]; + } + + let flags = *self as c_uint; + + let mut networks = Vec::new(); + + if flags & (FFINetwork::Dash as c_uint) != 0 { + networks.push(Network::Dash); + } + if flags & (FFINetwork::Testnet as c_uint) != 0 { + networks.push(Network::Testnet); + } + if flags & (FFINetwork::Regtest as c_uint) != 0 { + networks.push(Network::Regtest); + } + if flags & (FFINetwork::Devnet as c_uint) != 0 { + networks.push(Network::Devnet); + } + + networks + } +} + +impl FFINetwork { + /// Try to convert to a single Network + /// Returns None if multiple networks are set or if NoNetworks is set + pub fn try_into_single_network(&self) -> Option { + let flags = *self as c_uint; + + // Check if it's a single network + match flags { + 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), + _ => None, // Multiple networks or NoNetworks + } + } +} + +use std::convert::TryFrom; + +impl TryFrom for Network { + type Error = &'static str; + + fn try_from(value: FFINetwork) -> Result { + match value.try_into_single_network() { + Some(network) => Ok(network), + None => Err("FFINetwork must represent exactly one network"), } } } @@ -37,6 +87,31 @@ impl From for FFINetwork { } } +/// FFI Balance type for representing wallet balances +#[repr(C)] +#[derive(Debug, Clone, Copy, Default)] +pub struct FFIBalance { + /// Confirmed balance in duffs + pub confirmed: u64, + /// Unconfirmed balance in duffs + pub unconfirmed: u64, + /// Immature balance in duffs (e.g., mining rewards) + pub immature: u64, + /// Total balance (confirmed + unconfirmed) in duffs + pub total: u64, +} + +impl From 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, + } + } +} + /// Opaque wallet handle pub struct FFIWallet { pub(crate) wallet: Arc, @@ -97,24 +172,12 @@ impl FFIAccountResult { } } -/// Opaque account handle -pub struct FFIAccount { - pub(crate) account: Arc, -} - -impl FFIAccount { - /// Create a new FFI account handle - pub fn new(account: &key_wallet::Account) -> Self { - FFIAccount { - account: Arc::new(account.clone()), - } - } - - /// Get a reference to the inner account - pub fn inner(&self) -> &key_wallet::Account { - self.account.as_ref() - } -} +/// Forward declaration for FFIAccount (defined in account.rs) +pub use crate::account::FFIAccount; +#[cfg(feature = "bls")] +pub use crate::account::FFIBLSAccount; +#[cfg(feature = "eddsa")] +pub use crate::account::FFIEdDSAAccount; /// Standard account subtype #[repr(C)] @@ -134,7 +197,7 @@ pub enum FFIStandardAccountType { /// - Identity accounts: Registration, top-up, and invitation funding /// - Provider accounts: Various masternode provider key types (voting, owner, operator, platform) #[repr(C)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub enum FFIAccountType { /// Standard BIP44 account (m/44'/coin_type'/account'/x/x) StandardBIP44 = 0, @@ -161,47 +224,37 @@ pub enum FFIAccountType { } impl FFIAccountType { - /// Convert to AccountType with optional indices - /// Returns None if required parameters are missing (e.g., registration_index for IdentityTopUp) - pub fn to_account_type( - self, - index: u32, - registration_index: Option, - ) -> Option { + /// Convert to AccountType with the provided index (used where applicable). + /// For types needing an index (e.g., IdentityTopUp.registration_index), the provided index is used. + pub fn to_account_type(self, index: u32) -> key_wallet::AccountType { use key_wallet::account::account_type::StandardAccountType; match self { - FFIAccountType::StandardBIP44 => Some(key_wallet::AccountType::Standard { + FFIAccountType::StandardBIP44 => key_wallet::AccountType::Standard { index, standard_account_type: StandardAccountType::BIP44Account, - }), - FFIAccountType::StandardBIP32 => Some(key_wallet::AccountType::Standard { + }, + FFIAccountType::StandardBIP32 => key_wallet::AccountType::Standard { index, standard_account_type: StandardAccountType::BIP32Account, - }), - FFIAccountType::CoinJoin => Some(key_wallet::AccountType::CoinJoin { + }, + FFIAccountType::CoinJoin => key_wallet::AccountType::CoinJoin { index, - }), - FFIAccountType::IdentityRegistration => { - Some(key_wallet::AccountType::IdentityRegistration) - } + }, + FFIAccountType::IdentityRegistration => key_wallet::AccountType::IdentityRegistration, FFIAccountType::IdentityTopUp => { // IdentityTopUp requires a registration_index - registration_index.map(|reg_idx| key_wallet::AccountType::IdentityTopUp { - registration_index: reg_idx, - }) + key_wallet::AccountType::IdentityTopUp { + registration_index: index, + } } FFIAccountType::IdentityTopUpNotBoundToIdentity => { - Some(key_wallet::AccountType::IdentityTopUpNotBoundToIdentity) - } - FFIAccountType::IdentityInvitation => Some(key_wallet::AccountType::IdentityInvitation), - FFIAccountType::ProviderVotingKeys => Some(key_wallet::AccountType::ProviderVotingKeys), - FFIAccountType::ProviderOwnerKeys => Some(key_wallet::AccountType::ProviderOwnerKeys), - FFIAccountType::ProviderOperatorKeys => { - Some(key_wallet::AccountType::ProviderOperatorKeys) - } - FFIAccountType::ProviderPlatformKeys => { - Some(key_wallet::AccountType::ProviderPlatformKeys) + key_wallet::AccountType::IdentityTopUpNotBoundToIdentity } + FFIAccountType::IdentityInvitation => key_wallet::AccountType::IdentityInvitation, + FFIAccountType::ProviderVotingKeys => key_wallet::AccountType::ProviderVotingKeys, + FFIAccountType::ProviderOwnerKeys => key_wallet::AccountType::ProviderOwnerKeys, + FFIAccountType::ProviderOperatorKeys => key_wallet::AccountType::ProviderOperatorKeys, + FFIAccountType::ProviderPlatformKeys => key_wallet::AccountType::ProviderPlatformKeys, } } @@ -294,7 +347,7 @@ pub enum FFIAccountCreationOptionType { /// Create specific accounts with full control SpecificAccounts = 3, /// Create no accounts at all - None = 4, + NoAccounts = 4, } /// FFI structure for wallet account creation options @@ -359,7 +412,7 @@ impl FFIWalletAccountCreationOptions { match self.option_type { FFIAccountCreationOptionType::Default => WalletAccountCreationOptions::Default, - FFIAccountCreationOptionType::None => WalletAccountCreationOptions::None, + FFIAccountCreationOptionType::NoAccounts => WalletAccountCreationOptions::None, FFIAccountCreationOptionType::BIP44AccountsOnly => { let mut bip44_set = BTreeSet::new(); if !self.bip44_indices.is_null() && self.bip44_count > 0 { @@ -440,11 +493,7 @@ impl FFIWalletAccountCreationOptions { ); let mut accounts = Vec::new(); for &ffi_type in slice { - // Use a dummy index for special accounts that don't need one - // Skip accounts that require parameters we don't have - if let Some(account_type) = ffi_type.to_account_type(0, None) { - accounts.push(account_type); - } + accounts.push(ffi_type.to_account_type(0)); } Some(accounts) } else { diff --git a/key-wallet-ffi/src/utxo.rs b/key-wallet-ffi/src/utxo.rs index 5f03c2c77..8dbe0cccc 100644 --- a/key-wallet-ffi/src/utxo.rs +++ b/key-wallet-ffi/src/utxo.rs @@ -67,10 +67,8 @@ impl FFIUTXO { } if !self.script_pubkey.is_null() && self.script_len > 0 { // Reconstruct the boxed slice with DST pointer - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut( - self.script_pubkey, - self.script_len, - )); + let _ = + Box::from_raw(ptr::slice_from_raw_parts_mut(self.script_pubkey, self.script_len)); self.script_pubkey = ptr::null_mut(); self.script_len = 0; } @@ -101,7 +99,19 @@ pub unsafe extern "C" fn managed_wallet_get_utxos( } let managed_info = &*managed_info; - let network_rust: key_wallet::Network = network.into(); + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + *count_out = 0; + *utxos_out = ptr::null_mut(); + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return false; + } + }; // Get UTXOs from the managed wallet info let utxos = managed_info.inner().get_utxos(network_rust); @@ -203,7 +213,7 @@ pub unsafe extern "C" fn utxo_array_free(utxos: *mut FFIUTXO, count: usize) { } // Free the array itself by reconstructing the boxed slice with DST pointer - let _ = Box::from_raw(std::ptr::slice_from_raw_parts_mut(utxos, count)); + let _ = Box::from_raw(ptr::slice_from_raw_parts_mut(utxos, count)); } } diff --git a/key-wallet-ffi/src/wallet.rs b/key-wallet-ffi/src/wallet.rs index d9d7a0639..560187ae2 100644 --- a/key-wallet-ffi/src/wallet.rs +++ b/key-wallet-ffi/src/wallet.rs @@ -29,7 +29,7 @@ use crate::types::{FFINetwork, FFIWallet, FFIWalletAccountCreationOptions}; pub unsafe extern "C" fn wallet_create_from_mnemonic_with_options( mnemonic: *const c_char, passphrase: *const c_char, - network: FFINetwork, + networks: FFINetwork, account_options: *const FFIWalletAccountCreationOptions, error: *mut FFIError, ) -> *mut FFIWallet { @@ -83,7 +83,8 @@ pub unsafe extern "C" fn wallet_create_from_mnemonic_with_options( } }; - let network_rust: key_wallet::Network = network.into(); + // Convert networks bit flags to vector + let networks_rust = networks.parse_networks(); // Convert account creation options let creation_options = if account_options.is_null() { @@ -93,7 +94,7 @@ pub unsafe extern "C" fn wallet_create_from_mnemonic_with_options( }; let wallet = if passphrase_str.is_empty() { - match Wallet::from_mnemonic(mnemonic, network_rust, creation_options) { + match Wallet::from_mnemonic(mnemonic, &networks_rust, creation_options) { Ok(w) => w, Err(e) => { FFIError::set_error( @@ -110,7 +111,7 @@ pub unsafe extern "C" fn wallet_create_from_mnemonic_with_options( match Wallet::from_mnemonic_with_passphrase( mnemonic, passphrase_str.to_string(), - network_rust, + &networks_rust, creation_options, ) { Ok(w) => w, @@ -129,7 +130,7 @@ pub unsafe extern "C" fn wallet_create_from_mnemonic_with_options( Box::into_raw(Box::new(FFIWallet::new(wallet))) } -/// Create a new wallet from mnemonic (backward compatibility) +/// Create a new wallet from mnemonic (backward compatibility - single network) /// /// # Safety /// @@ -166,7 +167,7 @@ pub unsafe extern "C" fn wallet_create_from_mnemonic( pub unsafe extern "C" fn wallet_create_from_seed_with_options( seed: *const u8, seed_len: usize, - network: FFINetwork, + networks: FFINetwork, account_options: *const FFIWalletAccountCreationOptions, error: *mut FFIError, ) -> *mut FFIWallet { @@ -188,7 +189,9 @@ pub unsafe extern "C" fn wallet_create_from_seed_with_options( let mut seed_array = [0u8; 64]; seed_array.copy_from_slice(seed_bytes); let seed = Seed::new(seed_array); - let network_rust: key_wallet::Network = network.into(); + + // Convert networks bit flags to vector + let networks_rust = networks.parse_networks(); // Convert account creation options let creation_options = if account_options.is_null() { @@ -197,7 +200,7 @@ pub unsafe extern "C" fn wallet_create_from_seed_with_options( (*account_options).to_wallet_options() }; - match Wallet::from_seed(seed, network_rust, creation_options) { + match Wallet::from_seed(seed, &networks_rust, creation_options) { Ok(wallet) => { FFIError::set_success(error); Box::into_raw(Box::new(FFIWallet::new(wallet))) @@ -236,160 +239,6 @@ pub unsafe extern "C" fn wallet_create_from_seed( ) } -/// Create a new wallet from seed bytes -/// -/// # Safety -/// -/// - `seed_bytes` must be a valid pointer to a byte array of `seed_len` length -/// - `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 pointer must be freed with `wallet_free` when no longer needed -#[no_mangle] -pub unsafe extern "C" fn wallet_create_from_seed_bytes( - seed_bytes: *const u8, - seed_len: usize, - network: FFINetwork, - error: *mut FFIError, -) -> *mut FFIWallet { - if seed_bytes.is_null() { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Seed bytes are null".to_string()); - return ptr::null_mut(); - } - - let seed_slice = unsafe { slice::from_raw_parts(seed_bytes, seed_len) }; - let network_rust: key_wallet::Network = network.into(); - - // from_seed_bytes expects specific length - if seed_len != 64 { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - format!("Invalid seed length: {}, expected 64", seed_len), - ); - return ptr::null_mut(); - } - - let mut seed_array = [0u8; 64]; - seed_array.copy_from_slice(seed_slice); - - match Wallet::from_seed( - Seed::new(seed_array), - network_rust, - WalletAccountCreationOptions::Default, - ) { - Ok(wallet) => { - FFIError::set_success(error); - Box::into_raw(Box::new(FFIWallet::new(wallet))) - } - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - format!("Failed to create wallet from seed bytes: {}", e), - ); - ptr::null_mut() - } - } -} - -/// Create a watch-only wallet from extended public key -/// -/// # Safety -/// -/// - `xpub` must be a valid pointer to a null-terminated C string -/// - `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 -#[no_mangle] -pub unsafe extern "C" fn wallet_create_from_xpub( - xpub: *const c_char, - network: FFINetwork, - error: *mut FFIError, -) -> *mut FFIWallet { - if xpub.is_null() { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - "Extended public key is null".to_string(), - ); - return ptr::null_mut(); - } - - let xpub_str = match CStr::from_ptr(xpub).to_str() { - Ok(s) => s, - Err(_) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - "Invalid UTF-8 in extended public key".to_string(), - ); - return ptr::null_mut(); - } - }; - - let network_rust: key_wallet::Network = network.into(); - - use key_wallet::ExtendedPubKey; - use std::str::FromStr; - - let xpub = match ExtendedPubKey::from_str(xpub_str) { - Ok(xpub) => xpub, - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - format!("Invalid extended public key: {}", e), - ); - return ptr::null_mut(); - } - }; - - // Create a watch-only wallet with the given xpub as account 0 - use key_wallet::account::StandardAccountType; - use key_wallet::{Account, AccountCollection, AccountType}; - - let account_type = AccountType::Standard { - index: 0, - standard_account_type: StandardAccountType::BIP44Account, - }; - - // Create account 0 with the provided xpub - match Account::new(None, account_type, xpub, network_rust) { - Ok(account) => { - // Create an AccountCollection and add the account - let mut account_collection = AccountCollection::new(); - let _ = account_collection.insert(account); - - // Create the accounts map - let mut accounts = std::collections::BTreeMap::new(); - accounts.insert(network_rust, account_collection); - - // Create the watch-only wallet - match Wallet::from_xpub(xpub, accounts) { - Ok(wallet) => { - FFIError::set_success(error); - Box::into_raw(Box::new(FFIWallet::new(wallet))) - } - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - format!("Failed to create watch-only wallet: {}", e), - ); - ptr::null_mut() - } - } - } - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - format!("Failed to create account: {}", e), - ); - ptr::null_mut() - } - } -} - /// Create a new random wallet with options /// /// # Safety @@ -399,11 +248,12 @@ pub unsafe extern "C" fn wallet_create_from_xpub( /// - The caller must ensure all pointers remain valid for the duration of this call #[no_mangle] pub unsafe extern "C" fn wallet_create_random_with_options( - network: FFINetwork, + networks: FFINetwork, account_options: *const FFIWalletAccountCreationOptions, error: *mut FFIError, ) -> *mut FFIWallet { - let network_rust: key_wallet::Network = network.into(); + // Convert networks bit flags to vector + let networks_rust = networks.parse_networks(); // Convert account creation options let creation_options = if account_options.is_null() { @@ -412,7 +262,7 @@ pub unsafe extern "C" fn wallet_create_random_with_options( (*account_options).to_wallet_options() }; - match Wallet::new_random(network_rust, creation_options) { + match Wallet::new_random(&networks_rust, creation_options) { Ok(wallet) => { FFIError::set_success(error); Box::into_raw(Box::new(FFIWallet::new(wallet))) @@ -543,7 +393,20 @@ pub unsafe extern "C" fn wallet_get_xpub( unsafe { let wallet = &*wallet; - let network_rust: Network = network.into(); + + use std::convert::TryInto; + // Try to convert to a single network + let network_rust: Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network for getting xpub".to_string(), + ); + return ptr::null_mut(); + } + }; match wallet.inner().get_bip44_account(network_rust, account_index) { Some(account) => { @@ -620,7 +483,7 @@ pub unsafe extern "C" fn wallet_free_const(wallet: *const FFIWallet) { pub unsafe extern "C" fn wallet_add_account( wallet: *mut FFIWallet, network: FFINetwork, - account_type: c_uint, + account_type: crate::types::FFIAccountType, account_index: c_uint, ) -> crate::types::FFIAccountResult { if wallet.is_null() { @@ -631,54 +494,27 @@ pub unsafe extern "C" fn wallet_add_account( } let wallet = &mut *wallet; - let network_rust: key_wallet::Network = network.into(); - - use crate::types::FFIAccountType; - - let account_type_enum = match account_type { - 0 => FFIAccountType::StandardBIP44, - 1 => FFIAccountType::StandardBIP32, - 2 => FFIAccountType::CoinJoin, - 3 => FFIAccountType::IdentityRegistration, - 4 => { - // IdentityTopUp requires a registration_index - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "IdentityTopUp accounts require a registration_index. Use a specialized function instead".to_string(), - ); - } - 5 => FFIAccountType::IdentityTopUpNotBoundToIdentity, - 6 => FFIAccountType::IdentityInvitation, - 7 => FFIAccountType::ProviderVotingKeys, - 8 => FFIAccountType::ProviderOwnerKeys, - 9 => FFIAccountType::ProviderOperatorKeys, - 10 => FFIAccountType::ProviderPlatformKeys, - _ => { + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { return crate::types::FFIAccountResult::error( FFIErrorCode::InvalidInput, - format!("Invalid account type: {}", account_type), + "Must specify exactly one network".to_string(), ); } }; - let account_type = match account_type_enum.to_account_type(account_index, None) { - Some(at) => at, - None => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - format!("Missing required parameters for account type {}", account_type), - ); - } - }; + let account_type_rust = account_type.to_account_type(account_index); match wallet.inner_mut() { Some(w) => { // Use the proper add_account method - match w.add_account(account_type, network_rust, None) { + match w.add_account(account_type_rust, network_rust, None) { Ok(()) => { // Get the account we just added if let Some(account_collection) = w.accounts.get(&network_rust) { - if let Some(account) = account_collection.account_of_type(account_type) { + if let Some(account) = account_collection.account_of_type(account_type_rust) + { let ffi_account = crate::types::FFIAccount::new(account); return crate::types::FFIAccountResult::success(Box::into_raw( Box::new(ffi_account), @@ -716,7 +552,7 @@ pub unsafe extern "C" fn wallet_add_account( pub unsafe extern "C" fn wallet_add_account_with_xpub_bytes( wallet: *mut FFIWallet, network: FFINetwork, - account_type: c_uint, + account_type: crate::types::FFIAccountType, account_index: c_uint, xpub_bytes: *const u8, xpub_len: usize, @@ -736,45 +572,19 @@ pub unsafe extern "C" fn wallet_add_account_with_xpub_bytes( } let wallet = &mut *wallet; - let network_rust: key_wallet::Network = network.into(); - - use crate::types::FFIAccountType; - use key_wallet::ExtendedPubKey; - - let account_type_enum = match account_type { - 0 => FFIAccountType::StandardBIP44, - 1 => FFIAccountType::StandardBIP32, - 2 => FFIAccountType::CoinJoin, - 3 => FFIAccountType::IdentityRegistration, - 4 => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "IdentityTopUp accounts require a registration_index. Use a specialized function instead".to_string(), - ); - } - 5 => FFIAccountType::IdentityTopUpNotBoundToIdentity, - 6 => FFIAccountType::IdentityInvitation, - 7 => FFIAccountType::ProviderVotingKeys, - 8 => FFIAccountType::ProviderOwnerKeys, - 9 => FFIAccountType::ProviderOperatorKeys, - 10 => FFIAccountType::ProviderPlatformKeys, - _ => { + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { return crate::types::FFIAccountResult::error( FFIErrorCode::InvalidInput, - format!("Invalid account type: {}", account_type), + "Must specify exactly one network".to_string(), ); } }; - let account_type = match account_type_enum.to_account_type(account_index, None) { - Some(at) => at, - None => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - format!("Missing required parameters for account type {}", account_type), - ); - } - }; + use key_wallet::ExtendedPubKey; + + let account_type_rust = account_type.to_account_type(account_index); // Parse the xpub from bytes (assuming it's a string representation) let xpub_slice = slice::from_raw_parts(xpub_bytes, xpub_len); @@ -799,11 +609,11 @@ pub unsafe extern "C" fn wallet_add_account_with_xpub_bytes( }; match wallet.inner_mut() { - Some(w) => match w.add_account(account_type, network_rust, Some(xpub)) { + Some(w) => match w.add_account(account_type_rust, network_rust, Some(xpub)) { Ok(()) => { // Get the account we just added if let Some(account_collection) = w.accounts.get(&network_rust) { - if let Some(account) = account_collection.account_of_type(account_type) { + if let Some(account) = account_collection.account_of_type(account_type_rust) { let ffi_account = crate::types::FFIAccount::new(account); return crate::types::FFIAccountResult::success(Box::into_raw(Box::new( ffi_account, @@ -840,7 +650,7 @@ pub unsafe extern "C" fn wallet_add_account_with_xpub_bytes( pub unsafe extern "C" fn wallet_add_account_with_string_xpub( wallet: *mut FFIWallet, network: FFINetwork, - account_type: c_uint, + account_type: crate::types::FFIAccountType, account_index: c_uint, xpub_string: *const c_char, ) -> crate::types::FFIAccountResult { @@ -859,45 +669,19 @@ pub unsafe extern "C" fn wallet_add_account_with_string_xpub( } let wallet = &mut *wallet; - let network_rust: key_wallet::Network = network.into(); - - use crate::types::FFIAccountType; - use key_wallet::ExtendedPubKey; - - let account_type_enum = match account_type { - 0 => FFIAccountType::StandardBIP44, - 1 => FFIAccountType::StandardBIP32, - 2 => FFIAccountType::CoinJoin, - 3 => FFIAccountType::IdentityRegistration, - 4 => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - "IdentityTopUp accounts require a registration_index. Use a specialized function instead".to_string(), - ); - } - 5 => FFIAccountType::IdentityTopUpNotBoundToIdentity, - 6 => FFIAccountType::IdentityInvitation, - 7 => FFIAccountType::ProviderVotingKeys, - 8 => FFIAccountType::ProviderOwnerKeys, - 9 => FFIAccountType::ProviderOperatorKeys, - 10 => FFIAccountType::ProviderPlatformKeys, - _ => { + let network_rust: key_wallet::Network = match network.try_into() { + Ok(n) => n, + Err(_) => { return crate::types::FFIAccountResult::error( FFIErrorCode::InvalidInput, - format!("Invalid account type: {}", account_type), + "Must specify exactly one network".to_string(), ); } }; - let account_type = match account_type_enum.to_account_type(account_index, None) { - Some(at) => at, - None => { - return crate::types::FFIAccountResult::error( - FFIErrorCode::InvalidInput, - format!("Missing required parameters for account type {}", account_type), - ); - } - }; + use key_wallet::ExtendedPubKey; + + let account_type_rust = account_type.to_account_type(account_index); // Parse the xpub from C string let xpub_str = match CStr::from_ptr(xpub_string).to_str() { @@ -921,11 +705,11 @@ pub unsafe extern "C" fn wallet_add_account_with_string_xpub( }; match wallet.inner_mut() { - Some(w) => match w.add_account(account_type, network_rust, Some(xpub)) { + Some(w) => match w.add_account(account_type_rust, network_rust, Some(xpub)) { Ok(()) => { // Get the account we just added if let Some(account_collection) = w.accounts.get(&network_rust) { - if let Some(account) = account_collection.account_of_type(account_type) { + if let Some(account) = account_collection.account_of_type(account_type_rust) { let ffi_account = crate::types::FFIAccount::new(account); return crate::types::FFIAccountResult::success(Box::into_raw(Box::new( ffi_account, diff --git a/key-wallet-ffi/src/wallet_manager.rs b/key-wallet-ffi/src/wallet_manager.rs index d8767213f..4aa4e4845 100644 --- a/key-wallet-ffi/src/wallet_manager.rs +++ b/key-wallet-ffi/src/wallet_manager.rs @@ -4,6 +4,10 @@ #[path = "wallet_manager_tests.rs"] mod tests; +#[cfg(test)] +#[path = "wallet_manager_serialization_tests.rs"] +mod serialization_tests; + use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_uint}; use std::ptr; @@ -89,17 +93,7 @@ pub unsafe extern "C" fn wallet_manager_add_wallet_from_mnemonic_with_options( } }; - // Generate wallet ID from mnemonic + passphrase - use sha2::{Digest, Sha256}; - let mut hasher = Sha256::new(); - hasher.update(mnemonic_str.as_bytes()); - hasher.update(passphrase_str.as_bytes()); - let hash = hasher.finalize(); - let mut wallet_id = [0u8; 32]; - wallet_id.copy_from_slice(&hash); - - let network_rust: Network = network.into(); - let name = format!("Wallet {}", hex::encode(&wallet_id[0..4])); + let networks_rust = network.parse_networks(); unsafe { let manager_ref = &*manager; @@ -115,16 +109,6 @@ pub unsafe extern "C" fn wallet_manager_add_wallet_from_mnemonic_with_options( } }; - // Check if wallet already exists - if manager_guard.get_wallet(&wallet_id).is_some() { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - "Wallet already exists".to_string(), - ); - return false; - } - // Convert account creation options let creation_options = if account_options.is_null() { key_wallet::wallet::initialization::WalletAccountCreationOptions::Default @@ -134,15 +118,13 @@ pub unsafe extern "C" fn wallet_manager_add_wallet_from_mnemonic_with_options( // Use the WalletManager's public method to create the wallet match manager_guard.create_wallet_from_mnemonic( - wallet_id, - name, mnemonic_str, passphrase_str, - Some(network_rust), + networks_rust.as_slice(), None, // birth_height creation_options, ) { - Ok(_) => { + Ok(wallet_id) => { // Track the wallet ID let mut ids_guard = match manager_ref.wallet_ids.lock() { Ok(g) => g, @@ -199,6 +181,291 @@ pub unsafe extern "C" fn wallet_manager_add_wallet_from_mnemonic( ) } +/// Add a wallet from mnemonic to the manager and return serialized bytes +/// +/// Creates a wallet from a mnemonic phrase, adds it to the manager, optionally downgrading it +/// to a pubkey-only wallet (watch-only or externally signable), and returns the serialized wallet bytes. +/// +/// # Safety +/// +/// - `manager` must be a valid pointer to an FFIWalletManager instance +/// - `mnemonic` must be a valid pointer to a null-terminated C string +/// - `passphrase` must be a valid pointer to a null-terminated C string or null +/// - `birth_height` is optional, pass 0 for default +/// - `account_options` must be a valid pointer to FFIWalletAccountCreationOptions or null +/// - `downgrade_to_pubkey_wallet` if true, creates a watch-only or externally signable wallet +/// - `allow_external_signing` if true AND downgrade_to_pubkey_wallet is true, creates an externally signable wallet +/// - `wallet_bytes_out` must be a valid pointer to a pointer that will receive the serialized bytes +/// - `wallet_bytes_len_out` must be a valid pointer that will receive the byte length +/// - `wallet_id_out` must be a valid pointer to a 32-byte array that will receive the wallet ID +/// - `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 caller must free the returned wallet_bytes using wallet_manager_free_wallet_bytes() +#[cfg(feature = "bincode")] +#[no_mangle] +pub unsafe extern "C" fn wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager: *mut FFIWalletManager, + mnemonic: *const c_char, + passphrase: *const c_char, + network: FFINetwork, + birth_height: c_uint, + account_options: *const crate::types::FFIWalletAccountCreationOptions, + downgrade_to_pubkey_wallet: bool, + allow_external_signing: bool, + wallet_bytes_out: *mut *mut u8, + wallet_bytes_len_out: *mut usize, + wallet_id_out: *mut u8, + error: *mut FFIError, +) -> bool { + // Validate input parameters + if manager.is_null() + || mnemonic.is_null() + || wallet_bytes_out.is_null() + || wallet_bytes_len_out.is_null() + || wallet_id_out.is_null() + { + FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); + return false; + } + + // Parse mnemonic string + let mnemonic_str = unsafe { + match CStr::from_ptr(mnemonic).to_str() { + Ok(s) => s, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid UTF-8 in mnemonic".to_string(), + ); + return false; + } + } + }; + + // Parse passphrase string + let passphrase_str = if passphrase.is_null() { + "" + } else { + unsafe { + match CStr::from_ptr(passphrase).to_str() { + Ok(s) => s, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Invalid UTF-8 in passphrase".to_string(), + ); + return false; + } + } + } + }; + + // Convert networks + let networks = network.parse_networks(); + + // Convert account creation options + let creation_options = if account_options.is_null() { + key_wallet::wallet::initialization::WalletAccountCreationOptions::Default + } else { + unsafe { (*account_options).to_wallet_options() } + }; + + // Get the manager and call the proper method + let manager_ref = unsafe { &*manager }; + let mut manager_guard = match manager_ref.manager.lock() { + Ok(guard) => guard, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Failed to lock manager".to_string(), + ); + return false; + } + }; + + // Convert birth_height: 0 means None, any other value means Some(value) + let birth_height = if birth_height == 0 { + None + } else { + Some(birth_height) + }; + + let (serialized, wallet_id) = match manager_guard + .create_wallet_from_mnemonic_return_serialized_bytes( + mnemonic_str, + passphrase_str, + &networks, + birth_height, + creation_options, + downgrade_to_pubkey_wallet, + allow_external_signing, + ) { + Ok(result) => result, + Err(e) => { + let ffi_error: FFIError = e.into(); + if !error.is_null() { + unsafe { + *error = ffi_error; + } + } + return false; + } + }; + + // Track the wallet ID in the FFI manager + if let Ok(mut wallet_ids) = manager_ref.wallet_ids.lock() { + wallet_ids.push(wallet_id); + } + + // Allocate memory for the serialized bytes + let boxed_bytes = serialized.into_boxed_slice(); + let bytes_len = boxed_bytes.len(); + let bytes_ptr = Box::into_raw(boxed_bytes) as *mut u8; + + // Write output values + unsafe { + *wallet_bytes_out = bytes_ptr; + *wallet_bytes_len_out = bytes_len; + ptr::copy_nonoverlapping(wallet_id.as_ptr(), wallet_id_out, 32); + } + + FFIError::set_success(error); + true +} + +/// Free wallet bytes buffer +/// +/// # Safety +/// +/// - `wallet_bytes` must be a valid pointer to a buffer allocated by wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes +/// - `bytes_len` must match the original allocation size +/// - The pointer must not be used after calling this function +/// - This function must only be called once per buffer +#[cfg(feature = "bincode")] +#[no_mangle] +pub unsafe extern "C" fn wallet_manager_free_wallet_bytes(wallet_bytes: *mut u8, bytes_len: usize) { + if !wallet_bytes.is_null() && bytes_len > 0 { + unsafe { + // Reconstruct the boxed slice with the correct DST pointer + ptr::write_bytes(wallet_bytes, 0, bytes_len); + let _ = Box::from_raw(ptr::slice_from_raw_parts_mut(wallet_bytes, bytes_len)); + } + } +} + +/// Import a wallet from bincode-serialized bytes +/// +/// Deserializes a wallet from bytes and adds it to the manager. +/// Returns a 32-byte wallet ID on success. +/// +/// # Safety +/// +/// - `manager` must be a valid pointer to an FFIWalletManager instance +/// - `wallet_bytes` must be a valid pointer to bincode-serialized wallet bytes +/// - `wallet_bytes_len` must be the exact length of the wallet bytes +/// - `wallet_id_out` must be a valid pointer to a 32-byte array that will receive the wallet ID +/// - `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 +#[cfg(feature = "bincode")] +#[no_mangle] +pub unsafe extern "C" fn wallet_manager_import_wallet_from_bytes( + manager: *mut FFIWalletManager, + wallet_bytes: *const u8, + wallet_bytes_len: usize, + wallet_id_out: *mut u8, + error: *mut FFIError, +) -> bool { + // Validate input parameters + if manager.is_null() + || wallet_bytes.is_null() + || wallet_bytes_len == 0 + || wallet_id_out.is_null() + { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Null pointer or invalid length provided".to_string(), + ); + return false; + } + + // Create a byte slice from the raw pointer + let wallet_bytes_slice = unsafe { std::slice::from_raw_parts(wallet_bytes, wallet_bytes_len) }; + + // Get the manager reference + let manager_ref = unsafe { &*manager }; + + // Lock the manager + let mut manager_guard = match manager_ref.manager.lock() { + Ok(g) => g, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + "Failed to lock wallet manager".to_string(), + ); + return false; + } + }; + + // Import the wallet + match manager_guard.import_wallet_from_bytes(wallet_bytes_slice) { + Ok(wallet_id) => { + // Copy the wallet ID to the output buffer + unsafe { + ptr::copy_nonoverlapping(wallet_id.as_ptr(), wallet_id_out, 32); + } + + // Track the wallet ID in our FFI manager + let mut ids_guard = match manager_ref.wallet_ids.lock() { + Ok(g) => g, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + "Failed to lock wallet IDs".to_string(), + ); + return false; + } + }; + ids_guard.push(wallet_id); + + FFIError::set_success(error); + true + } + Err(e) => { + // Convert the error to FFI error + match e { + key_wallet_manager::wallet_manager::WalletError::WalletExists(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidState, + "Wallet already exists in the manager".to_string(), + ); + } + key_wallet_manager::wallet_manager::WalletError::InvalidParameter(msg) => { + FFIError::set_error( + error, + FFIErrorCode::SerializationError, + format!("Failed to deserialize wallet: {}", msg), + ); + } + _ => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to import wallet: {:?}", e), + ); + } + } + false + } + } +} + /// Get wallet IDs /// /// # Safety @@ -415,7 +682,17 @@ pub unsafe extern "C" fn wallet_manager_get_receive_address( } }; - let network_rust: Network = network.into(); + let network_rust: Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return ptr::null_mut(); + } + }; // Use the WalletManager's public method to get next receive address use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; @@ -498,7 +775,17 @@ pub unsafe extern "C" fn wallet_manager_get_change_address( } }; - let network_rust: Network = network.into(); + let network_rust: Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return ptr::null_mut(); + } + }; // Use the WalletManager's public method to get next change address use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; @@ -669,7 +956,17 @@ pub unsafe extern "C" fn wallet_manager_process_transaction( }; // Convert FFINetwork to Network - let network = network.into(); + let network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return false; + } + }; // Convert FFI context to native TransactionContext let context = unsafe { (*context).to_transaction_context() }; @@ -737,7 +1034,17 @@ pub unsafe extern "C" fn wallet_manager_get_monitored_addresses( } }; - let network_rust: Network = network.into(); + let network_rust: Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return false; + } + }; let mut all_addresses: Vec<*mut c_char> = Vec::new(); // Collect addresses from all wallets for this network @@ -823,7 +1130,17 @@ pub unsafe extern "C" fn wallet_manager_update_height( } }; - let network_rust: Network = network.into(); + let network_rust: Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return false; + } + }; manager_guard.update_height(network_rust, height); FFIError::set_success(error); @@ -861,7 +1178,17 @@ pub unsafe extern "C" fn wallet_manager_current_height( } }; - let network_rust: Network = network.into(); + let network_rust: Network = match network.try_into() { + Ok(n) => n, + Err(_) => { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Must specify exactly one network".to_string(), + ); + return 0; + } + }; // Get current height from network state if it exists let height = manager_guard diff --git a/key-wallet-ffi/src/wallet_manager_serialization_tests.rs b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs new file mode 100644 index 000000000..2df30bf94 --- /dev/null +++ b/key-wallet-ffi/src/wallet_manager_serialization_tests.rs @@ -0,0 +1,419 @@ +//! Tests for wallet serialization FFI functions + +#[cfg(all(test, feature = "bincode"))] +mod tests { + use crate::error::{FFIError, FFIErrorCode}; + use crate::types::{FFINetwork, FFIWalletAccountCreationOptions}; + use crate::wallet_manager; + use std::ffi::CString; + use std::ptr; + + const TEST_MNEMONIC: &str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + #[test] + fn test_create_wallet_return_serialized_bytes_full_wallet() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + // Create a full wallet with private keys + let success = unsafe { + wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, // birth_height + ptr::null(), // default account options + false, // don't downgrade to pubkey wallet + false, // allow_external_signing + &mut wallet_bytes_out, + &mut wallet_bytes_len_out, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(success, "Failed to create wallet"); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + + // Verify wallet ID is not all zeros + assert!(wallet_id_out.iter().any(|&b| b != 0), "Wallet ID should not be all zeros"); + + // Clean up + unsafe { + wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + wallet_manager::wallet_manager_free(manager); + } + } + + #[test] + fn test_create_wallet_return_serialized_bytes_watch_only() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + // Create a watch-only wallet (no private keys) + let success = unsafe { + wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + ptr::null(), + true, // downgrade to pubkey wallet + false, // watch-only + &mut wallet_bytes_out, + &mut wallet_bytes_len_out, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(success, "Failed to create watch-only wallet"); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + + // Clean up + unsafe { + wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + wallet_manager::wallet_manager_free(manager); + } + } + + #[test] + fn test_create_wallet_return_serialized_bytes_externally_signable() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + // Create an externally signable wallet + let success = unsafe { + wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + ptr::null(), + true, // downgrade to pubkey wallet + true, // externally signable + &mut wallet_bytes_out, + &mut wallet_bytes_len_out, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(success, "Failed to create externally signable wallet"); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + + // Clean up + unsafe { + wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + wallet_manager::wallet_manager_free(manager); + } + } + + #[test] + fn test_create_wallet_with_passphrase() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("test_passphrase").unwrap(); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + // Create wallet with passphrase + let success = unsafe { + wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + ptr::null(), + false, + false, + &mut wallet_bytes_out, + &mut wallet_bytes_len_out, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(success, "Failed to create wallet with passphrase"); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + + // Clean up + unsafe { + wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + wallet_manager::wallet_manager_free(manager); + } + } + + #[test] + fn test_import_serialized_wallet() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut original_wallet_id = [0u8; 32]; + + // First create and serialize a wallet + let success = unsafe { + wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + ptr::null(), + false, + false, + &mut wallet_bytes_out, + &mut wallet_bytes_len_out, + original_wallet_id.as_mut_ptr(), + error, + ) + }; + + assert!(success); + assert!(!wallet_bytes_out.is_null()); + + // Now import the wallet into a new manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let mut imported_wallet_id = [0u8; 32]; + let import_success = unsafe { + wallet_manager::wallet_manager_import_wallet_from_bytes( + manager, + wallet_bytes_out, + wallet_bytes_len_out, + imported_wallet_id.as_mut_ptr(), + error, + ) + }; + + assert!(import_success, "Failed to import wallet"); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + + // Wallet IDs should match + assert_eq!(original_wallet_id, imported_wallet_id, "Wallet IDs should match"); + + // Clean up + unsafe { + wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + wallet_manager::wallet_manager_free(manager); + } + } + + #[test] + fn test_invalid_mnemonic() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let invalid_mnemonic = CString::new("invalid mnemonic phrase").unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + let success = unsafe { + wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + invalid_mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + ptr::null(), + false, + false, + &mut wallet_bytes_out, + &mut wallet_bytes_len_out, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(!success, "Should fail with invalid mnemonic"); + assert_ne!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(wallet_bytes_out.is_null()); + assert_eq!(wallet_bytes_len_out, 0); + } + + #[test] + fn test_null_mnemonic() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + let success = unsafe { + wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + ptr::null(), + ptr::null(), + FFINetwork::Testnet, + 0, + ptr::null(), + false, + false, + &mut wallet_bytes_out, + &mut wallet_bytes_len_out, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(!success, "Should fail with null mnemonic"); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); + } + + #[test] + fn test_create_wallet_with_custom_account_options() { + use crate::types::FFIAccountCreationOptionType; + + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + // Create custom account options (BIP44 accounts only) + let bip44_indices = vec![0u32, 1u32, 2u32]; + + let account_options = FFIWalletAccountCreationOptions { + option_type: FFIAccountCreationOptionType::BIP44AccountsOnly, + bip44_indices: bip44_indices.as_ptr(), + bip44_count: bip44_indices.len(), + bip32_indices: ptr::null(), + bip32_count: 0, + coinjoin_indices: ptr::null(), + coinjoin_count: 0, + topup_indices: ptr::null(), + topup_count: 0, + special_account_types: ptr::null(), + special_account_types_count: 0, + }; + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + let success = unsafe { + wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + &account_options, + false, + false, + &mut wallet_bytes_out, + &mut wallet_bytes_len_out, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(success, "Failed to create wallet with custom options"); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + + // Clean up + unsafe { + wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + wallet_manager::wallet_manager_free(manager); + } + } +} diff --git a/key-wallet-ffi/src/wallet_manager_tests.rs b/key-wallet-ffi/src/wallet_manager_tests.rs index 7c55a8495..e22789ffd 100644 --- a/key-wallet-ffi/src/wallet_manager_tests.rs +++ b/key-wallet-ffi/src/wallet_manager_tests.rs @@ -1293,4 +1293,305 @@ mod tests { wallet_manager::wallet_manager_free(manager); } } + + #[cfg(feature = "bincode")] + #[test] + fn test_create_wallet_from_mnemonic_return_serialized_bytes() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create a wallet manager + let manager = crate::wallet_manager::wallet_manager_create(error); + assert!(!manager.is_null()); + + // Test basic wallet creation and serialization + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + let success = unsafe { + crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, // birth_height + ptr::null(), // default account options + false, // don't downgrade to pubkey wallet + false, // allow_external_signing + &mut wallet_bytes_out as *mut *mut u8, + &mut wallet_bytes_len_out as *mut usize, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(success); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + assert_ne!(wallet_id_out, [0u8; 32]); + + // Store the wallet ID for comparison + let original_wallet_id = wallet_id_out; + + // Clean up the serialized bytes + unsafe { + crate::wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + } + + // Test with downgrade to watch-only wallet (create new manager to avoid duplicate wallet ID) + let manager2 = crate::wallet_manager::wallet_manager_create(error); + assert!(!manager2.is_null()); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + let success = unsafe { + crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager2, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + ptr::null(), + true, // downgrade to pubkey wallet + false, // watch-only, not externally signable + &mut wallet_bytes_out as *mut *mut u8, + &mut wallet_bytes_len_out as *mut usize, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + if !success { + let error_msg = if unsafe { (*error).message.is_null() } { + "No error message".to_string() + } else { + unsafe { std::ffi::CStr::from_ptr((*error).message).to_string_lossy().to_string() } + }; + panic!("Function failed with error: {:?} - {}", unsafe { (*error).code }, error_msg); + } + assert!(success); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + // The wallet ID should be the same since it's derived from the same mnemonic + assert_eq!(wallet_id_out, original_wallet_id); + + // Import the watch-only wallet to verify it works (create third manager for import) + let manager3 = crate::wallet_manager::wallet_manager_create(error); + assert!(!manager3.is_null()); + + let wallet_bytes_slice = + unsafe { slice::from_raw_parts(wallet_bytes_out, wallet_bytes_len_out) }; + let mut import_wallet_id_out = [0u8; 32]; + + let import_success = unsafe { + crate::wallet_manager::wallet_manager_import_wallet_from_bytes( + manager3, + wallet_bytes_slice.as_ptr(), + wallet_bytes_slice.len(), + import_wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(import_success); + assert_eq!(import_wallet_id_out, original_wallet_id); + + // Clean up + unsafe { + crate::wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + wallet_manager::wallet_manager_free(manager2); + wallet_manager::wallet_manager_free(manager3); + } + + // Test with externally signable wallet (create fourth manager) + let manager4 = crate::wallet_manager::wallet_manager_create(error); + assert!(!manager4.is_null()); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + let success = unsafe { + crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager4, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + ptr::null(), + true, // downgrade to pubkey wallet + true, // externally signable + &mut wallet_bytes_out as *mut *mut u8, + &mut wallet_bytes_len_out as *mut usize, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(success); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + assert_eq!(wallet_id_out, original_wallet_id); + + // Clean up + unsafe { + crate::wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + } + + // Test with invalid mnemonic (create fifth manager) + let manager5 = crate::wallet_manager::wallet_manager_create(error); + assert!(!manager5.is_null()); + + let invalid_mnemonic = CString::new("invalid mnemonic phrase").unwrap(); + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + let success = unsafe { + crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager5, + invalid_mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 0, + ptr::null(), + false, + false, + &mut wallet_bytes_out as *mut *mut u8, + &mut wallet_bytes_len_out as *mut usize, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(!success); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidMnemonic); + assert!(wallet_bytes_out.is_null()); + assert_eq!(wallet_bytes_len_out, 0); + + // Clean up all managers + unsafe { + crate::wallet_manager::wallet_manager_free(manager); + crate::wallet_manager::wallet_manager_free(manager4); + crate::wallet_manager::wallet_manager_free(manager5); + } + } + + #[cfg(feature = "bincode")] + #[test] + fn test_serialized_wallet_across_managers() { + let mut error = FFIError::success(); + let error = &mut error as *mut FFIError; + + // Create first wallet manager + let manager1 = crate::wallet_manager::wallet_manager_create(error); + assert!(!manager1.is_null()); + + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut wallet_bytes_out: *mut u8 = ptr::null_mut(); + let mut wallet_bytes_len_out: usize = 0; + let mut wallet_id_out = [0u8; 32]; + + // Create and serialize a wallet with the first manager + let success = unsafe { + crate::wallet_manager::wallet_manager_add_wallet_from_mnemonic_return_serialized_bytes( + manager1, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + 100, // birth_height + ptr::null(), // default account options + false, // don't downgrade to pubkey wallet + false, // allow_external_signing + &mut wallet_bytes_out as *mut *mut u8, + &mut wallet_bytes_len_out as *mut usize, + wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(success); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert!(!wallet_bytes_out.is_null()); + assert!(wallet_bytes_len_out > 0); + + // Store the wallet ID for comparison + let original_wallet_id = wallet_id_out; + + // Create a copy of the serialized bytes before freeing the manager + 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 + }; + + // Clean up the first manager + unsafe { + crate::wallet_manager::wallet_manager_free(manager1); + } + + // Create a completely new wallet manager + let manager2 = crate::wallet_manager::wallet_manager_create(error); + assert!(!manager2.is_null()); + + // Import the wallet using the serialized bytes in the new manager + let mut import_wallet_id_out = [0u8; 32]; + let import_success = unsafe { + crate::wallet_manager::wallet_manager_import_wallet_from_bytes( + manager2, + wallet_bytes_copy.as_ptr(), + wallet_bytes_copy.len(), + import_wallet_id_out.as_mut_ptr(), + error, + ) + }; + + assert!(import_success); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + assert_eq!( + import_wallet_id_out, original_wallet_id, + "Wallet ID should be the same after import" + ); + + // Verify we can get the wallet from the new manager + let wallet = unsafe { + crate::wallet_manager::wallet_manager_get_wallet( + manager2, + import_wallet_id_out.as_ptr(), + error, + ) + }; + assert!(!wallet.is_null()); + assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); + + // Clean up + unsafe { + crate::wallet_manager::wallet_manager_free_wallet_bytes( + wallet_bytes_out, + wallet_bytes_len_out, + ); + crate::wallet_manager::wallet_manager_free(manager2); + } + } } diff --git a/key-wallet-ffi/src/wallet_tests.rs b/key-wallet-ffi/src/wallet_tests.rs index 19d8d78f4..d56e6d2ee 100644 --- a/key-wallet-ffi/src/wallet_tests.rs +++ b/key-wallet-ffi/src/wallet_tests.rs @@ -4,7 +4,7 @@ mod wallet_tests { use crate::account::account_free; use crate::error::{FFIError, FFIErrorCode}; - use crate::types::FFINetwork; + use crate::types::{FFIAccountType, FFINetwork}; use crate::wallet; use std::ffi::CString; use std::ptr; @@ -57,40 +57,6 @@ mod wallet_tests { } } - #[test] - fn test_wallet_creation_from_xpub() { - let mut error = FFIError::success(); - let error = &mut error as *mut FFIError; - - // Create a wallet first to get a xpub - let seed = [0x02u8; 64]; - let source_wallet = unsafe { - wallet::wallet_create_from_seed(seed.as_ptr(), seed.len(), FFINetwork::Testnet, error) - }; - assert!(!source_wallet.is_null()); - - // Get xpub - let xpub = unsafe { wallet::wallet_get_xpub(source_wallet, FFINetwork::Testnet, 0, error) }; - assert!(!xpub.is_null()); - - // Create watch-only wallet from xpub - let watch_wallet = - unsafe { wallet::wallet_create_from_xpub(xpub, FFINetwork::Testnet, error) }; - assert!(!watch_wallet.is_null()); - - // Verify it's watch-only - let is_watch_only = unsafe { wallet::wallet_is_watch_only(watch_wallet, error) }; - assert!(is_watch_only); - - // Clean up - unsafe { - wallet::wallet_free(source_wallet); - wallet::wallet_free(watch_wallet); - - let _ = CString::from_raw(xpub); - } - } - #[test] fn test_wallet_creation_methods() { let mut error = FFIError::success(); @@ -236,7 +202,7 @@ mod wallet_tests { let seed_bytes = [0x05u8; 64]; let wallet = unsafe { - wallet::wallet_create_from_seed_bytes( + wallet::wallet_create_from_seed( seed_bytes.as_ptr(), seed_bytes.len(), FFINetwork::Testnet, @@ -259,9 +225,8 @@ mod wallet_tests { let error = &mut error as *mut FFIError; // Test with null seed bytes - let wallet = unsafe { - wallet::wallet_create_from_seed_bytes(ptr::null(), 64, FFINetwork::Testnet, error) - }; + let wallet = + unsafe { wallet::wallet_create_from_seed(ptr::null(), 64, FFINetwork::Testnet, error) }; assert!(wallet.is_null()); assert_eq!(unsafe { (*error).code }, FFIErrorCode::InvalidInput); @@ -295,26 +260,6 @@ mod wallet_tests { unsafe { wallet::wallet_free(wallet_with_mnemonic); } - - // Create watch-only wallet (no mnemonic) - let seed = [0x06u8; 64]; - let source_wallet = unsafe { - wallet::wallet_create_from_seed(seed.as_ptr(), seed.len(), FFINetwork::Testnet, error) - }; - let xpub = unsafe { wallet::wallet_get_xpub(source_wallet, FFINetwork::Testnet, 0, error) }; - let watch_wallet = - unsafe { wallet::wallet_create_from_xpub(xpub, FFINetwork::Testnet, error) }; - - // Test has_mnemonic - should return false for watch-only - let has_mnemonic = unsafe { wallet::wallet_has_mnemonic(watch_wallet, error) }; - assert!(!has_mnemonic); - - // Clean up - unsafe { - wallet::wallet_free(source_wallet); - wallet::wallet_free(watch_wallet); - let _ = CString::from_raw(xpub); - } } #[test] @@ -337,7 +282,14 @@ mod wallet_tests { assert!(!wallet.is_null()); // Test adding account - check if it succeeds or fails gracefully - let result = unsafe { wallet::wallet_add_account(wallet, FFINetwork::Testnet, 0, 1) }; + let result = unsafe { + wallet::wallet_add_account( + wallet, + FFINetwork::Testnet, + FFIAccountType::StandardBIP44, + 1, + ) + }; // 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); @@ -365,8 +317,14 @@ mod wallet_tests { #[test] fn test_wallet_add_account_null() { // Test with null wallet - let result = - unsafe { wallet::wallet_add_account(ptr::null_mut(), FFINetwork::Testnet, 0, 0) }; + let result = unsafe { + wallet::wallet_add_account( + ptr::null_mut(), + FFINetwork::Testnet, + FFIAccountType::StandardBIP44, + 0, + ) + }; assert!(result.account.is_null()); assert_ne!(result.error_code, 0); diff --git a/key-wallet-ffi/tests/integration_test.rs b/key-wallet-ffi/tests/integration_test.rs index b1c63d480..c114bb6ae 100644 --- a/key-wallet-ffi/tests/integration_test.rs +++ b/key-wallet-ffi/tests/integration_test.rs @@ -141,76 +141,12 @@ fn test_seed_to_wallet_workflow() { }; assert!(!wallet.is_null()); - // 3. Test the wallet created from seed - // Since we can't add a wallet from seed to manager, just verify it works - let mut wallet_balance = key_wallet_ffi::balance::FFIBalance::default(); - let success = unsafe { - key_wallet_ffi::balance::wallet_get_balance( - wallet, - FFINetwork::Testnet, - &mut wallet_balance, - error, - ) - }; - assert!(success); - assert_eq!(wallet_balance.confirmed, 0); - // Clean up unsafe { key_wallet_ffi::wallet::wallet_free(wallet); } } -#[test] -fn test_watch_only_wallet() { - let mut error = FFIError::success(); - let error = &mut error as *mut FFIError; - - // 1. Create a regular wallet from seed - let seed = vec![0x01u8; 64]; - let source_wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_seed( - seed.as_ptr(), - seed.len(), - FFINetwork::Testnet, - error, - ) - }; - assert!(!source_wallet.is_null()); - - // 2. Get xpub - let xpub = unsafe { - key_wallet_ffi::wallet::wallet_get_xpub(source_wallet, FFINetwork::Testnet, 0, error) - }; - assert!(!xpub.is_null()); - - // 3. Create watch-only wallet from xpub - let watch_wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_xpub(xpub, FFINetwork::Testnet, error) - }; - assert!(!watch_wallet.is_null()); - - // 4. Verify it's watch-only - let is_watch_only = - unsafe { key_wallet_ffi::wallet::wallet_is_watch_only(watch_wallet, error) }; - assert!(is_watch_only); - - // 5. Verify regular wallet is not watch-only - let is_watch_only = - unsafe { key_wallet_ffi::wallet::wallet_is_watch_only(source_wallet, error) }; - assert!(!is_watch_only); - - // 6. Since we can't derive addresses directly from wallets anymore, - // we'll just test that both wallets exist and have correct properties - - // Clean up - unsafe { - key_wallet_ffi::wallet::wallet_free(source_wallet); - key_wallet_ffi::wallet::wallet_free(watch_wallet); - key_wallet_ffi::utils::string_free(xpub); - } -} - #[test] fn test_derivation_paths() { let mut error = FFIError::success(); diff --git a/key-wallet-ffi/tests/test_account_collection.rs b/key-wallet-ffi/tests/test_account_collection.rs new file mode 100644 index 000000000..47c277f88 --- /dev/null +++ b/key-wallet-ffi/tests/test_account_collection.rs @@ -0,0 +1,196 @@ +//! Integration tests for account collection FFI functions + +use key_wallet_ffi::account::account_free; +use key_wallet_ffi::account_collection::*; +use key_wallet_ffi::types::{ + FFIAccountCreationOptionType, FFINetwork, FFIWalletAccountCreationOptions, +}; +use key_wallet_ffi::wallet::{wallet_create_from_mnemonic_with_options, wallet_free}; +use std::ffi::CString; +use std::ptr; + +#[test] +fn test_account_collection_comprehensive() { + unsafe { + // Create a test mnemonic + let mnemonic = CString::new( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + ).unwrap(); + + // Create wallet with various account types + let account_options = FFIWalletAccountCreationOptions { + option_type: FFIAccountCreationOptionType::AllAccounts, + bip44_indices: [0, 1, 2].as_ptr(), + bip44_count: 3, + bip32_indices: [0].as_ptr(), + bip32_count: 1, + coinjoin_indices: [0, 1].as_ptr(), + coinjoin_count: 2, + topup_indices: [0, 1, 2].as_ptr(), + topup_count: 3, + special_account_types: ptr::null(), + special_account_types_count: 0, + }; + + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + FFINetwork::Testnet, + &account_options, + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection for testnet + let collection = + wallet_get_account_collection(wallet, FFINetwork::Testnet, ptr::null_mut()); + assert!(!collection.is_null()); + + // Test account count + let count = account_collection_count(collection); + assert!(count > 0, "Should have at least some accounts"); + + // Test BIP44 accounts + let mut bip44_indices: *mut u32 = ptr::null_mut(); + let mut bip44_count: usize = 0; + let success = + account_collection_get_bip44_indices(collection, &mut bip44_indices, &mut bip44_count); + assert!(success); + assert_eq!(bip44_count, 3, "Should have 3 BIP44 accounts"); + + // Get each BIP44 account + for i in 0..3 { + let account = account_collection_get_bip44_account(collection, i); + assert!(!account.is_null(), "BIP44 account {} should exist", i); + account_free(account); + } + + // Test BIP32 accounts + let mut bip32_indices: *mut u32 = ptr::null_mut(); + let mut bip32_count: usize = 0; + let success = + account_collection_get_bip32_indices(collection, &mut bip32_indices, &mut bip32_count); + assert!(success); + assert_eq!(bip32_count, 1, "Should have 1 BIP32 account"); + + let bip32_account = account_collection_get_bip32_account(collection, 0); + assert!(!bip32_account.is_null()); + account_free(bip32_account); + + // Test CoinJoin accounts + let mut coinjoin_indices: *mut u32 = ptr::null_mut(); + let mut coinjoin_count: usize = 0; + let success = account_collection_get_coinjoin_indices( + collection, + &mut coinjoin_indices, + &mut coinjoin_count, + ); + assert!(success); + assert_eq!(coinjoin_count, 2, "Should have 2 CoinJoin accounts"); + + // Test special accounts existence + assert!(account_collection_has_identity_registration(collection)); + assert!(account_collection_has_identity_invitation(collection)); + assert!(account_collection_has_provider_voting_keys(collection)); + assert!(account_collection_has_provider_owner_keys(collection)); + + // Test getting special accounts + let identity_reg = account_collection_get_identity_registration(collection); + assert!(!identity_reg.is_null()); + account_free(identity_reg); + + let identity_inv = account_collection_get_identity_invitation(collection); + assert!(!identity_inv.is_null()); + account_free(identity_inv); + + // Test identity topup accounts + let mut topup_indices: *mut u32 = ptr::null_mut(); + let mut topup_count: usize = 0; + let success = account_collection_get_identity_topup_indices( + collection, + &mut topup_indices, + &mut topup_count, + ); + assert!(success); + assert_eq!(topup_count, 3, "Should have 3 identity topup accounts"); + + // Get each topup account + for i in 0..3 { + let topup = account_collection_get_identity_topup(collection, i); + assert!(!topup.is_null(), "Identity topup {} should exist", i); + account_free(topup); + } + + // Clean up arrays + if !bip44_indices.is_null() { + free_u32_array(bip44_indices, bip44_count); + } + if !bip32_indices.is_null() { + free_u32_array(bip32_indices, bip32_count); + } + if !coinjoin_indices.is_null() { + free_u32_array(coinjoin_indices, coinjoin_count); + } + if !topup_indices.is_null() { + free_u32_array(topup_indices, topup_count); + } + + // Clean up + account_collection_free(collection); + wallet_free(wallet); + } +} + +#[test] +fn test_account_collection_minimal() { + unsafe { + // Create a test mnemonic + let mnemonic = CString::new( + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + ).unwrap(); + + // Create wallet with minimal accounts (default) + let wallet = wallet_create_from_mnemonic_with_options( + mnemonic.as_ptr(), + ptr::null(), + FFINetwork::Testnet, + ptr::null(), // Use default options + ptr::null_mut(), + ); + assert!(!wallet.is_null()); + + // Get account collection + let collection = + wallet_get_account_collection(wallet, FFINetwork::Testnet, ptr::null_mut()); + assert!(!collection.is_null()); + + // Should have at least some default accounts + let count = account_collection_count(collection); + assert!(count > 0, "Default wallet should have some accounts"); + + // Check for BIP44 account 0 (should exist by default) + let account0 = account_collection_get_bip44_account(collection, 0); + assert!(!account0.is_null(), "Default wallet should have BIP44 account 0"); + account_free(account0); + + // Clean up + account_collection_free(collection); + wallet_free(wallet); + } +} + +#[test] +fn test_account_collection_null_safety() { + unsafe { + // Test null safety of various functions + assert_eq!(account_collection_count(ptr::null()), 0); + assert!(!account_collection_has_identity_registration(ptr::null())); + assert!(!account_collection_has_identity_invitation(ptr::null())); + assert!(account_collection_get_bip44_account(ptr::null(), 0).is_null()); + assert!(account_collection_get_identity_registration(ptr::null()).is_null()); + + // Test free with null (should not crash) + account_collection_free(ptr::null_mut()); + free_u32_array(ptr::null_mut(), 0); + } +} diff --git a/key-wallet-ffi/tests/test_import_wallet.rs b/key-wallet-ffi/tests/test_import_wallet.rs new file mode 100644 index 000000000..5b211d51d --- /dev/null +++ b/key-wallet-ffi/tests/test_import_wallet.rs @@ -0,0 +1,74 @@ +//! Test for wallet import from bytes via FFI + +#[cfg(feature = "bincode")] +#[cfg(test)] +mod tests { + use key_wallet_ffi::error::{FFIError, FFIErrorCode}; + use key_wallet_ffi::types::FFINetwork; + use key_wallet_ffi::wallet_manager::*; + use std::ptr; + + #[test] + fn test_import_wallet_from_bytes() { + unsafe { + // Create a wallet manager + let mut error = FFIError::success(); + let manager = wallet_manager_create(&mut error); + assert_eq!(error.code, FFIErrorCode::Success); + assert!(!manager.is_null()); + + // First, create a wallet from mnemonic + let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about\0"; + let passphrase = "\0"; + + let success = wallet_manager_add_wallet_from_mnemonic( + manager, + mnemonic.as_ptr() as *const i8, + passphrase.as_ptr() as *const i8, + FFINetwork::Dash, + &mut error, + ); + assert!(success); + assert_eq!(error.code, FFIErrorCode::Success); + + // Get the wallet for serialization + let mut wallet_ids_ptr: *mut u8 = ptr::null_mut(); + let mut count: usize = 0; + let success = + wallet_manager_get_wallet_ids(manager, &mut wallet_ids_ptr, &mut count, &mut error); + assert!(success); + assert_eq!(count, 1); + assert!(!wallet_ids_ptr.is_null()); + + // Get the wallet + let wallet_ptr = wallet_manager_get_wallet(manager, wallet_ids_ptr, &mut error); + assert!(!wallet_ptr.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Now we would serialize the wallet to bytes here if we had that functionality exposed + // For now, we'll just test that the import function exists and compiles + + // Create a second manager to test import + let manager2 = wallet_manager_create(&mut error); + assert_eq!(error.code, FFIErrorCode::Success); + assert!(!manager2.is_null()); + + // Test with invalid input (null bytes) + let mut imported_wallet_id = [0u8; 32]; + let success = wallet_manager_import_wallet_from_bytes( + manager2, + ptr::null(), + 0, + imported_wallet_id.as_mut_ptr(), + &mut error, + ); + assert!(!success); + assert_eq!(error.code, FFIErrorCode::InvalidInput); + + // Clean up + wallet_manager_free_wallet_ids(wallet_ids_ptr, count); + wallet_manager_free(manager); + wallet_manager_free(manager2); + } + } +} diff --git a/key-wallet-ffi/tests/test_improved_watch_only.rs b/key-wallet-ffi/tests/test_improved_watch_only.rs deleted file mode 100644 index f1de12389..000000000 --- a/key-wallet-ffi/tests/test_improved_watch_only.rs +++ /dev/null @@ -1,57 +0,0 @@ -#[test] -fn test_improved_watch_only_wallet_creation() { - use key_wallet_ffi::error::{FFIError, FFIErrorCode}; - use key_wallet_ffi::types::FFINetwork; - - let mut error = FFIError::success(); - let error = &mut error as *mut FFIError; - - // 1. Create a regular wallet to get an xpub - let seed = vec![0x01u8; 64]; - let source_wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_seed( - seed.as_ptr(), - seed.len(), - FFINetwork::Testnet, - error, - ) - }; - assert!(!source_wallet.is_null()); - - // 2. Get xpub from the regular wallet - let xpub = unsafe { - key_wallet_ffi::wallet::wallet_get_xpub(source_wallet, FFINetwork::Testnet, 0, error) - }; - assert!(!xpub.is_null()); - - // 3. Create a watch-only wallet using the improved implementation - // This now properly creates an AccountCollection with account 0 - let watch_wallet = unsafe { - key_wallet_ffi::wallet::wallet_create_from_xpub(xpub, FFINetwork::Testnet, error) - }; - assert!(!watch_wallet.is_null()); - assert_eq!(unsafe { (*error).code }, FFIErrorCode::Success); - - // 4. Create wallet managers to derive addresses - let source_manager = key_wallet_ffi::wallet_manager::wallet_manager_create(error); - assert!(!source_manager.is_null()); - - let watch_manager = key_wallet_ffi::wallet_manager::wallet_manager_create(error); - assert!(!watch_manager.is_null()); - - // 5. Test that we can create watch-only wallets from xpub - // The wallet manager doesn't support adding wallets from xpub directly, - // but we've verified that wallet_create_from_xpub works correctly - - println!("✅ Watch-only wallet properly created with AccountCollection!"); - println!(" Watch-only wallet can be created from xpub"); - - // Clean up - unsafe { - key_wallet_ffi::wallet::wallet_free(source_wallet); - key_wallet_ffi::wallet::wallet_free(watch_wallet); - key_wallet_ffi::utils::string_free(xpub); - key_wallet_ffi::wallet_manager::wallet_manager_free(source_manager); - key_wallet_ffi::wallet_manager::wallet_manager_free(watch_manager); - } -} diff --git a/key-wallet-ffi/tests/test_managed_account_collection.rs b/key-wallet-ffi/tests/test_managed_account_collection.rs new file mode 100644 index 000000000..8c65456a0 --- /dev/null +++ b/key-wallet-ffi/tests/test_managed_account_collection.rs @@ -0,0 +1,561 @@ +//! Tests for managed account collection FFI bindings + +use key_wallet_ffi::error::{FFIError, FFIErrorCode}; +use key_wallet_ffi::managed_account_collection::*; +use key_wallet_ffi::types::{ + FFIAccountCreationOptionType, FFINetwork, FFIWalletAccountCreationOptions, +}; +use key_wallet_ffi::wallet_manager::{ + wallet_manager_add_wallet_from_mnemonic_with_options, wallet_manager_create, + wallet_manager_free, wallet_manager_free_wallet_ids, wallet_manager_get_wallet_ids, +}; +use std::ffi::CString; +use std::ptr; + +const TEST_MNEMONIC: &str = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + +#[test] +fn test_managed_account_collection_basic() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Add a wallet with default accounts + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), // Use default options + &mut error, + ); + assert!(success); + assert_eq!(error.code, FFIErrorCode::Success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = + wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); + assert!(success); + assert_eq!(count_out, 1); + assert!(!wallet_ids_out.is_null()); + + // Get the managed account collection + let collection = managed_wallet_get_account_collection( + manager, + wallet_ids_out, + FFINetwork::Testnet, + &mut error, + ); + assert!(!collection.is_null()); + assert_eq!(error.code, FFIErrorCode::Success); + + // Check that we have some accounts + let count = managed_account_collection_count(collection); + assert!(count > 0); + + // Check BIP44 accounts + let mut indices: *mut std::os::raw::c_uint = ptr::null_mut(); + let mut indices_count: usize = 0; + let success = managed_account_collection_get_bip44_indices( + collection, + &mut indices, + &mut indices_count, + ); + assert!(success); + assert!(indices_count > 0); + + // Get first BIP44 account + let account = managed_account_collection_get_bip44_account(collection, 0); + assert!(!account.is_null()); + + // Clean up + key_wallet_ffi::managed_account::managed_account_free(account); + if !indices.is_null() { + key_wallet_ffi::account_collection::free_u32_array(indices, indices_count); + } + managed_account_collection_free(collection); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } +} + +#[test] +fn test_managed_account_collection_with_special_accounts() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Create wallet with special accounts + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut options = FFIWalletAccountCreationOptions::default_options(); + options.option_type = FFIAccountCreationOptionType::AllAccounts; + + // Add various special accounts + let special_types = [ + key_wallet_ffi::types::FFIAccountType::ProviderVotingKeys, + key_wallet_ffi::types::FFIAccountType::ProviderOwnerKeys, + key_wallet_ffi::types::FFIAccountType::IdentityRegistration, + key_wallet_ffi::types::FFIAccountType::IdentityInvitation, + ]; + options.special_account_types = special_types.as_ptr(); + options.special_account_types_count = special_types.len(); + + // Configure standard accounts + let bip44_indices = [0, 4, 5, 8]; + let bip32_indices = [0]; + let coinjoin_indices = [0, 1]; + let topup_indices = [0, 1, 2]; + + options.bip44_indices = bip44_indices.as_ptr(); + options.bip44_count = bip44_indices.len(); + + options.bip32_indices = bip32_indices.as_ptr(); + options.bip32_count = bip32_indices.len(); + + options.coinjoin_indices = coinjoin_indices.as_ptr(); + options.coinjoin_count = coinjoin_indices.len(); + + options.topup_indices = topup_indices.as_ptr(); + options.topup_count = topup_indices.len(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + &options, + &mut error, + ); + assert!(success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = + wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); + assert!(success); + assert_eq!(count_out, 1); + + // Get the managed account collection + let collection = managed_wallet_get_account_collection( + manager, + wallet_ids_out, + FFINetwork::Testnet, + &mut error, + ); + assert!(!collection.is_null()); + + // Verify BIP44 accounts + let mut indices: *mut std::os::raw::c_uint = ptr::null_mut(); + let mut indices_count: usize = 0; + let success = managed_account_collection_get_bip44_indices( + collection, + &mut indices, + &mut indices_count, + ); + assert!(success); + assert_eq!(indices_count, 4); + if !indices.is_null() { + key_wallet_ffi::account_collection::free_u32_array(indices, indices_count); + } + + // Verify BIP32 accounts + let success = managed_account_collection_get_bip32_indices( + collection, + &mut indices, + &mut indices_count, + ); + assert!(success); + assert_eq!(indices_count, 1); + if !indices.is_null() { + key_wallet_ffi::account_collection::free_u32_array(indices, indices_count); + } + + // Verify CoinJoin accounts + let success = managed_account_collection_get_coinjoin_indices( + collection, + &mut indices, + &mut indices_count, + ); + assert!(success); + assert_eq!(indices_count, 2); + if !indices.is_null() { + key_wallet_ffi::account_collection::free_u32_array(indices, indices_count); + } + + // Check special accounts existence + assert!(managed_account_collection_has_identity_registration(collection)); + assert!(managed_account_collection_has_identity_invitation(collection)); + assert!(managed_account_collection_has_provider_voting_keys(collection)); + assert!(managed_account_collection_has_provider_owner_keys(collection)); + + // Get specific accounts + let identity_reg = managed_account_collection_get_identity_registration(collection); + assert!(!identity_reg.is_null()); + key_wallet_ffi::managed_account::managed_account_free(identity_reg); + + let voting_keys = managed_account_collection_get_provider_voting_keys(collection); + assert!(!voting_keys.is_null()); + key_wallet_ffi::managed_account::managed_account_free(voting_keys); + + // Clean up + managed_account_collection_free(collection); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } +} + +#[test] +fn test_managed_account_collection_summary() { + unsafe { + use std::ffi::CStr; + + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Create wallet with multiple account types + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut options = FFIWalletAccountCreationOptions::default_options(); + options.option_type = FFIAccountCreationOptionType::AllAccounts; + + // Add various special accounts + let special_types = [ + key_wallet_ffi::types::FFIAccountType::ProviderVotingKeys, + key_wallet_ffi::types::FFIAccountType::ProviderOwnerKeys, + key_wallet_ffi::types::FFIAccountType::IdentityRegistration, + ]; + options.special_account_types = special_types.as_ptr(); + options.special_account_types_count = special_types.len(); + + // Configure standard accounts + let bip44_indices = [0, 1, 2]; + let bip32_indices = [0]; + + options.bip44_indices = bip44_indices.as_ptr(); + options.bip44_count = bip44_indices.len(); + + options.bip32_indices = bip32_indices.as_ptr(); + options.bip32_count = bip32_indices.len(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + &options, + &mut error, + ); + assert!(success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = + wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); + assert!(success); + assert_eq!(count_out, 1); + + // Get the managed account collection + let collection = managed_wallet_get_account_collection( + manager, + wallet_ids_out, + FFINetwork::Testnet, + &mut error, + ); + assert!(!collection.is_null()); + + // Get the summary + let summary_ptr = managed_account_collection_summary(collection); + assert!(!summary_ptr.is_null()); + + // Convert to Rust string to verify content + let summary_cstr = CStr::from_ptr(summary_ptr); + let summary = summary_cstr.to_str().unwrap(); + + // Verify the summary contains expected content + assert!(summary.contains("Managed Account Summary:")); + assert!(summary.contains("BIP44 Accounts")); + assert!(summary.contains("BIP32 Accounts")); + assert!(summary.contains("Identity Registration Account")); + assert!(summary.contains("Provider Voting Keys Account")); + assert!(summary.contains("Provider Owner Keys Account")); + + // Clean up + key_wallet_ffi::utils::string_free(summary_ptr); + managed_account_collection_free(collection); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } +} + +#[test] +fn test_managed_account_collection_summary_data() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Create wallet with various account types + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let mut options = FFIWalletAccountCreationOptions::default_options(); + options.option_type = FFIAccountCreationOptionType::AllAccounts; + + // Add various special accounts + let special_types = [ + key_wallet_ffi::types::FFIAccountType::IdentityRegistration, + key_wallet_ffi::types::FFIAccountType::IdentityInvitation, + ]; + options.special_account_types = special_types.as_ptr(); + options.special_account_types_count = special_types.len(); + + // Configure standard accounts + let bip44_indices = [0, 1, 2, 5]; + let bip32_indices = [0]; + let coinjoin_indices = [0, 1]; + let topup_indices = [0, 1, 2]; + + options.bip44_indices = bip44_indices.as_ptr(); + options.bip44_count = bip44_indices.len(); + + options.bip32_indices = bip32_indices.as_ptr(); + options.bip32_count = bip32_indices.len(); + + options.coinjoin_indices = coinjoin_indices.as_ptr(); + options.coinjoin_count = coinjoin_indices.len(); + + options.topup_indices = topup_indices.as_ptr(); + options.topup_count = topup_indices.len(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + &options, + &mut error, + ); + assert!(success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = + wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); + assert!(success); + assert_eq!(count_out, 1); + + // Get the managed account collection + let collection = managed_wallet_get_account_collection( + manager, + wallet_ids_out, + FFINetwork::Testnet, + &mut error, + ); + assert!(!collection.is_null()); + + // Get the summary data + let summary = managed_account_collection_summary_data(collection); + assert!(!summary.is_null()); + + let summary_ref = &*summary; + + // Verify BIP44 indices + assert_eq!(summary_ref.bip44_count, 4); + assert!(!summary_ref.bip44_indices.is_null()); + let bip44_slice = + std::slice::from_raw_parts(summary_ref.bip44_indices, summary_ref.bip44_count); + assert_eq!(bip44_slice, &[0, 1, 2, 5]); + + // Verify BIP32 indices + assert_eq!(summary_ref.bip32_count, 1); + assert!(!summary_ref.bip32_indices.is_null()); + + // Verify CoinJoin indices + assert_eq!(summary_ref.coinjoin_count, 2); + assert!(!summary_ref.coinjoin_indices.is_null()); + + // Verify identity topup indices + assert_eq!(summary_ref.identity_topup_count, 3); + assert!(!summary_ref.identity_topup_indices.is_null()); + + // Verify boolean flags + assert!(summary_ref.has_identity_registration); + assert!(summary_ref.has_identity_invitation); + + // Clean up + managed_account_collection_summary_free(summary); + managed_account_collection_free(collection); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } +} + +#[test] +fn test_managed_account_collection_null_safety() { + unsafe { + let mut error = FFIError::success(); + + // Test with null manager + let collection = managed_wallet_get_account_collection( + ptr::null(), + ptr::null(), + FFINetwork::Testnet, + &mut error, + ); + assert!(collection.is_null()); + assert_eq!(error.code, FFIErrorCode::InvalidInput); + + // Test with null collection for various functions + assert_eq!(managed_account_collection_count(ptr::null()), 0); + assert!(!managed_account_collection_has_identity_registration(ptr::null())); + assert!(managed_account_collection_get_bip44_account(ptr::null(), 0).is_null()); + assert!(managed_account_collection_summary(ptr::null()).is_null()); + assert!(managed_account_collection_summary_data(ptr::null()).is_null()); + + // Test free with null (should not crash) + managed_account_collection_free(ptr::null_mut()); + managed_account_collection_summary_free(ptr::null_mut()); + } +} + +#[test] +fn test_managed_account_collection_nonexistent_accounts() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Create wallet with minimal accounts + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), // Default options + &mut error, + ); + assert!(success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = + wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); + assert!(success); + assert_eq!(count_out, 1); + + // Get the managed account collection + let collection = managed_wallet_get_account_collection( + manager, + wallet_ids_out, + FFINetwork::Testnet, + &mut error, + ); + assert!(!collection.is_null()); + + // Try to get non-existent accounts + let account = managed_account_collection_get_bip44_account(collection, 999); + assert!(account.is_null()); + + let account = managed_account_collection_get_bip32_account(collection, 999); + assert!(account.is_null()); + + let account = managed_account_collection_get_coinjoin_account(collection, 999); + assert!(account.is_null()); + + let account = managed_account_collection_get_identity_topup(collection, 999); + assert!(account.is_null()); + + // Clean up + managed_account_collection_free(collection); + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } +} + +#[test] +fn test_managed_account_collection_wrong_network() { + unsafe { + let mut error = FFIError::success(); + + // Create wallet manager + let manager = wallet_manager_create(&mut error); + assert!(!manager.is_null()); + + // Create wallet on testnet + let mnemonic = CString::new(TEST_MNEMONIC).unwrap(); + let passphrase = CString::new("").unwrap(); + + let success = wallet_manager_add_wallet_from_mnemonic_with_options( + manager, + mnemonic.as_ptr(), + passphrase.as_ptr(), + FFINetwork::Testnet, + ptr::null(), + &mut error, + ); + assert!(success); + + // Get wallet IDs + let mut wallet_ids_out: *mut u8 = ptr::null_mut(); + let mut count_out: usize = 0; + + let success = + wallet_manager_get_wallet_ids(manager, &mut wallet_ids_out, &mut count_out, &mut error); + assert!(success); + assert_eq!(count_out, 1); + + // Try to get collection for mainnet (should fail or return empty) + let collection = managed_wallet_get_account_collection( + manager, + wallet_ids_out, + FFINetwork::Dash, // Wrong network (mainnet) + &mut error, + ); + + // Should either be null or have no accounts + if !collection.is_null() { + let count = managed_account_collection_count(collection); + assert_eq!(count, 0); + managed_account_collection_free(collection); + } + + // Clean up + wallet_manager_free_wallet_ids(wallet_ids_out, count_out); + wallet_manager_free(manager); + } +} diff --git a/key-wallet-ffi/tests/test_valid_addr.rs b/key-wallet-ffi/tests/test_valid_addr.rs index e8c84edf0..cea0b72de 100644 --- a/key-wallet-ffi/tests/test_valid_addr.rs +++ b/key-wallet-ffi/tests/test_valid_addr.rs @@ -11,7 +11,7 @@ fn test_valid_testnet_address() { Mnemonic::from_phrase(mnemonic_str, key_wallet::mnemonic::Language::English).unwrap(); let wallet = - Wallet::from_mnemonic(mnemonic, Network::Testnet, WalletAccountCreationOptions::Default) + Wallet::from_mnemonic(mnemonic, &[Network::Testnet], WalletAccountCreationOptions::Default) .unwrap(); if let Some(account) = wallet.get_bip44_account(Network::Testnet, 0) { diff --git a/key-wallet-manager/Cargo.toml b/key-wallet-manager/Cargo.toml index ca3d611c2..eeabee5f7 100644 --- a/key-wallet-manager/Cargo.toml +++ b/key-wallet-manager/Cargo.toml @@ -9,10 +9,11 @@ readme = "README.md" license = "CC0-1.0" [features] -default = ["std"] +default = ["std", "bincode"] std = ["key-wallet/std", "dashcore/std", "dashcore_hashes/std", "secp256k1/std"] serde = ["dep:serde", "key-wallet/serde", "dashcore/serde"] getrandom = ["key-wallet/getrandom"] +bincode = ["dep:bincode","key-wallet/bincode"] [dependencies] key-wallet = { path = "../key-wallet", default-features = false } @@ -21,6 +22,8 @@ dashcore_hashes = { path = "../hashes", default-features = false } secp256k1 = { version = "0.30.0", default-features = false, features = ["recovery"] } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } async-trait = "0.1" +bincode = { version = "=2.0.0-rc.3", optional = true } +zeroize = { version = "1.8", features = ["derive"] } [dev-dependencies] hex = "0.4" diff --git a/key-wallet-manager/examples/wallet_creation.rs b/key-wallet-manager/examples/wallet_creation.rs index cc115da36..5680a0f63 100644 --- a/key-wallet-manager/examples/wallet_creation.rs +++ b/key-wallet-manager/examples/wallet_creation.rs @@ -13,7 +13,7 @@ use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePr use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::{AccountType, Network}; use key_wallet_manager::spv_wallet_manager::SPVWalletManager; -use key_wallet_manager::wallet_manager::{WalletId, WalletManager}; +use key_wallet_manager::wallet_manager::WalletManager; fn main() { println!("=== Wallet Creation Example ===\n"); @@ -23,54 +23,48 @@ fn main() { let mut manager = WalletManager::::new(); - // Create a wallet ID (32 bytes) - let wallet_id: WalletId = [1u8; 32]; - - let result = manager.create_wallet( - wallet_id, - "My First Wallet".to_string(), + let result = manager.create_wallet_with_random_mnemonic( WalletAccountCreationOptions::Default, Network::Testnet, ); - match result { - Ok(_) => { + let wallet_id = match result { + Ok(wallet_id) => { println!("✅ Wallet created successfully!"); println!(" Wallet ID: {}", hex::encode(wallet_id)); println!(" Total wallets: {}", manager.wallet_count()); + wallet_id } Err(e) => { println!("❌ Failed to create wallet: {:?}", e); return; } - } + }; // Example 2: Create wallet from mnemonic println!("\n2. Creating wallet from mnemonic..."); let test_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; - let wallet_id2: WalletId = [2u8; 32]; - let result = manager.create_wallet_from_mnemonic( - wallet_id2, - "Restored Wallet".to_string(), test_mnemonic, "", // No passphrase - Some(Network::Testnet), + &[Network::Testnet], Some(100_000), // Birth height key_wallet::wallet::initialization::WalletAccountCreationOptions::Default, ); - match result { - Ok(_) => { + let wallet_id2 = match result { + Ok(wallet_id2) => { println!("✅ Wallet created from mnemonic!"); println!(" Wallet ID: {}", hex::encode(wallet_id2)); + wallet_id2 } Err(e) => { println!("❌ Failed to create wallet from mnemonic: {:?}", e); + return; } - } + }; // Example 3: Managing accounts println!("\n3. Managing wallet accounts..."); @@ -135,19 +129,16 @@ fn main() { let mut spv_manager = SPVWalletManager::with_base(WalletManager::::new()); - let wallet_id3: WalletId = [3u8; 32]; - // Create a wallet through SPVWalletManager - let spv_result = spv_manager.base.create_wallet( - wallet_id3, - "SPV Wallet".to_string(), + let spv_result = spv_manager.base.create_wallet_with_random_mnemonic( WalletAccountCreationOptions::Default, Network::Testnet, ); match spv_result { - Ok(_) => { + Ok(wallet_id3) => { println!("✅ SPV wallet created!"); + println!(" Wallet ID: {}", hex::encode(wallet_id3)); println!(" Sync status: {:?}", spv_manager.sync_status(Network::Testnet)); println!(" Sync height: {}", spv_manager.sync_height(Network::Testnet)); diff --git a/key-wallet-manager/src/wallet_manager/mod.rs b/key-wallet-manager/src/wallet_manager/mod.rs index 20a6518b1..4ca8d7bc8 100644 --- a/key-wallet-manager/src/wallet_manager/mod.rs +++ b/key-wallet-manager/src/wallet_manager/mod.rs @@ -12,16 +12,17 @@ use alloc::string::String; use alloc::vec::Vec; use dashcore::blockdata::transaction::Transaction; use dashcore::Txid; +use key_wallet::transaction_checking::TransactionContext; +use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; use key_wallet::wallet::managed_wallet_info::{ManagedWalletInfo, TransactionRecord}; +use key_wallet::wallet::WalletType; use key_wallet::{Account, AccountType, Address, ExtendedPrivKey, Mnemonic, Network, Wallet}; use key_wallet::{ExtendedPubKey, WalletBalance}; +use key_wallet::{Utxo, UtxoSet}; use std::collections::BTreeSet; use std::str::FromStr; - -use key_wallet::transaction_checking::TransactionContext; -use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; -use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; -use key_wallet::{Utxo, UtxoSet}; +use zeroize::Zeroize; /// Unique identifier for a wallet (32-byte hash) pub type WalletId = [u8; 32]; @@ -109,45 +110,44 @@ impl WalletManager { } /// Create a new wallet from mnemonic and add it to the manager - #[allow(clippy::too_many_arguments)] + /// Returns the computed wallet ID pub fn create_wallet_from_mnemonic( &mut self, - wallet_id: WalletId, - name: String, mnemonic: &str, passphrase: &str, - network: Option, + networks: &[Network], birth_height: Option, account_creation_options: key_wallet::wallet::initialization::WalletAccountCreationOptions, - ) -> Result<&T, WalletError> { - if self.wallets.contains_key(&wallet_id) { - return Err(WalletError::WalletExists(wallet_id)); - } - - let network = network - .ok_or(WalletError::InvalidParameter("Network must be specified".to_string()))?; - + ) -> Result { let mnemonic_obj = Mnemonic::from_phrase(mnemonic, key_wallet::mnemonic::Language::English) .map_err(|e| WalletError::InvalidMnemonic(e.to_string()))?; // Use appropriate wallet creation method based on whether a passphrase is provided let wallet = if passphrase.is_empty() { - Wallet::from_mnemonic(mnemonic_obj, network, account_creation_options) + Wallet::from_mnemonic(mnemonic_obj, networks, account_creation_options) .map_err(|e| WalletError::WalletCreation(e.to_string()))? } else { // For wallets with passphrase, use the provided options Wallet::from_mnemonic_with_passphrase( mnemonic_obj, passphrase.to_string(), - network, + networks, account_creation_options, ) .map_err(|e| WalletError::WalletCreation(e.to_string()))? }; + // Compute wallet ID from the wallet's root public key + let wallet_id = wallet.compute_wallet_id(); + + // Check if wallet already exists + if self.wallets.contains_key(&wallet_id) { + return Err(WalletError::WalletExists(wallet_id)); + } + // Create managed wallet info from the wallet to properly initialize accounts // This ensures the ManagedAccountCollection is synchronized with the Wallet's accounts - let mut managed_info = T::from_wallet_with_name(&wallet, name); + let mut managed_info = T::from_wallet(&wallet); managed_info.set_birth_height(birth_height); managed_info.set_first_loaded_at(current_timestamp()); @@ -161,40 +161,147 @@ impl WalletManager { self.wallets.insert(wallet_id, wallet_mut); self.wallet_infos.insert(wallet_id, managed_info); - Ok(self.wallet_infos.get(&wallet_id).unwrap()) + Ok(wallet_id) } - /// Create a new empty wallet and add it to the manager - pub fn create_wallet( + /// Create a wallet from mnemonic and return it as serialized bytes + /// + /// This function creates a wallet from a mnemonic phrase and returns it as bincode-serialized bytes. + /// It supports downgrading to a public-key-only wallet for security purposes. + /// + /// # Arguments + /// * `mnemonic` - The mnemonic phrase + /// * `passphrase` - Optional BIP39 passphrase (empty string for no passphrase) + /// * `networks` - The networks for the wallet + /// * `birth_height` - Optional birth height for wallet scanning + /// * `account_creation_options` - Which accounts to create initially + /// * `downgrade_to_pubkey_wallet` - If true, creates a wallet without private keys + /// * `allow_external_signing` - If true and downgraded, creates an externally signable wallet (e.g., for hardware wallets) + /// + /// # Returns + /// A tuple containing: + /// * The serialized wallet bytes + /// * The wallet ID + /// + /// # Security Note + /// When `downgrade_to_pubkey_wallet` is true, the returned wallet contains NO private key material, + /// making it safe to use on potentially compromised systems or for creating watch-only wallets. + #[cfg(feature = "bincode")] + #[allow(clippy::too_many_arguments)] + pub fn create_wallet_from_mnemonic_return_serialized_bytes( &mut self, - wallet_id: WalletId, - name: String, + mnemonic: &str, + passphrase: &str, + networks: &[Network], + birth_height: Option, account_creation_options: key_wallet::wallet::initialization::WalletAccountCreationOptions, - network: Network, - ) -> Result<&T, WalletError> { + downgrade_to_pubkey_wallet: bool, + allow_external_signing: bool, + ) -> Result<(Vec, WalletId), WalletError> { + let mnemonic_obj = Mnemonic::from_phrase(mnemonic, key_wallet::mnemonic::Language::English) + .map_err(|e| WalletError::InvalidMnemonic(e.to_string()))?; + + // Create the initial wallet from mnemonic + let mut wallet = if passphrase.is_empty() { + Wallet::from_mnemonic(mnemonic_obj, networks, account_creation_options) + .map_err(|e| WalletError::WalletCreation(e.to_string()))? + } else { + Wallet::from_mnemonic_with_passphrase( + mnemonic_obj, + passphrase.to_string(), + networks, + account_creation_options, + ) + .map_err(|e| WalletError::WalletCreation(e.to_string()))? + }; + + // Downgrade to pubkey-only wallet if requested + let final_wallet = if downgrade_to_pubkey_wallet { + // Extract the public key and accounts from the full wallet + let root_xpub = wallet.root_extended_pub_key(); + + // Copy the accounts structure (but without private keys) + let accounts = wallet.accounts.clone(); + + let wallet_type = if allow_external_signing { + WalletType::ExternalSignable(root_xpub) + } else { + WalletType::WatchOnly(root_xpub) + }; + // Create a new wallet with only public keys + let pubkey_wallet = Wallet { + wallet_id: wallet.wallet_id, + wallet_type, + accounts, + }; + + // Zeroize the wallet containing private keys before dropping + wallet.zeroize(); + drop(wallet); + + pubkey_wallet + } else { + wallet + }; + + // Compute wallet ID + let wallet_id = final_wallet.compute_wallet_id(); + + // Check if wallet already exists if self.wallets.contains_key(&wallet_id) { return Err(WalletError::WalletExists(wallet_id)); } - // For now, create a wallet with a fixed test mnemonic - // In production, you'd generate a random mnemonic or use new_random with proper features - let test_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + // Serialize the wallet to bytes + let serialized_bytes = bincode::encode_to_vec(&final_wallet, bincode::config::standard()) + .map_err(|e| { + WalletError::InvalidParameter(format!("Failed to serialize wallet: {}", e)) + })?; + + // Add the wallet to the manager + let mut managed_info = T::from_wallet(&final_wallet); + managed_info.set_birth_height(birth_height); + managed_info.set_first_loaded_at(current_timestamp()); + + self.wallets.insert(wallet_id, final_wallet); + self.wallet_infos.insert(wallet_id, managed_info); + + Ok((serialized_bytes, wallet_id)) + } + + /// Create a new wallet with a random mnemonic and add it to the manager + /// Returns the generated wallet ID + pub fn create_wallet_with_random_mnemonic( + &mut self, + account_creation_options: key_wallet::wallet::initialization::WalletAccountCreationOptions, + network: Network, + ) -> Result { + // Generate a random mnemonic (24 words for maximum security) let mnemonic = - Mnemonic::from_phrase(test_mnemonic, key_wallet::mnemonic::Language::English) - .map_err(|e| WalletError::WalletCreation(e.to_string()))?; + Mnemonic::generate(24, key_wallet::mnemonic::Language::English).map_err(|e| { + WalletError::WalletCreation(format!("Failed to generate mnemonic: {}", e)) + })?; - let wallet = Wallet::from_mnemonic(mnemonic, network, account_creation_options) + let wallet = Wallet::from_mnemonic(mnemonic, &[network], account_creation_options) .map_err(|e| WalletError::WalletCreation(e.to_string()))?; + // Compute wallet ID from the wallet's root public key + let wallet_id = wallet.compute_wallet_id(); + + // Check if wallet already exists + if self.wallets.contains_key(&wallet_id) { + return Err(WalletError::WalletExists(wallet_id)); + } + // Create managed wallet info - let mut managed_info = T::with_name(wallet.wallet_id, name); + let mut managed_info = T::from_wallet(&wallet); let network_state = self.get_or_create_network_state(network); managed_info.set_birth_height(Some(network_state.current_height)); managed_info.set_first_loaded_at(current_timestamp()); self.wallets.insert(wallet_id, wallet); self.wallet_infos.insert(wallet_id, managed_info); - Ok(self.wallet_infos.get(&wallet_id).unwrap()) + Ok(wallet_id) } /// Get a wallet by ID @@ -252,45 +359,45 @@ impl WalletManager { /// Import a wallet from an extended private key and add it to the manager /// /// # Arguments - /// * `wallet_id` - Unique identifier for the wallet - /// * `name` - Human-readable name for the wallet /// * `xprv` - The extended private key string (base58check encoded) /// * `network` - Network for the wallet /// * `account_creation_options` - Specifies which accounts to create during initialization /// /// # Returns - /// * `Ok(&T)` - Reference to the created wallet info + /// * `Ok(WalletId)` - The computed wallet ID /// * `Err(WalletError)` - If the wallet already exists or creation fails pub fn import_wallet_from_extended_priv_key( &mut self, - wallet_id: WalletId, - name: String, xprv: &str, network: Network, account_creation_options: key_wallet::wallet::initialization::WalletAccountCreationOptions, - ) -> Result<&T, WalletError> { - if self.wallets.contains_key(&wallet_id) { - return Err(WalletError::WalletExists(wallet_id)); - } - + ) -> Result { // Parse the extended private key let extended_priv_key = ExtendedPrivKey::from_str(xprv) .map_err(|e| WalletError::InvalidParameter(format!("Invalid xprv: {}", e)))?; // Create wallet from extended private key let wallet = - Wallet::from_extended_key(extended_priv_key, network, account_creation_options) + Wallet::from_extended_key(extended_priv_key, &[network], account_creation_options) .map_err(|e| WalletError::WalletCreation(e.to_string()))?; + // Compute wallet ID from the wallet's root public key + let wallet_id = wallet.compute_wallet_id(); + + // Check if wallet already exists + if self.wallets.contains_key(&wallet_id) { + return Err(WalletError::WalletExists(wallet_id)); + } + // Create managed wallet info - let mut managed_info = T::from_wallet_with_name(&wallet, name); + let mut managed_info = T::from_wallet(&wallet); managed_info .set_birth_height(Some(self.get_or_create_network_state(network).current_height)); managed_info.set_first_loaded_at(current_timestamp()); self.wallets.insert(wallet_id, wallet); self.wallet_infos.insert(wallet_id, managed_info); - Ok(self.wallet_infos.get(&wallet_id).unwrap()) + Ok(wallet_id) } /// Import a wallet from an extended public key and add it to the manager @@ -299,25 +406,20 @@ impl WalletManager { /// but cannot sign them. /// /// # Arguments - /// * `wallet_id` - Unique identifier for the wallet - /// * `name` - Human-readable name for the wallet /// * `xpub` - The extended public key string (base58check encoded) /// * `network` - Network for the wallet + /// * `can_sign_externally` - If true, creates an externally signable wallet (e.g., for hardware wallets). + /// If false, creates a pure watch-only wallet. /// /// # Returns - /// * `Ok(&T)` - Reference to the created wallet info + /// * `Ok(WalletId)` - The computed wallet ID /// * `Err(WalletError)` - If the wallet already exists or creation fails pub fn import_wallet_from_xpub( &mut self, - wallet_id: WalletId, - name: String, xpub: &str, network: Network, - ) -> Result<&T, WalletError> { - if self.wallets.contains_key(&wallet_id) { - return Err(WalletError::WalletExists(wallet_id)); - } - + can_sign_externally: bool, + ) -> Result { // Parse the extended public key let extended_pub_key = ExtendedPubKey::from_str(xpub) .map_err(|e| WalletError::InvalidParameter(format!("Invalid xpub: {}", e)))?; @@ -325,19 +427,74 @@ impl WalletManager { // Create an empty account collection for the watch-only wallet let accounts = alloc::collections::BTreeMap::from([(network, Default::default())]); - // Create watch-only wallet from extended public key - let wallet = Wallet::from_xpub(extended_pub_key, accounts) + // Create watch-only or externally signable wallet from extended public key + let wallet = Wallet::from_xpub(extended_pub_key, accounts, can_sign_externally) .map_err(|e| WalletError::WalletCreation(e.to_string()))?; + // Compute wallet ID from the wallet's root public key + let wallet_id = wallet.compute_wallet_id(); + + // Check if wallet already exists + if self.wallets.contains_key(&wallet_id) { + return Err(WalletError::WalletExists(wallet_id)); + } + // Create managed wallet info - let mut managed_info = T::from_wallet_with_name(&wallet, name); + let mut managed_info = T::from_wallet(&wallet); managed_info .set_birth_height(Some(self.get_or_create_network_state(network).current_height)); managed_info.set_first_loaded_at(current_timestamp()); self.wallets.insert(wallet_id, wallet); self.wallet_infos.insert(wallet_id, managed_info); - Ok(self.wallet_infos.get(&wallet_id).unwrap()) + Ok(wallet_id) + } + + /// Import a wallet from serialized bytes + /// + /// Deserializes a wallet from bincode-encoded bytes and adds it to the manager. + /// This is useful for restoring wallets from backups or transferring wallets + /// between systems. + /// + /// # Arguments + /// * `wallet_bytes` - The bincode-serialized wallet bytes + /// + /// # Returns + /// * `Ok(WalletId)` - The computed wallet ID of the imported wallet + /// * `Err(WalletError)` - If deserialization fails or the wallet already exists + #[cfg(feature = "bincode")] + pub fn import_wallet_from_bytes( + &mut self, + wallet_bytes: &[u8], + ) -> Result { + // Deserialize the wallet from bincode + let wallet: Wallet = bincode::decode_from_slice(wallet_bytes, bincode::config::standard()) + .map_err(|e| { + WalletError::InvalidParameter(format!("Failed to deserialize wallet: {}", e)) + })? + .0; + + // Compute wallet ID from the wallet's root public key + let wallet_id = wallet.compute_wallet_id(); + + // Check if wallet already exists + if self.wallets.contains_key(&wallet_id) { + return Err(WalletError::WalletExists(wallet_id)); + } + + // Create managed wallet info from the imported wallet + let mut managed_info = T::from_wallet(&wallet); + + // Use the current network's height as the birth height since we don't know when it was originally created + if let Some(network) = wallet.accounts.keys().next() { + let network_state = self.get_or_create_network_state(*network); + managed_info.set_birth_height(Some(network_state.current_height)); + } + managed_info.set_first_loaded_at(current_timestamp()); + + self.wallets.insert(wallet_id, wallet); + self.wallet_infos.insert(wallet_id, managed_info); + Ok(wallet_id) } /// Check a transaction against all wallets and update their states if relevant diff --git a/key-wallet-manager/tests/integration_test.rs b/key-wallet-manager/tests/integration_test.rs index 30afbe653..4d4c91bc4 100644 --- a/key-wallet-manager/tests/integration_test.rs +++ b/key-wallet-manager/tests/integration_test.rs @@ -7,7 +7,7 @@ use key_wallet::wallet::initialization::WalletAccountCreationOptions; use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::{mnemonic::Language, Mnemonic, Network}; -use key_wallet_manager::wallet_manager::{WalletError, WalletId, WalletManager}; +use key_wallet_manager::wallet_manager::{WalletError, WalletManager}; #[test] fn test_wallet_manager_creation() { @@ -25,20 +25,15 @@ fn test_wallet_manager_from_mnemonic() { let mnemonic = Mnemonic::generate(12, Language::English).unwrap(); let mut manager = WalletManager::::new(); - // Create a wallet ID - let wallet_id: WalletId = [1u8; 32]; - // Create a wallet from mnemonic - let wallet = manager.create_wallet_from_mnemonic( - wallet_id, - "Test Wallet".to_string(), + let wallet_result = manager.create_wallet_from_mnemonic( &mnemonic.to_string(), "", - Some(Network::Testnet), + &[Network::Testnet], None, // birth_height - key_wallet::wallet::initialization::WalletAccountCreationOptions::Default, + WalletAccountCreationOptions::Default, ); - assert!(wallet.is_ok(), "Failed to create wallet: {:?}", wallet); + assert!(wallet_result.is_ok(), "Failed to create wallet: {:?}", wallet_result); assert_eq!(manager.wallet_count(), 1); } @@ -46,17 +41,13 @@ fn test_wallet_manager_from_mnemonic() { fn test_account_management() { let mut manager = WalletManager::::new(); - // Create a wallet ID - let wallet_id: WalletId = [1u8; 32]; - // Create a wallet first - let wallet = manager.create_wallet( - wallet_id, - "Test Wallet".to_string(), + let wallet_result = manager.create_wallet_with_random_mnemonic( WalletAccountCreationOptions::Default, Network::Testnet, ); - assert!(wallet.is_ok(), "Failed to create wallet: {:?}", wallet); + assert!(wallet_result.is_ok(), "Failed to create wallet: {:?}", wallet_result); + let wallet_id = wallet_result.unwrap(); // Add accounts to the wallet // Note: Index 0 already exists from wallet creation, so use index 1 @@ -81,17 +72,13 @@ fn test_account_management() { fn test_address_generation() { let mut manager = WalletManager::::new(); - // Create a wallet ID - let wallet_id: WalletId = [1u8; 32]; - // Create a wallet first - let wallet = manager.create_wallet( - wallet_id, - "Test Wallet".to_string(), + let wallet_result = manager.create_wallet_with_random_mnemonic( WalletAccountCreationOptions::Default, Network::Testnet, ); - assert!(wallet.is_ok(), "Failed to create wallet: {:?}", wallet); + assert!(wallet_result.is_ok(), "Failed to create wallet: {:?}", wallet_result); + let wallet_id = wallet_result.unwrap(); // The wallet should already have account 0 from creation // But the managed wallet info might not have the account collection initialized @@ -142,17 +129,13 @@ fn test_utxo_management() { let mut manager = WalletManager::::new(); - // Create a wallet ID - let wallet_id: WalletId = [1u8; 32]; - // Create a wallet first - let wallet = manager.create_wallet( - wallet_id, - "Test Wallet".to_string(), + let wallet_result = manager.create_wallet_with_random_mnemonic( WalletAccountCreationOptions::Default, Network::Testnet, ); - assert!(wallet.is_ok(), "Failed to create wallet: {:?}", wallet); + assert!(wallet_result.is_ok(), "Failed to create wallet: {:?}", wallet_result); + let wallet_id = wallet_result.unwrap(); // For UTXO management, we need to process transactions that create UTXOs // The WalletManager doesn't have an add_utxo method directly @@ -172,17 +155,13 @@ fn test_utxo_management() { fn test_balance_calculation() { let mut manager = WalletManager::::new(); - // Create a wallet ID - let wallet_id: WalletId = [1u8; 32]; - // Create a wallet first - let wallet = manager.create_wallet( - wallet_id, - "Test Wallet".to_string(), + let wallet_result = manager.create_wallet_with_random_mnemonic( WalletAccountCreationOptions::Default, Network::Testnet, ); - assert!(wallet.is_ok(), "Failed to create wallet: {:?}", wallet); + assert!(wallet_result.is_ok(), "Failed to create wallet: {:?}", wallet_result); + let wallet_id = wallet_result.unwrap(); // For balance testing, we would need to process transactions // The WalletManager doesn't have add_utxo directly diff --git a/key-wallet-manager/tests/spv_integration_tests.rs b/key-wallet-manager/tests/spv_integration_tests.rs index 6674cfcf4..1e0088e00 100644 --- a/key-wallet-manager/tests/spv_integration_tests.rs +++ b/key-wallet-manager/tests/spv_integration_tests.rs @@ -13,7 +13,7 @@ use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; use key_wallet::Network; use key_wallet_manager::spv_wallet_manager::{SPVSyncStatus, SPVWalletManager}; use key_wallet_manager::wallet_interface::WalletInterface; -use key_wallet_manager::wallet_manager::{WalletId, WalletManager}; +use key_wallet_manager::wallet_manager::WalletManager; /// Create a test transaction fn create_test_transaction(value: u64) -> Transaction { @@ -69,44 +69,17 @@ fn create_mock_filter(block: &Block) -> BlockFilter { BlockFilter::new(&filter_bytes) } -#[test] -fn test_spv_integration_basic() { - let mut spv = SPVWalletManager::with_base(WalletManager::::new()); - - // Create a test wallet - let wallet_id: WalletId = [1u8; 32]; - spv.base - .create_wallet( - wallet_id, - "Test Wallet".to_string(), - WalletAccountCreationOptions::Default, - Network::Testnet, - ) - .ok(); - - // Verify initial state - assert_eq!(spv.sync_status(Network::Testnet), SPVSyncStatus::Idle); - assert_eq!(spv.sync_height(Network::Testnet), 0); -} - #[tokio::test] async fn test_filter_checking() { let mut spv = SPVWalletManager::with_base(WalletManager::::new()); - // Create a test wallet - let wallet_id: WalletId = [1u8; 32]; - // Add a test address to monitor - simplified for testing // In reality, addresses would be generated from wallet accounts - spv.base - .create_wallet( - wallet_id, - "Test Wallet".to_string(), - WalletAccountCreationOptions::Default, - Network::Testnet, - ) - .ok(); + let _wallet_id = spv + .base + .create_wallet_with_random_mnemonic(WalletAccountCreationOptions::Default, Network::Testnet) + .expect("Failed to create wallet"); // Create a test block with a transaction let tx = create_test_transaction(100000); @@ -128,15 +101,10 @@ async fn test_block_processing() { let mut spv = SPVWalletManager::with_base(WalletManager::::new()); // Create a test wallet - let wallet_id: WalletId = [1u8; 32]; - spv.base - .create_wallet( - wallet_id, - "Test Wallet".to_string(), - WalletAccountCreationOptions::Default, - Network::Testnet, - ) - .ok(); + let _wallet_id = spv + .base + .create_wallet_with_random_mnemonic(WalletAccountCreationOptions::Default, Network::Testnet) + .expect("Failed to create wallet"); // Create a transaction let tx = create_test_transaction(100000); @@ -151,12 +119,6 @@ async fn test_block_processing() { assert_eq!(result.len(), 0); } -#[test] -fn test_mempool_transaction() { - // This test would need async runtime to work with the async trait - // For now, we'll skip this test or make it simpler -} - #[test] fn test_queued_blocks() { let mut spv = SPVWalletManager::with_base(WalletManager::::new()); @@ -232,25 +194,14 @@ fn test_sync_status_tracking() { assert_eq!(spv.sync_status(Network::Testnet), SPVSyncStatus::Synced); } -#[test] -fn test_reorg_handling() { - // This test requires async runtime - // For now, we'll skip the full implementation -} - #[tokio::test] async fn test_multiple_wallets() { let mut spv = SPVWalletManager::with_base(WalletManager::::new()); // Create and add multiple wallets - for i in 0..3 { - let mut wallet_id = [0u8; 32]; - wallet_id[0] = i as u8; // Make each ID unique - + for _i in 0..3 { spv.base - .create_wallet( - wallet_id, - format!("Test Wallet {}", i), + .create_wallet_with_random_mnemonic( WalletAccountCreationOptions::Default, Network::Testnet, ) @@ -283,15 +234,10 @@ async fn test_spent_utxo_tracking() { let mut spv = SPVWalletManager::with_base(WalletManager::::new()); // Create a test wallet - let wallet_id: WalletId = [1u8; 32]; - spv.base - .create_wallet( - wallet_id, - "Test Wallet".to_string(), - WalletAccountCreationOptions::Default, - Network::Testnet, - ) - .ok(); + let _wallet_id = spv + .base + .create_wallet_with_random_mnemonic(WalletAccountCreationOptions::Default, Network::Testnet) + .expect("Failed to create wallet"); // Create a transaction let tx1 = create_test_transaction(100000); diff --git a/key-wallet-manager/tests/test_serialized_wallets.rs b/key-wallet-manager/tests/test_serialized_wallets.rs new file mode 100644 index 000000000..5885e77f7 --- /dev/null +++ b/key-wallet-manager/tests/test_serialized_wallets.rs @@ -0,0 +1,110 @@ +#[cfg(feature = "bincode")] +#[cfg(test)] +mod tests { + use key_wallet::wallet::initialization::WalletAccountCreationOptions; + use key_wallet::wallet::managed_wallet_info::ManagedWalletInfo; + use key_wallet::Network; + use key_wallet_manager::wallet_manager::WalletManager; + + #[test] + fn test_create_wallet_return_serialized_bytes() { + let mut manager = WalletManager::::new(); + + let test_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + + // Test 1: Create full wallet with private keys + let result = manager.create_wallet_from_mnemonic_return_serialized_bytes( + test_mnemonic, + "", + &[Network::Testnet], + Some(100_000), + WalletAccountCreationOptions::Default, + false, // Don't downgrade + false, + ); + assert!(result.is_ok()); + let (bytes, wallet_id) = result.unwrap(); + assert!(!bytes.is_empty()); + println!("Full wallet ID: {}", hex::encode(wallet_id)); + + // Test 2: Create watch-only wallet (no private keys) + let mut manager2 = WalletManager::::new(); + let result = manager2.create_wallet_from_mnemonic_return_serialized_bytes( + test_mnemonic, + "", + &[Network::Testnet], + Some(100_000), + WalletAccountCreationOptions::Default, + true, // Downgrade to pubkey wallet + false, // Watch-only, not externally signable + ); + assert!(result.is_ok()); + let (bytes2, wallet_id2) = result.unwrap(); + assert!(!bytes2.is_empty()); + + // Same wallet ID because it's derived from the same root public key + assert_eq!(wallet_id, wallet_id2); + println!("Watch-only wallet ID: {}", hex::encode(wallet_id2)); + + // Test 3: Create externally signable wallet (for hardware wallets) + let mut manager3 = WalletManager::::new(); + let result = manager3.create_wallet_from_mnemonic_return_serialized_bytes( + test_mnemonic, + "", + &[Network::Testnet], + Some(100_000), + WalletAccountCreationOptions::Default, + true, // Downgrade to pubkey wallet + true, // Externally signable (for hardware wallets) + ); + assert!(result.is_ok()); + let (bytes3, wallet_id3) = result.unwrap(); + assert!(!bytes3.is_empty()); + assert_eq!(wallet_id, wallet_id3); + println!("Externally signable wallet ID: {}", hex::encode(wallet_id3)); + + // Test 4: Import the serialized wallet back + let mut manager4 = WalletManager::::new(); + let import_result = manager4.import_wallet_from_bytes(&bytes); + assert!(import_result.is_ok()); + assert_eq!(import_result.unwrap(), wallet_id); + } + + #[test] + fn test_wallet_with_passphrase() { + let mut manager = WalletManager::::new(); + + let test_mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"; + let passphrase = "test_passphrase"; + + let result = manager.create_wallet_from_mnemonic_return_serialized_bytes( + test_mnemonic, + passphrase, + &[Network::Testnet], + None, + WalletAccountCreationOptions::Default, + false, + false, + ); + assert!(result.is_ok()); + let (bytes, wallet_id) = result.unwrap(); + assert!(!bytes.is_empty()); + + // Wallet ID with passphrase should be different + let mut manager2 = WalletManager::::new(); + let result2 = manager2.create_wallet_from_mnemonic_return_serialized_bytes( + test_mnemonic, + "", // No passphrase + &[Network::Testnet], + None, + WalletAccountCreationOptions::Default, + false, + false, + ); + assert!(result2.is_ok()); + let (_bytes2, wallet_id2) = result2.unwrap(); + + // Different wallet IDs because different passphrases + assert_ne!(wallet_id, wallet_id2); + } +} diff --git a/key-wallet/Cargo.toml b/key-wallet/Cargo.toml index 559caac39..0023939ea 100644 --- a/key-wallet/Cargo.toml +++ b/key-wallet/Cargo.toml @@ -22,7 +22,7 @@ internals = { path = "../internals", package = "dashcore-private" } dashcore_hashes = { path = "../hashes", default-features = false } dashcore = { path="../dash" } secp256k1 = { version = "0.30.0", default-features = false, features = ["hashes", "recovery"] } -bip39 = { version = "2.2.0", default-features = false, features = ["chinese-simplified", "chinese-traditional", "czech", "french", "italian", "japanese", "korean", "portuguese", "spanish"] } +bip39 = { version = "2.2.0", default-features = false, features = ["chinese-simplified", "chinese-traditional", "czech", "french", "italian", "japanese", "korean", "portuguese", "spanish", "zeroize"] } serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } base58ck = { version = "0.1.0", default-features = false } bitflags = { version = "2.6", default-features = false } @@ -41,6 +41,7 @@ base64 = { version = "0.22", optional = true } serde_json = { version = "1.0", optional = true } hex = { version = "0.4"} hkdf = { version = "0.12", default-features = false } +zeroize = { version = "1.8", features = ["derive"] } [dev-dependencies] hex = "0.4" diff --git a/key-wallet/src/account/account_type.rs b/key-wallet/src/account/account_type.rs index 8221687c8..7bae610f7 100644 --- a/key-wallet/src/account/account_type.rs +++ b/key-wallet/src/account/account_type.rs @@ -4,6 +4,7 @@ use crate::bip32::{ChildNumber, DerivationPath}; use crate::dip9::DerivationPathReference; +use crate::transaction_checking::transaction_router::AccountTypeToCheck; use crate::Network; #[cfg(feature = "bincode")] use bincode_derive::{Decode, Encode}; @@ -64,6 +65,35 @@ pub enum AccountType { ProviderPlatformKeys, } +impl From 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, + } + } +} + impl AccountType { /// Get the primary index for this account type /// Returns None for provider key types and identity types that don't have account indices diff --git a/key-wallet/src/address_metadata_tests.rs b/key-wallet/src/address_metadata_tests.rs index e0572ec14..4a753f898 100644 --- a/key-wallet/src/address_metadata_tests.rs +++ b/key-wallet/src/address_metadata_tests.rs @@ -27,7 +27,7 @@ mod tests { // Basic test that wallet and accounts can be created let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -48,7 +48,7 @@ mod tests { #[test] fn test_multiple_accounts() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); diff --git a/key-wallet/src/bip32.rs b/key-wallet/src/bip32.rs index 9b80c0757..cfbd59715 100644 --- a/key-wallet/src/bip32.rs +++ b/key-wallet/src/bip32.rs @@ -111,6 +111,14 @@ impl fmt::Debug for ChainCode { } } +// Manual implementation of Zeroize for ChainCode +// Note: ChainCode is Copy, so this won't prevent copies in registers/stack +impl zeroize::Zeroize for ChainCode { + fn zeroize(&mut self) { + zeroize::Zeroize::zeroize(&mut self.0); + } +} + impl fmt::LowerHex for ChainCode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { for &byte in &self.0 { diff --git a/key-wallet/src/managed_account/managed_account_trait.rs b/key-wallet/src/managed_account/managed_account_trait.rs index 9d89f4eca..5e9b41acc 100644 --- a/key-wallet/src/managed_account/managed_account_trait.rs +++ b/key-wallet/src/managed_account/managed_account_trait.rs @@ -8,9 +8,9 @@ use crate::managed_account::managed_account_type::ManagedAccountType; use crate::utxo::Utxo; use crate::wallet::balance::WalletBalance; use crate::Network; -use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::collections::BTreeMap; use dashcore::blockdata::transaction::OutPoint; -use dashcore::{Address, Txid}; +use dashcore::Txid; /// Common trait for all managed account types pub trait ManagedAccountTrait { @@ -44,12 +44,6 @@ pub trait ManagedAccountTrait { /// Get mutable transactions fn transactions_mut(&mut self) -> &mut BTreeMap; - /// Get monitored addresses - fn monitored_addresses(&self) -> &BTreeSet
; - - /// Get mutable monitored addresses - fn monitored_addresses_mut(&mut self) -> &mut BTreeSet
; - /// Get UTXOs fn utxos(&self) -> &BTreeMap; diff --git a/key-wallet/src/managed_account/mod.rs b/key-wallet/src/managed_account/mod.rs index 7b79041fb..0769bf7fe 100644 --- a/key-wallet/src/managed_account/mod.rs +++ b/key-wallet/src/managed_account/mod.rs @@ -19,7 +19,7 @@ use crate::wallet::balance::WalletBalance; #[cfg(feature = "eddsa")] use crate::AddressInfo; use crate::{ExtendedPubKey, Network}; -use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::collections::BTreeMap; use dashcore::blockdata::transaction::OutPoint; use dashcore::Txid; use dashcore::{Address, ScriptBuf}; @@ -54,8 +54,6 @@ pub struct ManagedAccount { pub balance: WalletBalance, /// Transaction history for this account pub transactions: BTreeMap, - /// Monitored addresses for transaction detection - pub monitored_addresses: BTreeSet
, /// UTXO set for this account pub utxos: BTreeMap, } @@ -70,7 +68,6 @@ impl ManagedAccount { is_watch_only, balance: WalletBalance::default(), transactions: BTreeMap::new(), - monitored_addresses: BTreeSet::new(), utxos: BTreeMap::new(), } } @@ -819,14 +816,6 @@ impl ManagedAccountTrait for ManagedAccount { &mut self.transactions } - fn monitored_addresses(&self) -> &BTreeSet
{ - &self.monitored_addresses - } - - fn monitored_addresses_mut(&mut self) -> &mut BTreeSet
{ - &mut self.monitored_addresses - } - fn utxos(&self) -> &BTreeMap { &self.utxos } diff --git a/key-wallet/src/mnemonic.rs b/key-wallet/src/mnemonic.rs index 44987ee75..0e3bee099 100644 --- a/key-wallet/src/mnemonic.rs +++ b/key-wallet/src/mnemonic.rs @@ -14,6 +14,7 @@ use bip39 as bip39_crate; use rand::{RngCore, SeedableRng}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use zeroize::{Zeroize, ZeroizeOnDrop}; /// Language for mnemonic generation #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -50,7 +51,7 @@ impl From for bip39_crate::Language { } /// BIP39 Mnemonic phrase -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Zeroize, ZeroizeOnDrop)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct Mnemonic { inner: bip39_crate::Mnemonic, diff --git a/key-wallet/src/seed.rs b/key-wallet/src/seed.rs index eb196a368..9c4ccda52 100644 --- a/key-wallet/src/seed.rs +++ b/key-wallet/src/seed.rs @@ -2,20 +2,20 @@ //! //! A seed is a 512-bit (64 bytes) value used to derive HD wallet keys. +use crate::error::{Error, Result}; use alloc::string::String; use alloc::vec::Vec; #[cfg(feature = "bincode")] use bincode_derive::{Decode, Encode}; use core::fmt; use core::str::FromStr; +use dashcore_hashes::hex::FromHex; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; - -use crate::error::{Error, Result}; -use dashcore_hashes::hex::FromHex; +use zeroize::Zeroize; /// A BIP32 seed (512 bits / 64 bytes) -#[derive(Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Zeroize)] #[cfg_attr(feature = "bincode", derive(Encode, Decode))] pub struct Seed([u8; 64]); diff --git a/key-wallet/src/tests/advanced_transaction_tests.rs b/key-wallet/src/tests/advanced_transaction_tests.rs index 2faaf160c..075022ae7 100644 --- a/key-wallet/src/tests/advanced_transaction_tests.rs +++ b/key-wallet/src/tests/advanced_transaction_tests.rs @@ -14,7 +14,7 @@ fn test_multi_account_transaction() { // Test transaction involving multiple accounts let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); diff --git a/key-wallet/src/tests/backup_restore_tests.rs b/key-wallet/src/tests/backup_restore_tests.rs index d6f880f42..220588e34 100644 --- a/key-wallet/src/tests/backup_restore_tests.rs +++ b/key-wallet/src/tests/backup_restore_tests.rs @@ -16,7 +16,7 @@ fn test_wallet_mnemonic_export() { let wallet = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -36,7 +36,7 @@ fn test_wallet_mnemonic_export() { #[test] fn test_wallet_full_backup_restore() { let mut original_wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -81,7 +81,7 @@ fn test_wallet_full_backup_restore() { // Restore wallet let mut restored_wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -124,7 +124,7 @@ fn test_wallet_partial_backup() { // Test backing up only essential data (mnemonic + account indices) let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -145,7 +145,7 @@ fn test_wallet_partial_backup() { ]; for account_type in &account_metadata { - wallet.add_account(account_type.clone(), Network::Testnet, None).unwrap(); + wallet.add_account(*account_type, Network::Testnet, None).unwrap(); } // Verify accounts were added @@ -154,57 +154,12 @@ fn test_wallet_partial_backup() { assert_eq!(collection.coinjoin_accounts.len(), 1); } -#[test] -fn test_wallet_encrypted_backup() { - // Test wallet backup with encryption (simulated) - let passphrase = "strong_passphrase_123!@#"; - let mnemonic = Mnemonic::from_phrase( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language::English, - ).unwrap(); - - let wallet = Wallet::from_mnemonic_with_passphrase( - mnemonic.clone(), - passphrase.to_string(), - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - // Simulate encrypted backup - struct EncryptedBackup { - encrypted_mnemonic: Vec, // In real implementation, would be encrypted - _salt: [u8; 32], - network: Network, - } - - let backup = EncryptedBackup { - encrypted_mnemonic: mnemonic.to_string().into_bytes(), // Would be encrypted in real implementation - _salt: [0u8; 32], // Would be random salt - network: Network::Testnet, - }; - - // Simulate decryption and restoration - let decrypted_mnemonic = String::from_utf8(backup.encrypted_mnemonic).unwrap(); - let restored_mnemonic = Mnemonic::from_phrase(&decrypted_mnemonic, Language::English).unwrap(); - - let restored_wallet = Wallet::from_mnemonic_with_passphrase( - restored_mnemonic, - passphrase.to_string(), - backup.network, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - assert_eq!(wallet.wallet_id, restored_wallet.wallet_id); -} - #[test] fn test_wallet_metadata_backup() { // Test backing up wallet metadata (labels, settings, etc.) let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -235,7 +190,7 @@ fn test_wallet_metadata_backup() { ]; for item in &metadata { - wallet.add_account(item.account_type.clone(), Network::Testnet, None).unwrap(); + wallet.add_account(item.account_type, Network::Testnet, None).unwrap(); } // Verify metadata can be associated with accounts @@ -253,7 +208,7 @@ fn test_multi_network_backup_restore() { let mut wallet = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -296,7 +251,7 @@ fn test_multi_network_backup_restore() { // Restore and verify let mut restored = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -327,7 +282,7 @@ fn test_incremental_backup() { // Test incremental backup of changes since last backup let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -378,37 +333,3 @@ fn test_incremental_backup() { wallet.accounts.get(&Network::Testnet).map(|c| c.coinjoin_accounts.len()).unwrap_or(0); assert_eq!(coinjoin_count, 1); } - -#[test] -fn test_backup_version_compatibility() { - // Test handling of backups from different wallet versions - struct VersionedBackup { - version: u32, - mnemonic: String, - network: Network, - } - - let backup_v1 = VersionedBackup { - version: 1, - mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(), - network: Network::Testnet, - }; - - // Simulate migration from older version - let mnemonic = Mnemonic::from_phrase(&backup_v1.mnemonic, Language::English).unwrap(); - - let wallet = match backup_v1.version { - 1 => { - // Version 1 migration logic - Wallet::from_mnemonic( - mnemonic, - backup_v1.network, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap() - } - _ => panic!("Unsupported backup version"), - }; - - assert_ne!(wallet.wallet_id, [0u8; 32]); -} diff --git a/key-wallet/src/tests/coinjoin_mixing_tests.rs b/key-wallet/src/tests/coinjoin_mixing_tests.rs deleted file mode 100644 index eedf3e6d2..000000000 --- a/key-wallet/src/tests/coinjoin_mixing_tests.rs +++ /dev/null @@ -1,377 +0,0 @@ -//! Tests for CoinJoin mixing functionality -//! -//! Tests CoinJoin rounds, denomination creation, and privacy features. - -use crate::account::AccountType; -use crate::wallet::Wallet; -use crate::Network; -use dashcore::hashes::Hash; -use dashcore::{OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid}; -use std::collections::{HashMap, HashSet}; - -/// CoinJoin denomination amounts (in duffs) -const DENOMINATIONS: [u64; 5] = [ - 100_001, // 0.00100001 DASH - 1_000_010, // 0.01000010 DASH - 10_000_100, // 0.10000100 DASH - 100_001_000, // 1.00001000 DASH - 1_000_010_000, // 10.00010000 DASH -]; - -#[derive(Debug, Clone)] -struct CoinJoinRound { - _round_id: u64, - denomination: u64, - _participants: Vec, - _collateral_required: u64, -} - -#[derive(Debug, Clone)] -struct ParticipantInfo { - _participant_id: u32, - _inputs: Vec, - _output_addresses: Vec, -} - -#[test] -fn test_coinjoin_denomination_creation() { - // Test creating standard CoinJoin denominations - - let mut wallet = Wallet::new_random( - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - wallet - .add_account( - AccountType::CoinJoin { - index: 0, - }, - Network::Testnet, - None, - ) - .unwrap(); - - // Simulate creating denominations from a large input - let input_amount = 5_000_000_000u64; // 50 DASH - let mut remaining = input_amount; - let mut denominations_created = HashMap::new(); - - // Create maximum denominations starting from largest - for &denom in DENOMINATIONS.iter().rev() { - while remaining >= denom { - *denominations_created.entry(denom).or_insert(0) += 1; - remaining -= denom; - } - } - - // Verify denominations created efficiently - assert!(remaining < DENOMINATIONS[0]); // Less than smallest denomination left - - // Check we created multiple denominations - assert!(denominations_created.len() > 0); - - // Verify total value preserved (minus remainder) - let total_denominated: u64 = - denominations_created.iter().map(|(denom, count)| denom * count).sum(); - assert_eq!(total_denominated, input_amount - remaining); -} - -#[test] -fn test_coinjoin_output_shuffling() { - // Test that CoinJoin outputs are properly shuffled - let num_participants = 10; - let outputs_per_participant = 3; - - // Create output addresses - let mut all_outputs = Vec::new(); - for i in 0..num_participants { - for j in 0..outputs_per_participant { - all_outputs.push(format!("output_{}_{}", i, j)); - } - } - - // Simulate shuffling (in real implementation would use secure randomness) - let original_order = all_outputs.clone(); - - // Simple shuffle simulation - let mut shuffled = all_outputs.clone(); - shuffled.reverse(); // Simple transformation for testing - - // Verify all outputs still present - let original_set: HashSet<_> = original_order.iter().collect(); - let shuffled_set: HashSet<_> = shuffled.iter().collect(); - assert_eq!(original_set, shuffled_set); - - // Verify order changed (in real implementation) - assert_ne!(original_order, shuffled); -} - -#[test] -fn test_coinjoin_fee_calculation() { - // Test CoinJoin fee calculations - let denomination = DENOMINATIONS[2]; // 0.1 DASH - let num_inputs = 3; - let num_outputs = 3; - - // Estimate transaction size - let estimated_size = 10 + // Version + locktime - (num_inputs * 148) + // Approximate input size - (num_outputs * 34); // Approximate output size - - // Calculate fee (1 duff per byte as example) - let fee_rate = 1; // duffs per byte - let total_fee = estimated_size * fee_rate; - - // Each participant pays their share - let fee_per_participant = total_fee / num_inputs; - - assert!(fee_per_participant > 0); - assert!(fee_per_participant < denomination / 100); // Fee should be small relative to amount -} - -#[test] -fn test_coinjoin_collateral_handling() { - // Collateral amount (0.001% of denomination) - let denomination = DENOMINATIONS[3]; // 1 DASH - let collateral = denomination / 100000; // 0.001% - - // Verify collateral is reasonable - assert!(collateral > 0); - assert!(collateral < denomination / 100); // Less than 1% of denomination - - // Simulate collateral transaction - let collateral_tx = Transaction { - version: 2, - lock_time: 0, - input: vec![TxIn { - previous_output: OutPoint { - txid: Txid::from_byte_array([1u8; 32]), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: 0xffffffff, - witness: dashcore::Witness::default(), - }], - output: vec![TxOut { - value: collateral, - script_pubkey: ScriptBuf::new(), - }], - special_transaction_payload: None, - }; - - assert_eq!(collateral_tx.output[0].value, collateral); -} - -#[test] -fn test_coinjoin_round_timeout() { - // Test handling of CoinJoin round timeouts - use std::time::{Duration, Instant}; - - let round_timeout = Duration::from_secs(30); - let round_start = Instant::now(); - - // Simulate waiting for participants - let mut participants_joined = 0; - let required_participants = 3; - - // Simulate participants joining over time - while participants_joined < required_participants { - if round_start.elapsed() > round_timeout { - // Round timed out - break; - } - - // Simulate participant joining - participants_joined += 1; - - if participants_joined >= required_participants { - // Round can proceed - break; - } - } - - // Check if round succeeded or timed out - if participants_joined < required_participants { - // Round failed - return collateral - assert!(round_start.elapsed() >= round_timeout); - } else { - // Round succeeded - assert_eq!(participants_joined, required_participants); - } -} - -#[test] -fn test_multiple_denomination_mixing() { - // Test mixing multiple denominations in parallel - - let mut wallet = Wallet::new_random( - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - wallet - .add_account( - AccountType::CoinJoin { - index: 0, - }, - Network::Testnet, - None, - ) - .unwrap(); - - // Create rounds for different denominations - let rounds = vec![ - CoinJoinRound { - _round_id: 1, - denomination: DENOMINATIONS[0], // 0.001 DASH - _participants: Vec::new(), - _collateral_required: 100, - }, - CoinJoinRound { - _round_id: 2, - denomination: DENOMINATIONS[2], // 0.1 DASH - _participants: Vec::new(), - _collateral_required: 1000, - }, - CoinJoinRound { - _round_id: 3, - denomination: DENOMINATIONS[3], // 1 DASH - _participants: Vec::new(), - _collateral_required: 10000, - }, - ]; - - // Verify we can participate in multiple rounds - assert_eq!(rounds.len(), 3); - - // Each round has different denomination - let denoms: HashSet<_> = rounds.iter().map(|r| r.denomination).collect(); - assert_eq!(denoms.len(), rounds.len()); -} - -#[test] -fn test_coinjoin_transaction_verification() { - // Test verification of CoinJoin transaction structure - let num_participants = 5; - let denomination = DENOMINATIONS[2]; - - // Create CoinJoin transaction - let mut inputs = Vec::new(); - let mut outputs = Vec::new(); - - // Add inputs from each participant - for i in 0..num_participants { - inputs.push(TxIn { - previous_output: OutPoint { - txid: Txid::from_byte_array([i as u8; 32]), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: 0xffffffff, - witness: dashcore::Witness::default(), - }); - } - - // Add outputs (2 per participant for this round) - for _ in 0..num_participants * 2 { - outputs.push(TxOut { - value: denomination, - script_pubkey: ScriptBuf::new(), - }); - } - - let coinjoin_tx = Transaction { - version: 2, - lock_time: 0, - input: inputs, - output: outputs, - special_transaction_payload: None, - }; - - // Verify CoinJoin properties - assert_eq!(coinjoin_tx.input.len(), num_participants); - assert_eq!(coinjoin_tx.output.len(), num_participants * 2); - - // All outputs should have same value - let output_values: HashSet<_> = coinjoin_tx.output.iter().map(|o| o.value).collect(); - assert_eq!(output_values.len(), 1); // All same denomination - assert!(output_values.contains(&denomination)); -} - -#[test] -fn test_coinjoin_privacy_metrics() { - // Test measuring privacy achieved through CoinJoin - struct PrivacyMetrics { - anonymity_set: usize, - rounds_participated: u32, - percentage_mixed: f64, - } - - let total_balance = 10_000_000_000u64; // 100 DASH - let mixed_balance = 7_500_000_000u64; // 75 DASH - - let metrics = PrivacyMetrics { - anonymity_set: 50, // Number of possible sources for coins - rounds_participated: 5, - percentage_mixed: (mixed_balance as f64 / total_balance as f64) * 100.0, - }; - - // Verify privacy improvements - assert!(metrics.anonymity_set >= 10); // Minimum anonymity set - assert!(metrics.rounds_participated > 0); - assert!(metrics.percentage_mixed >= 75.0); // 75% mixed -} - -#[test] -fn test_coinjoin_session_management() { - // Test managing multiple CoinJoin sessions - #[derive(Debug)] - struct CoinJoinSession { - _session_id: u64, - state: SessionState, - participants: u32, - _timeout: std::time::Duration, - } - - #[derive(Debug, PartialEq)] - enum SessionState { - Queued, - Signing, - Broadcasting, - _Completed, - Failed, - } - - let mut sessions = Vec::new(); - - // Create multiple sessions - for i in 0..3 { - sessions.push(CoinJoinSession { - _session_id: i, - state: SessionState::Queued, - participants: 0, - _timeout: std::time::Duration::from_secs(30), - }); - } - - // Simulate session progression - sessions[0].state = SessionState::Signing; - sessions[0].participants = 5; - - sessions[1].state = SessionState::Broadcasting; - sessions[1].participants = 8; - - sessions[2].state = SessionState::Failed; // Timeout - - // Verify session management - assert_eq!(sessions[0].state, SessionState::Signing); - assert_eq!(sessions[1].state, SessionState::Broadcasting); - assert_eq!(sessions[2].state, SessionState::Failed); - - // Count successful sessions - let successful = sessions.iter().filter(|s| s.state != SessionState::Failed).count(); - assert_eq!(successful, 2); -} diff --git a/key-wallet/src/tests/edge_case_tests.rs b/key-wallet/src/tests/edge_case_tests.rs index 896a9ab8c..f45dec43e 100644 --- a/key-wallet/src/tests/edge_case_tests.rs +++ b/key-wallet/src/tests/edge_case_tests.rs @@ -54,7 +54,7 @@ fn test_corrupted_wallet_data_recovery() { let wallet = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -65,7 +65,7 @@ fn test_corrupted_wallet_data_recovery() { // Recovery: recreate from mnemonic let recovered_wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -82,7 +82,7 @@ fn test_network_mismatch_handling() { // Create wallet for testnet let testnet_wallet = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -90,7 +90,7 @@ fn test_network_mismatch_handling() { // Create wallet for mainnet with same mnemonic let mainnet_wallet = Wallet::from_mnemonic( mnemonic, - Network::Dash, + &[Network::Dash], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -134,7 +134,7 @@ fn test_zero_value_transaction_handling() { #[test] fn test_duplicate_account_handling() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -239,7 +239,7 @@ fn test_concurrent_access_simulation() { let wallet = Arc::new(Mutex::new( Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(), @@ -254,7 +254,7 @@ fn test_concurrent_access_simulation() { let wallet = wallet_clone.lock().unwrap(); let _id = wallet.wallet_id; // Simulate some work - std::thread::sleep(std::time::Duration::from_millis(10)); + thread::sleep(std::time::Duration::from_millis(10)); }); handles.push(handle); } @@ -269,26 +269,6 @@ fn test_concurrent_access_simulation() { assert_ne!(wallet.wallet_id, [0u8; 32]); } -#[test] -fn test_empty_wallet_operations() { - let wallet = Wallet::new_random( - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - // Operations on empty wallet should not panic - let network = Network::Testnet; - - // Get account that doesn't exist - let account = wallet.get_bip44_account(network, 999); - assert!(account.is_none()); - - // Get balance of empty wallet - // In real implementation: let balance = wallet.get_balance(network); - // assert_eq!(balance, 0); -} - #[test] fn test_passphrase_edge_cases() { let mnemonic = Mnemonic::from_phrase( @@ -299,7 +279,7 @@ fn test_passphrase_edge_cases() { // Test with empty passphrase - use regular from_mnemonic for empty passphrase let wallet1 = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -309,7 +289,7 @@ fn test_passphrase_edge_cases() { let wallet2 = Wallet::from_mnemonic_with_passphrase( mnemonic.clone(), long_passphrase, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -319,7 +299,7 @@ fn test_passphrase_edge_cases() { let wallet3 = Wallet::from_mnemonic_with_passphrase( mnemonic, special_passphrase.to_string(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -357,7 +337,7 @@ fn test_wallet_recovery_with_missing_accounts() { let mut wallet = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -388,7 +368,7 @@ fn test_wallet_recovery_with_missing_accounts() { // Recovery should handle gaps in account indices let recovered_wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); diff --git a/key-wallet/src/tests/integration_tests.rs b/key-wallet/src/tests/integration_tests.rs index 2eca78232..b519ba463 100644 --- a/key-wallet/src/tests/integration_tests.rs +++ b/key-wallet/src/tests/integration_tests.rs @@ -6,124 +6,6 @@ use crate::account::{AccountType, StandardAccountType}; use crate::mnemonic::{Language, Mnemonic}; use crate::wallet::Wallet; use crate::Network; -use dashcore::hashes::Hash; -use dashcore::{OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid}; - -#[test] -fn test_full_wallet_lifecycle() { - // 1. Create wallet - - let mut wallet = Wallet::new_random( - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - let wallet_id = wallet.wallet_id; - - // 2. Add multiple accounts - for i in 0..5 { - wallet - .add_account( - AccountType::Standard { - index: i, - standard_account_type: StandardAccountType::BIP44Account, - }, - Network::Testnet, - None, - ) - .unwrap(); - } - - // 3. Add different account types - wallet - .add_account( - AccountType::CoinJoin { - index: 0, - }, - Network::Testnet, - None, - ) - .unwrap(); - - // 4. Verify account structure - let collection = wallet.accounts.get(&Network::Testnet).unwrap(); - assert_eq!(collection.standard_bip44_accounts.len(), 5); // 0-4 - assert_eq!(collection.coinjoin_accounts.len(), 1); - - // 5. Export mnemonic for recovery - let mnemonic = match &wallet.wallet_type { - crate::wallet::WalletType::Mnemonic { - mnemonic, - .. - } => mnemonic.clone(), - _ => panic!("Expected mnemonic wallet"), - }; - - // 6. Destroy wallet and recover - drop(wallet); - - // 7. Recover wallet from mnemonic - let recovered_wallet = Wallet::from_mnemonic( - mnemonic, - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - // 8. Verify wallet ID matches - assert_eq!(recovered_wallet.wallet_id, wallet_id); - - // 9. Re-add accounts and verify they generate same addresses - // (In real implementation, would check address generation) -} - -#[test] -fn test_account_discovery_workflow() { - let mnemonic = Mnemonic::from_phrase( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language::English, - ).unwrap(); - - let mut wallet = Wallet::from_mnemonic( - mnemonic, - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - // Simulate account discovery process - let mut found_accounts = Vec::new(); - let max_gap = 5; // Stop after 5 consecutive unused accounts - let mut gap_count = 0; - - for i in 0..20 { - // In real implementation, would check blockchain for transactions - let has_transactions = i < 3 || i == 7; // Simulate accounts 0,1,2,7 having transactions - - if has_transactions { - // Try to add account, OK if it already exists (account 0 is created by default) - wallet - .add_account( - AccountType::Standard { - index: i, - standard_account_type: StandardAccountType::BIP44Account, - }, - Network::Testnet, - None, - ) - .ok(); - found_accounts.push(i); - gap_count = 0; - } else { - gap_count += 1; - if gap_count >= max_gap { - break; - } - } - } - - assert_eq!(found_accounts, vec![0, 1, 2, 7]); -} #[test] fn test_multi_network_wallet_management() { @@ -135,7 +17,7 @@ fn test_multi_network_wallet_management() { // Create wallet and add accounts on different networks let mut wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -191,7 +73,7 @@ fn test_multi_network_wallet_management() { #[test] fn test_wallet_with_all_account_types() { let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::AllAccounts( [0, 1].into(), [0].into(), @@ -215,186 +97,3 @@ fn test_wallet_with_all_account_types() { assert!(collection.provider_operator_keys.is_some()); assert!(collection.provider_platform_keys.is_some()); } - -#[test] -fn test_transaction_broadcast_simulation() { - let _wallet = Wallet::new_random( - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - // Simulate creating a transaction - let tx = Transaction { - version: 2, - lock_time: 0, - input: vec![TxIn { - previous_output: OutPoint { - txid: Txid::from_byte_array([1u8; 32]), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: 0xffffffff, - witness: dashcore::Witness::default(), - }], - output: vec![ - TxOut { - value: 100000, - script_pubkey: ScriptBuf::new(), - }, - TxOut { - value: 50000, // Change output - script_pubkey: ScriptBuf::new(), - }, - ], - special_transaction_payload: None, - }; - - // Simulate broadcast process - let txid = tx.txid(); - - // 1. Mark outputs as pending - // 2. Broadcast to network (simulated) - // 3. Wait for confirmation (simulated) - // 4. Update wallet state - - assert_ne!(txid, Txid::from_byte_array([0u8; 32])); -} - -#[test] -fn test_concurrent_wallet_operations() { - use std::sync::{Arc, Mutex}; - use std::thread; - - let wallet = Arc::new(Mutex::new( - Wallet::new_random( - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(), - )); - - let mut handles = Vec::new(); - - // Simulate concurrent operations - for i in 0..5 { - let wallet_clone = Arc::clone(&wallet); - - // Different operation types - let handle = match i % 3 { - 0 => { - // Add account - thread::spawn(move || { - let mut wallet = wallet_clone.lock().unwrap(); - wallet - .add_account( - AccountType::Standard { - index: i, - standard_account_type: StandardAccountType::BIP44Account, - }, - Network::Testnet, - None, - ) - .ok(); - }) - } - 1 => { - // Read balance (simulated) - thread::spawn(move || { - let wallet = wallet_clone.lock().unwrap(); - let _accounts = wallet.accounts.get(&Network::Testnet); - }) - } - _ => { - // Get account - thread::spawn(move || { - let wallet = wallet_clone.lock().unwrap(); - let _account = wallet.get_bip44_account(Network::Testnet, i); - }) - } - }; - - handles.push(handle); - } - - // Wait for all operations to complete - for handle in handles { - handle.join().unwrap(); - } - - // Verify wallet is still in valid state - let wallet = wallet.lock().unwrap(); - assert!(wallet.accounts.contains_key(&Network::Testnet)); -} - -#[test] -fn test_wallet_with_thousands_of_addresses() { - // Stress test with large number of addresses - - let _wallet = Wallet::new_random( - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - // Account 0 is already created by default, no need to add it - - // Simulate generating many addresses - let num_addresses = 1000; - let mut generation_times = Vec::new(); - - for _i in 0..num_addresses { - let start = std::time::Instant::now(); - - // In real implementation would generate address at index i - // let _address = account.derive_address(i); - - let elapsed = start.elapsed(); - generation_times.push(elapsed.as_micros()); - } - - // Calculate statistics - let avg_time: u128 = generation_times.iter().sum::() / generation_times.len() as u128; - let max_time = generation_times.iter().max().unwrap(); - - // Performance assertions - assert!(avg_time < 1000); // Average should be under 1ms - assert!(max_time < &10000); // Max should be under 10ms -} - -#[test] -fn test_wallet_recovery_with_used_addresses() { - // Test recovery when addresses have been used out of order - let mnemonic = Mnemonic::from_phrase( - "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about", - Language::English, - ).unwrap(); - - let _wallet = Wallet::from_mnemonic( - mnemonic.clone(), - Network::Testnet, - crate::wallet::initialization::WalletAccountCreationOptions::None, - ) - .unwrap(); - - // Simulate address usage pattern: 0, 1, 2, 5, 10, 15 - let used_indices = vec![0, 1, 2, 5, 10, 15]; - - // Recovery should discover all used addresses with gap limit - let gap_limit = 20; - let mut discovered = Vec::new(); - - for i in 0..30 { - if used_indices.contains(&i) { - discovered.push(i); - } - - // Check if we've exceeded gap limit - let last_used = discovered.last().copied().unwrap_or(0); - if i - last_used > gap_limit { - break; - } - } - - assert_eq!(discovered, used_indices); -} diff --git a/key-wallet/src/tests/mod.rs b/key-wallet/src/tests/mod.rs index 71f9f215e..fce02450e 100644 --- a/key-wallet/src/tests/mod.rs +++ b/key-wallet/src/tests/mod.rs @@ -2,31 +2,28 @@ //! //! This module contains exhaustive tests for all functionality. -#[cfg(test)] mod account_tests; -#[cfg(test)] + mod address_pool_tests; -#[cfg(test)] + mod advanced_transaction_tests; -#[cfg(test)] + mod backup_restore_tests; -#[cfg(test)] -mod coinjoin_mixing_tests; -#[cfg(test)] + mod edge_case_tests; -#[cfg(test)] + mod immature_transaction_tests; -#[cfg(test)] + mod integration_tests; -#[cfg(test)] + mod managed_account_collection_tests; -#[cfg(test)] + mod performance_tests; -#[cfg(test)] + mod special_transaction_tests; -#[cfg(test)] + mod transaction_tests; -#[cfg(test)] + mod utxo_tests; -#[cfg(test)] + mod wallet_tests; diff --git a/key-wallet/src/tests/performance_tests.rs b/key-wallet/src/tests/performance_tests.rs index d1bf668a4..1b34a4691 100644 --- a/key-wallet/src/tests/performance_tests.rs +++ b/key-wallet/src/tests/performance_tests.rs @@ -89,7 +89,7 @@ fn test_key_derivation_performance() { #[test] fn test_account_creation_performance() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -134,7 +134,7 @@ fn test_wallet_recovery_performance() { let start = Instant::now(); let _wallet = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -203,7 +203,7 @@ fn test_address_generation_batch_performance() { #[test] fn test_large_wallet_memory_usage() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -301,7 +301,7 @@ fn test_wallet_serialization_performance() { for _ in 0..iterations { let start = Instant::now(); let _wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -314,65 +314,6 @@ fn test_wallet_serialization_performance() { assert!(metrics.avg_time < Duration::from_millis(50)); } -#[test] -fn test_transaction_checking_performance() { - use dashcore::hashes::Hash; - use dashcore::{OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid}; - - // Create many transactions to check - let num_transactions = 1000; - let mut transactions = Vec::new(); - - for i in 0..num_transactions { - let tx = Transaction { - version: 2, - lock_time: 0, - input: vec![TxIn { - previous_output: OutPoint { - txid: Txid::from_byte_array([(i % 256) as u8; 32]), - vout: 0, - }, - script_sig: ScriptBuf::new(), - sequence: 0xffffffff, - witness: dashcore::Witness::default(), - }], - output: vec![TxOut { - value: 100000, - script_pubkey: ScriptBuf::new(), - }], - special_transaction_payload: None, - }; - transactions.push(tx); - } - - let start = Instant::now(); - - // Simulate checking transactions - for tx in &transactions { - let _txid = tx.txid(); - let _is_coinbase = tx.is_coin_base(); - // In real implementation would check against wallet addresses - } - - let elapsed = start.elapsed(); - let ops_per_second = num_transactions as f64 / elapsed.as_secs_f64(); - - // Print detailed performance metrics before assertion - println!("\n=== Transaction Checking Performance ==="); - println!("Checked {} transactions in {:?}", num_transactions, elapsed); - println!("Transactions per second: {:.2}", ops_per_second); - println!("Average time per transaction: {:?}", elapsed / num_transactions as u32); - println!("Expected: > 10,000 transactions/sec"); - println!("=========================================\n"); - - // Assert transaction checking performance - assert!( - ops_per_second > 10000.0, - "Should check >10000 transactions/sec, but got {:.2} tx/sec", - ops_per_second - ); -} - #[test] fn test_gap_limit_scan_performance() { use crate::managed_account::address_pool::{AddressPool, AddressPoolType, KeySource}; diff --git a/key-wallet/src/tests/wallet_tests.rs b/key-wallet/src/tests/wallet_tests.rs index ca27d4a29..56ff8246f 100644 --- a/key-wallet/src/tests/wallet_tests.rs +++ b/key-wallet/src/tests/wallet_tests.rs @@ -18,7 +18,7 @@ const TEST_MNEMONIC: &str = #[test] fn test_wallet_creation_random() { let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -41,7 +41,7 @@ fn test_wallet_creation_from_mnemonic() { let wallet = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -69,7 +69,7 @@ fn test_wallet_creation_from_seed() { let wallet = Wallet::from_seed( seed.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -101,7 +101,7 @@ fn test_wallet_creation_from_extended_key() { let wallet = Wallet::from_extended_key( master_key.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -130,7 +130,7 @@ fn test_wallet_creation_watch_only() { let root_pub_key = root_priv_key.to_root_extended_pub_key(); let master_xpub = root_pub_key.to_extended_pub_key(Network::Testnet); - let wallet = Wallet::from_xpub(master_xpub, BTreeMap::new()).unwrap(); + let wallet = Wallet::from_xpub(master_xpub, BTreeMap::new(), false).unwrap(); // Verify wallet properties assert!(wallet.is_watch_only()); @@ -159,7 +159,7 @@ fn test_wallet_creation_with_passphrase() { let wallet = Wallet::from_mnemonic_with_passphrase( mnemonic.clone(), passphrase.to_string(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -190,17 +190,17 @@ fn test_wallet_id_computation() { let root_priv_key = RootExtendedPrivKey::new_master(&seed).unwrap(); let root_pub_key = root_priv_key.to_root_extended_pub_key(); - let wallet_id = Wallet::compute_wallet_id(&root_pub_key); + let wallet_id = Wallet::compute_wallet_id_from_root_extended_pub_key(&root_pub_key); // Wallet ID should be deterministic - let wallet_id_2 = Wallet::compute_wallet_id(&root_pub_key); + let wallet_id_2 = Wallet::compute_wallet_id_from_root_extended_pub_key(&root_pub_key); assert_eq!(wallet_id, wallet_id_2); // Create wallet and verify ID matches let wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -214,13 +214,13 @@ fn test_wallet_recovery_same_mnemonic() { // Create two wallets from the same mnemonic let wallet1 = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); let wallet2 = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -250,7 +250,7 @@ fn test_wallet_multiple_networks() { // Create wallet with Testnet account let mut wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -287,7 +287,7 @@ fn test_wallet_multiple_networks() { #[test] fn test_wallet_account_addition() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -326,7 +326,7 @@ fn test_wallet_account_addition() { #[test] fn test_wallet_duplicate_account_error() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -359,7 +359,7 @@ fn test_wallet_duplicate_account_error() { #[test] fn test_wallet_to_watch_only() { let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -377,7 +377,7 @@ fn test_wallet_to_watch_only() { #[test] fn test_wallet_special_accounts() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -405,7 +405,7 @@ fn test_wallet_deterministic_key_derivation() { let wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -416,7 +416,7 @@ fn test_wallet_deterministic_key_derivation() { let mut test_wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs b/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs index 0727b90c1..a98e19fb3 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/asset_unlock.rs @@ -70,7 +70,7 @@ fn test_asset_unlock_classification() { #[test] fn test_asset_unlock_transaction_routing() { let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = @@ -161,7 +161,7 @@ fn test_asset_unlock_routing_to_bip32_account() { let network = Network::Testnet; // Create wallet with default options (includes both BIP44 and BIP32) - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet"); let mut managed_wallet_info = diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs index 43170d19c..60d6d012b 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/coinbase.rs @@ -65,7 +65,7 @@ fn test_coinbase_transaction_routing_to_bip44_receive_address() { let network = Network::Testnet; // Create a wallet with a BIP44 account - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with BIP44 account for coinbase test"); let mut managed_wallet_info = @@ -140,7 +140,7 @@ fn test_coinbase_transaction_routing_to_bip44_change_address() { let network = Network::Testnet; // Create a wallet with a BIP44 account - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with BIP44 account for coinbase change test"); let mut managed_wallet_info = @@ -214,7 +214,7 @@ fn test_coinbase_transaction_routing_to_bip44_change_address() { fn test_update_state_flag_behavior() { let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); @@ -349,7 +349,7 @@ fn test_coinbase_routing() { fn test_coinbase_transaction_with_payload_routing() { // Test coinbase with special payload routing to BIP44 account let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet"); let mut managed_wallet_info = diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs b/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs index 83dc9ba3c..be1e50bbc 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/identity_transactions.rs @@ -57,7 +57,7 @@ fn test_identity_registration() { fn test_identity_registration_account_routing() { let network = Network::Testnet; - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::None) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::None) .expect("Failed to create wallet without default accounts"); // Add identity registration account @@ -178,7 +178,7 @@ fn test_identity_registration_account_routing() { fn test_normal_payment_to_identity_address_not_detected() { let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs index 0bc7790cf..91c002210 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/provider.rs @@ -125,10 +125,10 @@ fn test_provider_registration_transaction_routing_check_owner_only() { let network = Network::Testnet; // We create another wallet that will hold keys not in our main wallet - let other_wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let other_wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut other_managed_wallet_info = @@ -261,10 +261,10 @@ fn test_provider_registration_transaction_routing_check_voting_only() { let network = Network::Testnet; // We create another wallet that will hold keys not in our main wallet - let other_wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let other_wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut other_managed_wallet_info = @@ -397,10 +397,10 @@ fn test_provider_registration_transaction_routing_check_operator_only() { let network = Network::Testnet; // We create another wallet that will hold keys not in our main wallet - let other_wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let other_wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut other_managed_wallet_info = @@ -579,10 +579,10 @@ fn test_provider_registration_transaction_routing_check_platform_only() { let network = Network::Testnet; // We create another wallet that will hold keys not in our main wallet - let other_wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let other_wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut other_managed_wallet_info = @@ -781,7 +781,7 @@ fn test_provider_update_service_with_operator_key() { fn test_provider_update_registrar_with_voting_and_operator() { // Test provider update registrar classification and routing let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = @@ -853,7 +853,7 @@ fn test_provider_update_registrar_with_voting_and_operator() { fn test_provider_revocation_classification_and_routing() { // Test that provider revocation transactions are properly classified and routed let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = diff --git a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs index 8d0522345..929b70c5e 100644 --- a/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs +++ b/key-wallet/src/transaction_checking/transaction_router/tests/routing.rs @@ -50,7 +50,7 @@ fn test_transaction_routing_to_bip44_account() { let network = Network::Testnet; // Create a wallet with a BIP44 account - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = @@ -110,7 +110,7 @@ fn test_transaction_routing_to_bip32_account() { let network = Network::Testnet; // Create a wallet with BIP32 accounts - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::None) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::None) .expect("Failed to create wallet without default accounts"); // Add a BIP32 account @@ -200,7 +200,7 @@ fn test_transaction_routing_to_coinjoin_account() { let network = Network::Testnet; // Create a wallet and add a CoinJoin account - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::None) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::None) .expect("Failed to create wallet without default accounts"); let account_type = AccountType::CoinJoin { @@ -296,7 +296,7 @@ fn test_transaction_affects_multiple_accounts() { let network = Network::Testnet; // Create a wallet with multiple accounts - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); // Add another BIP44 account @@ -421,7 +421,7 @@ fn test_transaction_affects_multiple_accounts() { fn test_next_address_method_restrictions() { let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Failed to create wallet with default options"); let mut managed_wallet_info = ManagedWalletInfo::from_wallet_with_name(&wallet, "Test".to_string()); diff --git a/key-wallet/src/transaction_checking/wallet_checker.rs b/key-wallet/src/transaction_checking/wallet_checker.rs index a24bb79e7..ed675c79c 100644 --- a/key-wallet/src/transaction_checking/wallet_checker.rs +++ b/key-wallet/src/transaction_checking/wallet_checker.rs @@ -295,7 +295,7 @@ mod tests { let other_network = Network::Dash; // Create wallet on testnet but check transaction on mainnet - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Should create wallet"); let mut managed_wallet = @@ -326,7 +326,7 @@ mod tests { let network = Network::Testnet; // Create wallet with multiple account types - let mut wallet = Wallet::new_random(network, WalletAccountCreationOptions::None) + let mut wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::None) .expect("Should create wallet"); // Add different types of accounts @@ -428,7 +428,7 @@ mod tests { #[test] fn test_wallet_checker_coinbase_immature_handling() { let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Should create wallet"); let mut managed_wallet = @@ -492,7 +492,7 @@ mod tests { #[test] fn test_wallet_checker_mempool_context() { let network = Network::Testnet; - let wallet = Wallet::new_random(network, WalletAccountCreationOptions::Default) + let wallet = Wallet::new_random(&[network], WalletAccountCreationOptions::Default) .expect("Should create wallet"); let mut managed_wallet = diff --git a/key-wallet/src/wallet/backup.rs b/key-wallet/src/wallet/backup.rs index 171484dc1..e80e3f74b 100644 --- a/key-wallet/src/wallet/backup.rs +++ b/key-wallet/src/wallet/backup.rs @@ -18,7 +18,7 @@ impl Wallet { /// use key_wallet::wallet::Wallet; /// /// let wallet = Wallet::new_random( - /// key_wallet::Network::Testnet, + /// &[key_wallet::Network::Testnet], /// key_wallet::wallet::initialization::WalletAccountCreationOptions::Default, /// ).unwrap(); /// @@ -71,7 +71,7 @@ mod tests { let original = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], WalletAccountCreationOptions::Default, ) .unwrap(); diff --git a/key-wallet/src/wallet/initialization.rs b/key-wallet/src/wallet/initialization.rs index d291de607..db7f3b27e 100644 --- a/key-wallet/src/wallet/initialization.rs +++ b/key-wallet/src/wallet/initialization.rs @@ -81,10 +81,10 @@ impl Wallet { /// Create a new wallet with a randomly generated mnemonic /// /// # Arguments - /// * `network` - Network for the wallet + /// * `networks` - List of networks to create accounts for /// * `account_creation_options` - Specifies which accounts to create during initialization pub fn new_random( - network: Network, + networks: &[Network], account_creation_options: WalletAccountCreationOptions, ) -> Result { let mnemonic = Mnemonic::generate(12, Language::English)?; @@ -96,8 +96,10 @@ impl Wallet { root_extended_private_key, }); - // Create accounts based on options - wallet.create_accounts_from_options(account_creation_options, network)?; + // Create accounts for each network + for network in networks { + wallet.create_accounts_from_options(account_creation_options.clone(), *network)?; + } Ok(wallet) } @@ -124,7 +126,7 @@ impl Wallet { | WalletType::ExternalSignable(root_extended_public_key) | WalletType::WatchOnly(root_extended_public_key) => root_extended_public_key.clone(), }; - let wallet_id = Self::compute_wallet_id(&root_pub_key); + let wallet_id = Self::compute_wallet_id_from_root_extended_pub_key(&root_pub_key); Self { wallet_id, @@ -137,11 +139,11 @@ impl Wallet { /// /// # Arguments /// * `mnemonic` - The mnemonic phrase - /// * `network` - Network for the wallet + /// * `networks` - List of networks to create accounts for /// * `account_creation_options` - Specifies which accounts to create during initialization pub fn from_mnemonic( mnemonic: Mnemonic, - network: Network, + networks: &[Network], account_creation_options: WalletAccountCreationOptions, ) -> Result { let seed = mnemonic.to_seed(""); @@ -152,8 +154,10 @@ impl Wallet { root_extended_private_key, }); - // Create accounts based on options - wallet.create_accounts_from_options(account_creation_options, network)?; + // Create accounts for each network + for network in networks { + wallet.create_accounts_from_options(account_creation_options.clone(), *network)?; + } Ok(wallet) } @@ -164,12 +168,12 @@ impl Wallet { /// # Arguments /// * `mnemonic` - The mnemonic phrase /// * `passphrase` - The BIP39 passphrase - /// * `network` - Network for the wallet + /// * `networks` - List of networks to create accounts for /// * `account_creation_options` - Specifies which accounts to create during initialization pub fn from_mnemonic_with_passphrase( mnemonic: Mnemonic, passphrase: String, - network: Network, + networks: &[Network], account_creation_options: WalletAccountCreationOptions, ) -> Result { let seed = mnemonic.to_seed(&passphrase); @@ -182,43 +186,58 @@ impl Wallet { root_extended_public_key, }); - // Create accounts based on options - wallet.create_accounts_with_passphrase_from_options( - account_creation_options, - passphrase.as_str(), - network, - )?; + // Create accounts for each network + for network in networks { + wallet.create_accounts_with_passphrase_from_options( + account_creation_options.clone(), + passphrase.as_str(), + *network, + )?; + } Ok(wallet) } - /// Create a watch-only wallet from extended public key + /// Create a watch-only or externally signable wallet from extended public key /// /// Watch-only wallets can generate addresses and monitor transactions but cannot sign. - /// This is useful for cold storage setups where the private keys are kept offline. + /// Externally signable wallets can also create unsigned transactions that can be signed by + /// external devices (hardware wallets, remote signing services, etc.). /// /// # Arguments /// * `master_xpub` - The master extended public key for the wallet /// * `accounts` - Pre-created account collections mapped by network. Since watch-only wallets /// cannot derive private keys, all accounts must be provided with their extended /// public keys already initialized. + /// * `can_sign_externally` - If true, creates an externally signable wallet that supports + /// transaction creation for external signing. If false, creates a pure watch-only wallet. /// /// # Returns - /// A new watch-only wallet instance + /// A new watch-only or externally signable wallet instance /// /// # Example /// ```ignore /// let accounts = BTreeMap::from([ /// (Network::Mainnet, account_collection), /// ]); - /// let wallet = Wallet::from_xpub(master_xpub, None, accounts)?; + /// // Create a pure watch-only wallet + /// let watch_wallet = Wallet::from_xpub(master_xpub, accounts.clone(), false)?; + /// + /// // Create an externally signable wallet (e.g., for hardware wallet) + /// let hw_wallet = Wallet::from_xpub(master_xpub, accounts, true)?; /// ``` pub fn from_xpub( master_xpub: ExtendedPubKey, accounts: BTreeMap, + can_sign_externally: bool, ) -> Result { let root_extended_public_key = RootExtendedPubKey::from_extended_pub_key(&master_xpub); - let mut wallet = Self::from_wallet_type(WalletType::WatchOnly(root_extended_public_key)); + let wallet_type = if can_sign_externally { + WalletType::ExternalSignable(root_extended_public_key) + } else { + WalletType::WatchOnly(root_extended_public_key) + }; + let mut wallet = Self::from_wallet_type(wallet_type); wallet.accounts = accounts; @@ -272,11 +291,11 @@ impl Wallet { /// /// # Arguments /// * `seed` - The seed bytes - /// * `network` - Network for the wallet + /// * `networks` - List of networks to create accounts for /// * `account_creation_options` - Specifies which accounts to create during initialization pub fn from_seed( seed: Seed, - network: Network, + networks: &[Network], account_creation_options: WalletAccountCreationOptions, ) -> Result { let root_extended_private_key = RootExtendedPrivKey::new_master(seed.as_slice())?; @@ -286,8 +305,10 @@ impl Wallet { root_extended_private_key, }); - // Create accounts based on options - wallet.create_accounts_from_options(account_creation_options, network)?; + // Create accounts for each network + for network in networks { + wallet.create_accounts_from_options(account_creation_options.clone(), *network)?; + } Ok(wallet) } @@ -296,33 +317,35 @@ impl Wallet { /// /// # Arguments /// * `seed_bytes` - The seed bytes array - /// * `network` - Network for the wallet + /// * `networks` - List of networks to create accounts for /// * `account_creation_options` - Specifies which accounts to create during initialization pub fn from_seed_bytes( seed_bytes: [u8; 64], - network: Network, + networks: &[Network], account_creation_options: WalletAccountCreationOptions, ) -> Result { - Self::from_seed(Seed::new(seed_bytes), network, account_creation_options) + Self::from_seed(Seed::new(seed_bytes), networks, account_creation_options) } /// Create a wallet from an extended private key /// /// # Arguments /// * `master_key` - The extended private key - /// * `network` - Network for the wallet + /// * `networks` - List of networks to create accounts for /// * `account_creation_options` - Specifies which accounts to create during initialization pub fn from_extended_key( master_key: ExtendedPrivKey, - network: Network, + networks: &[Network], account_creation_options: WalletAccountCreationOptions, ) -> Result { let root_extended_private_key = RootExtendedPrivKey::from_extended_priv_key(&master_key); let mut wallet = Self::from_wallet_type(WalletType::ExtendedPrivKey(root_extended_private_key)); - // Create accounts based on options - wallet.create_accounts_from_options(account_creation_options, network)?; + // Create accounts for each network + for network in networks { + wallet.create_accounts_from_options(account_creation_options.clone(), *network)?; + } Ok(wallet) } diff --git a/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs b/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs index 60ef822a6..eba4b8756 100644 --- a/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs +++ b/key-wallet/src/wallet/managed_wallet_info/managed_accounts.rs @@ -394,7 +394,7 @@ mod tests { fn test_add_managed_account() { // Create a test wallet without BLS accounts to avoid that complexity let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap(); diff --git a/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs index fe6258ed2..b862e9290 100644 --- a/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs +++ b/key-wallet/src/wallet/managed_wallet_info/wallet_info_interface.rs @@ -18,16 +18,13 @@ use std::collections::BTreeSet; /// Trait that wallet info types must implement to work with WalletManager pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccountOperations { - /// Create a new wallet info with the given ID and name - fn with_name(wallet_id: [u8; 32], name: String) -> Self; + /// Create a wallet info from an existing wallet + /// This properly initializes the wallet info from the wallet's state + fn from_wallet(wallet: &Wallet) -> Self; /// Create a wallet info from an existing wallet with proper account initialization /// Default implementation just uses with_name (backward compatibility) - fn from_wallet_with_name(wallet: &Wallet, name: String) -> Self { - // Default implementation for backward compatibility - // Types can override this to properly initialize from wallet accounts - Self::with_name(wallet.wallet_id, name) - } + fn from_wallet_with_name(wallet: &Wallet, name: String) -> Self; /// Get the wallet's unique ID fn wallet_id(&self) -> [u8; 32]; @@ -114,8 +111,8 @@ pub trait WalletInfoInterface: Sized + WalletTransactionChecker + ManagedAccount /// Default implementation for ManagedWalletInfo impl WalletInfoInterface for ManagedWalletInfo { - fn with_name(wallet_id: [u8; 32], name: String) -> Self { - Self::with_name(wallet_id, name) + fn from_wallet(wallet: &Wallet) -> Self { + Self::from_wallet_with_name(wallet, String::new()) } fn from_wallet_with_name(wallet: &Wallet, name: String) -> Self { diff --git a/key-wallet/src/wallet/mod.rs b/key-wallet/src/wallet/mod.rs index e53dfd70e..a9f2a8b79 100644 --- a/key-wallet/src/wallet/mod.rs +++ b/key-wallet/src/wallet/mod.rs @@ -31,6 +31,7 @@ use core::fmt; use dashcore_hashes::{sha256, Hash}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use zeroize::Zeroize; /// Type of wallet based on how it was created #[derive(Debug, Clone)] @@ -89,7 +90,9 @@ pub struct WalletScanResult { impl Wallet { /// Compute wallet ID from root public key - pub fn compute_wallet_id(root_pub_key: &RootExtendedPubKey) -> [u8; 32] { + pub fn compute_wallet_id_from_root_extended_pub_key( + root_pub_key: &RootExtendedPubKey, + ) -> [u8; 32] { let mut data = Vec::new(); data.extend_from_slice(&root_pub_key.root_public_key.serialize()); data.extend_from_slice(&root_pub_key.root_chain_code[..]); @@ -98,6 +101,11 @@ impl Wallet { let hash = sha256::Hash::hash(&data); hash.to_byte_array() } + + /// Compute wallet ID + pub fn compute_wallet_id(&self) -> [u8; 32] { + Self::compute_wallet_id_from_root_extended_pub_key(&self.root_extended_pub_key_cow()) + } } impl fmt::Display for Wallet { @@ -123,6 +131,59 @@ impl fmt::Display for Wallet { } } +// Manual implementation of Zeroize for Wallet +impl Zeroize for Wallet { + fn zeroize(&mut self) { + // Zeroize the wallet ID + self.wallet_id.zeroize(); + + // Zeroize the wallet type - handle each variant's sensitive data + match &mut self.wallet_type { + WalletType::Mnemonic { + mnemonic, + root_extended_private_key, + } => { + // Zeroize the mnemonic (now possible since it implements Zeroize) + mnemonic.zeroize(); + // We can't zeroize SecretKey directly, but we can zeroize the chain code + root_extended_private_key.zeroize(); + // Note: root_extended_private_key.root_private_key (SecretKey) doesn't implement Zeroize + } + WalletType::MnemonicWithPassphrase { + mnemonic, + root_extended_public_key, + } => { + // Zeroize the mnemonic + mnemonic.zeroize(); + // Zeroize the public key structure (best effort) + root_extended_public_key.zeroize(); + } + WalletType::Seed { + seed, + root_extended_private_key, + } => { + // We can't zeroize Seed directly as it doesn't implement Zeroize yet + // But we can zeroize the RootExtendedPrivKey + root_extended_private_key.zeroize(); + seed.zeroize(); + } + WalletType::ExtendedPrivKey(root_extended_private_key) => { + // Zeroize the chain code + root_extended_private_key.zeroize(); + // Note: root_private_key (SecretKey) doesn't implement Zeroize + } + WalletType::ExternalSignable(root_extended_public_key) + | WalletType::WatchOnly(root_extended_public_key) => { + // Public keys are not sensitive, but zeroize for consistency + root_extended_public_key.zeroize(); + } + } + + // Clear the accounts map, only public keys here so no need to go hardcore on zeroization + self.accounts.clear(); + } +} + #[cfg(test)] mod passphrase_test; @@ -136,7 +197,7 @@ mod tests { #[test] fn test_wallet_creation() { let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -155,7 +216,7 @@ mod tests { let wallet = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -180,7 +241,7 @@ mod tests { let mut bip44_set = BTreeSet::new(); bip44_set.insert(0); let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::BIP44AccountsOnly(bip44_set), ) .unwrap(); @@ -216,7 +277,7 @@ mod tests { // where Account holds immutable state and ManagedAccount holds mutable state let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -236,7 +297,7 @@ mod tests { let wallet = Wallet::from_mnemonic( mnemonic, - Network::Dash, + &[Network::Dash], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -255,7 +316,7 @@ mod tests { // Create first wallet let wallet1 = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -263,7 +324,7 @@ mod tests { // Create second wallet from same mnemonic (simulating recovery) let wallet2 = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -288,7 +349,7 @@ mod tests { #[test] fn test_multiple_account_creation() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -337,7 +398,7 @@ mod tests { #[test] fn test_wallet_with_managed_info() { let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -369,7 +430,7 @@ mod tests { // Create a regular wallet first to get the root xpub let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -379,7 +440,8 @@ mod tests { let root_xpub_as_extended = root_xpub.to_extended_pub_key(Network::Testnet); // Create watch-only wallet from root xpub - let mut watch_only = Wallet::from_xpub(root_xpub_as_extended, BTreeMap::new()).unwrap(); + let mut watch_only = + Wallet::from_xpub(root_xpub_as_extended, BTreeMap::new(), false).unwrap(); assert!(watch_only.is_watch_only()); assert!(!watch_only.has_mnemonic()); @@ -421,7 +483,7 @@ mod tests { // Create wallet without passphrase - use regular from_mnemonic for empty passphrase let wallet1 = Wallet::from_mnemonic( mnemonic.clone(), - network, + &[network], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -430,7 +492,7 @@ mod tests { let wallet2 = Wallet::from_mnemonic_with_passphrase( mnemonic, "TREZOR".to_string(), - network, + &[network], initialization::WalletAccountCreationOptions::None, ) .unwrap(); @@ -445,7 +507,7 @@ mod tests { #[test] fn test_account_management() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::BIP44AccountsOnly([0].into()), ) .unwrap(); @@ -485,7 +547,7 @@ mod tests { #[test] fn test_wallet_error_conditions() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -509,7 +571,7 @@ mod tests { #[test] fn test_wallet_id_generation() { let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -518,8 +580,7 @@ mod tests { assert_ne!(wallet.wallet_id, [0u8; 32]); // Wallet ID should be deterministic based on root public key - let root_pub_key = wallet.root_extended_pub_key(); - let computed_id = Wallet::compute_wallet_id(&root_pub_key); + let computed_id = wallet.compute_wallet_id(); assert_eq!(wallet.wallet_id, computed_id); // Test that wallets from the same mnemonic have the same ID @@ -530,13 +591,13 @@ mod tests { let wallet1 = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); let wallet2 = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], initialization::WalletAccountCreationOptions::Default, ) .unwrap(); diff --git a/key-wallet/src/wallet/passphrase_test.rs b/key-wallet/src/wallet/passphrase_test.rs index a89dfe71a..a69e7f5bc 100644 --- a/key-wallet/src/wallet/passphrase_test.rs +++ b/key-wallet/src/wallet/passphrase_test.rs @@ -21,7 +21,7 @@ mod tests { let wallet = Wallet::from_mnemonic_with_passphrase( mnemonic.clone(), passphrase.to_string(), - network, + &[network], WalletAccountCreationOptions::None, ) .expect("Should create wallet with passphrase"); @@ -70,7 +70,7 @@ mod tests { let wallet = Wallet::from_mnemonic_with_passphrase( mnemonic, passphrase.to_string(), - network, + &[network], WalletAccountCreationOptions::None, ) .expect("Should create wallet"); @@ -104,7 +104,7 @@ mod tests { let mut wallet = Wallet::from_mnemonic_with_passphrase( mnemonic, passphrase.to_string(), - network, + &[network], WalletAccountCreationOptions::None, ) .expect("Should create wallet"); @@ -150,7 +150,7 @@ mod tests { // Create regular wallet WITHOUT passphrase let mut wallet = - Wallet::from_mnemonic(mnemonic, network, WalletAccountCreationOptions::Default) + Wallet::from_mnemonic(mnemonic, &[network], WalletAccountCreationOptions::Default) .expect("Should create wallet"); // Try to use add_account_with_passphrase - should fail diff --git a/key-wallet/src/wallet/root_extended_keys.rs b/key-wallet/src/wallet/root_extended_keys.rs index 796affbd6..8572cc1ef 100644 --- a/key-wallet/src/wallet/root_extended_keys.rs +++ b/key-wallet/src/wallet/root_extended_keys.rs @@ -3,6 +3,7 @@ use crate::bip32::{ChainCode, ChildNumber, ExtendedPrivKey, ExtendedPubKey}; use crate::derivation_bls_bip32::ExtendedBLSPrivKey; use crate::wallet::WalletType; use crate::{Error, Network, Wallet}; +use alloc::borrow::Cow; #[cfg(feature = "bincode")] use bincode::{BorrowDecode, Decode, Encode}; #[cfg(feature = "bls")] @@ -19,6 +20,13 @@ pub struct RootExtendedPrivKey { pub root_chain_code: ChainCode, } +impl zeroize::Zeroize for RootExtendedPrivKey { + fn zeroize(&mut self) { + self.root_private_key.non_secure_erase(); + self.root_chain_code.zeroize(); + } +} + impl RootExtendedPrivKey { /// Create a new RootExtendedPrivKey pub fn new(root_private_key: secp256k1::SecretKey, root_chain_code: ChainCode) -> Self { @@ -234,6 +242,22 @@ pub struct RootExtendedPubKey { pub root_chain_code: ChainCode, } +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(); + } +} + impl RootExtendedPubKey { /// Create a new RootExtendedPubKey pub fn new(root_public_key: secp256k1::PublicKey, root_chain_code: ChainCode) -> Self { @@ -348,6 +372,33 @@ impl Wallet { } } + /// Get the root extended public key from the wallet type as Cow + pub fn root_extended_pub_key_cow(&self) -> Cow<'_, RootExtendedPubKey> { + match &self.wallet_type { + WalletType::Mnemonic { + root_extended_private_key, + .. + } => Cow::Owned(root_extended_private_key.to_root_extended_pub_key()), + WalletType::MnemonicWithPassphrase { + root_extended_public_key, + .. + } => Cow::Borrowed(root_extended_public_key), + WalletType::Seed { + root_extended_private_key, + .. + } => Cow::Owned(root_extended_private_key.to_root_extended_pub_key()), + WalletType::ExtendedPrivKey(root_extended_priv_key) => { + Cow::Owned(root_extended_priv_key.to_root_extended_pub_key()) + } + WalletType::ExternalSignable(root_extended_public_key) => { + Cow::Borrowed(root_extended_public_key) + } + WalletType::WatchOnly(root_extended_public_key) => { + Cow::Borrowed(root_extended_public_key) + } + } + } + /// Get the root extended private key from the wallet type pub(crate) fn root_extended_priv_key(&self) -> crate::Result<&RootExtendedPrivKey> { match &self.wallet_type { diff --git a/key-wallet/src/wallet_comprehensive_tests.rs b/key-wallet/src/wallet_comprehensive_tests.rs index 7b0039c71..73e565206 100644 --- a/key-wallet/src/wallet_comprehensive_tests.rs +++ b/key-wallet/src/wallet_comprehensive_tests.rs @@ -22,7 +22,7 @@ mod tests { #[test] fn test_wallet_creation() { let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -39,13 +39,13 @@ mod tests { let wallet1 = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); let wallet2 = Wallet::from_mnemonic( mnemonic, - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -77,7 +77,7 @@ mod tests { #[test] fn test_multiple_accounts() { let mut wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -122,7 +122,7 @@ mod tests { #[test] fn test_watch_only_wallet() { let wallet = Wallet::new_random( - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -132,7 +132,7 @@ mod tests { let root_xpub_as_extended = root_xpub.to_extended_pub_key(Network::Testnet); // Create watch-only wallet from the root xpub - let watch_only = Wallet::from_xpub(root_xpub_as_extended, BTreeMap::new()).unwrap(); + let watch_only = Wallet::from_xpub(root_xpub_as_extended, BTreeMap::new(), false).unwrap(); assert!(watch_only.is_watch_only()); assert!(!watch_only.has_mnemonic()); @@ -154,7 +154,7 @@ mod tests { // Create wallet without passphrase - use regular from_mnemonic for empty passphrase let wallet1 = Wallet::from_mnemonic( mnemonic.clone(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::Default, ) .unwrap(); @@ -163,7 +163,7 @@ mod tests { let wallet2 = Wallet::from_mnemonic_with_passphrase( mnemonic, "TREZOR".to_string(), - Network::Testnet, + &[Network::Testnet], crate::wallet::initialization::WalletAccountCreationOptions::None, ) .unwrap();