diff --git a/dash-spv-ffi/Cargo.toml b/dash-spv-ffi/Cargo.toml index 7eb8ceddd..1e6a5d831 100644 --- a/dash-spv-ffi/Cargo.toml +++ b/dash-spv-ffi/Cargo.toml @@ -23,8 +23,11 @@ log = "0.4" hex = "0.4" env_logger = "0.10" tracing = "0.1" -key-wallet-manager = { path = "../key-wallet-manager" } +# Use key-wallet-ffi for all wallet-related FFI types +key-wallet-ffi = { path = "../key-wallet-ffi" } +# Still need these for SPV client internals (not for FFI types) key-wallet = { path = "../key-wallet" } +key-wallet-manager = { path = "../key-wallet-manager" } rand = "0.8" [dev-dependencies] diff --git a/dash-spv-ffi/README.md b/dash-spv-ffi/README.md index bf0801ecf..82e7a4e02 100644 --- a/dash-spv-ffi/README.md +++ b/dash-spv-ffi/README.md @@ -97,19 +97,10 @@ dash_spv_ffi_config_destroy(config); ### Wallet Operations -- `dash_spv_ffi_client_add_watch_item(client, item)` - Add address/script to watch -- `dash_spv_ffi_client_remove_watch_item(client, item)` - Remove watch item - `dash_spv_ffi_client_get_address_balance(client, address)` - Get address balance - `dash_spv_ffi_client_get_utxos(client)` - Get all UTXOs - `dash_spv_ffi_client_get_utxos_for_address(client, address)` - Get UTXOs for address -### Watch Items - -- `dash_spv_ffi_watch_item_address(address)` - Create address watch item -- `dash_spv_ffi_watch_item_script(script_hex)` - Create script watch item -- `dash_spv_ffi_watch_item_outpoint(txid, vout)` - Create outpoint watch item -- `dash_spv_ffi_watch_item_destroy(item)` - Free watch item memory - ### Error Handling - `dash_spv_ffi_get_last_error()` - Get last error message diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index 094b8181a..15c3a4442 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -3,79 +3,6 @@ #include #include -typedef enum FFIAccountType { - /** - * Standard BIP44 account for regular transactions - */ - BIP44 = 0, - /** - * Standard BIP32 account for regular transactions - */ - BIP32 = 1, - /** - * CoinJoin account for private transactions - */ - CoinJoin = 2, - /** - * Identity registration funding - */ - IdentityRegistration = 3, - /** - * Identity top-up funding - */ - IdentityTopUp = 4, - /** - * Identity invitation funding - */ - IdentityInvitation = 5, - /** - * Provider voting keys (DIP-3) - */ - ProviderVotingKeys = 6, - /** - * Provider owner keys (DIP-3) - */ - ProviderOwnerKeys = 7, - /** - * Provider operator keys (DIP-3) - */ - ProviderOperatorKeys = 8, - /** - * Provider platform P2P keys (DIP-3, ED25519) - */ - ProviderPlatformKeys = 9, -} FFIAccountType; - -typedef enum FFIAccountTypePreference { - /** - * Use BIP44 account only - */ - BIP44 = 0, - /** - * Use BIP32 account only - */ - BIP32 = 1, - /** - * Prefer BIP44, fallback to BIP32 - */ - PreferBIP44 = 2, - /** - * Prefer BIP32, fallback to BIP44 - */ - PreferBIP32 = 3, -} FFIAccountTypePreference; - -typedef enum FFIAccountTypeUsed { - /** - * BIP44 account was used - */ - BIP44 = 0, - /** - * BIP32 account was used - */ - BIP32 = 1, -} FFIAccountTypeUsed; - typedef enum FFIMempoolStrategy { FetchAll = 0, BloomFilter = 1, @@ -105,29 +32,6 @@ typedef enum FFIValidationMode { Full = 2, } FFIValidationMode; -typedef enum FFIWalletAccountCreationOptions { - /** - * Default account creation: Creates account 0 for BIP44, account 0 for CoinJoin, - * and all special purpose accounts (Identity Registration, Identity Invitation, - * Provider keys, etc.) - */ - Default = 0, - /** - * Create only BIP44 accounts (no CoinJoin or special accounts) - */ - BIP44AccountsOnly = 1, - /** - * Create no accounts at all - useful for tests that want to manually control account creation - */ - None = 2, -} FFIWalletAccountCreationOptions; - -typedef enum FFIWatchItemType { - Address = 0, - Script = 1, - Outpoint = 2, -} FFIWatchItemType; - typedef struct FFIClientConfig FFIClientConfig; /** @@ -181,20 +85,6 @@ typedef struct FFISpvStats { uint64_t uptime; } FFISpvStats; -typedef struct FFIWatchItem { - enum FFIWatchItemType item_type; - struct FFIString data; -} FFIWatchItem; - -typedef struct FFIBalance { - uint64_t confirmed; - uint64_t pending; - uint64_t instantlocked; - uint64_t mempool; - uint64_t mempool_instant; - uint64_t total; -} FFIBalance; - /** * FFI-safe array that transfers ownership of memory to the C caller. * @@ -319,57 +209,6 @@ typedef struct FFIUnconfirmedTransaction { uintptr_t addresses_len; } FFIUnconfirmedTransaction; -typedef struct FFIUtxo { - struct FFIString txid; - uint32_t vout; - uint64_t amount; - struct FFIString script_pubkey; - struct FFIString address; - uint32_t height; - bool is_coinbase; - bool is_confirmed; - bool is_instantlocked; -} FFIUtxo; - -typedef struct FFITransactionResult { - struct FFIString txid; - int32_t version; - uint32_t locktime; - uint32_t size; - uint32_t weight; - uint64_t fee; - uint64_t confirmation_time; - uint32_t confirmation_height; -} FFITransactionResult; - -typedef struct FFIBlockResult { - struct FFIString hash; - uint32_t height; - uint32_t time; - uint32_t tx_count; -} FFIBlockResult; - -typedef struct FFIFilterMatch { - struct FFIString block_hash; - uint32_t height; - bool block_requested; -} FFIFilterMatch; - -typedef struct FFIAddressStats { - struct FFIString address; - uint32_t utxo_count; - uint64_t total_value; - uint64_t confirmed_value; - uint64_t pending_value; - uint32_t spendable_count; - uint32_t coinbase_count; -} FFIAddressStats; - -typedef struct FFIAddressGenerationResult { - struct FFIString *address; - enum FFIAccountTypeUsed account_type_used; -} FFIAddressGenerationResult; - struct FFIDashSpvClient *dash_spv_ffi_client_new(const struct FFIClientConfig *config); int32_t dash_spv_ffi_client_start(struct FFIDashSpvClient *client); @@ -473,14 +312,8 @@ struct FFISpvStats *dash_spv_ffi_client_get_stats(struct FFIDashSpvClient *clien bool dash_spv_ffi_client_is_filter_sync_available(struct FFIDashSpvClient *client); -int32_t dash_spv_ffi_client_add_watch_item(struct FFIDashSpvClient *client, - const struct FFIWatchItem *item); - -int32_t dash_spv_ffi_client_remove_watch_item(struct FFIDashSpvClient *client, - const struct FFIWatchItem *item); - -struct FFIBalance *dash_spv_ffi_client_get_address_balance(struct FFIDashSpvClient *client, - const char *address); +FFIBalance *dash_spv_ffi_client_get_address_balance(struct FFIDashSpvClient *client, + const char *address); struct FFIArray dash_spv_ffi_client_get_utxos(struct FFIDashSpvClient *client); @@ -517,7 +350,7 @@ struct FFIArray dash_spv_ffi_client_get_watched_addresses(struct FFIDashSpvClien struct FFIArray dash_spv_ffi_client_get_watched_scripts(struct FFIDashSpvClient *client); -struct FFIBalance *dash_spv_ffi_client_get_total_balance(struct FFIDashSpvClient *client); +FFIBalance *dash_spv_ffi_client_get_total_balance(struct FFIDashSpvClient *client); int32_t dash_spv_ffi_client_rescan_blockchain(struct FFIDashSpvClient *client, uint32_t _from_height); @@ -536,14 +369,14 @@ struct FFIArray dash_spv_ffi_client_get_address_utxos(struct FFIDashSpvClient *c int32_t dash_spv_ffi_client_enable_mempool_tracking(struct FFIDashSpvClient *client, enum FFIMempoolStrategy strategy); -struct FFIBalance *dash_spv_ffi_client_get_balance_with_mempool(struct FFIDashSpvClient *client); +FFIBalance *dash_spv_ffi_client_get_balance_with_mempool(struct FFIDashSpvClient *client); int32_t dash_spv_ffi_client_get_mempool_transaction_count(struct FFIDashSpvClient *client); int32_t dash_spv_ffi_client_record_send(struct FFIDashSpvClient *client, const char *txid); -struct FFIBalance *dash_spv_ffi_client_get_mempool_balance(struct FFIDashSpvClient *client, - const char *address); +FFIBalance *dash_spv_ffi_client_get_mempool_balance(struct FFIDashSpvClient *client, + const char *address); struct FFIClientConfig *dash_spv_ffi_config_new(enum FFINetwork network); @@ -702,271 +535,3 @@ const char *dash_spv_ffi_version(void); const char *dash_spv_ffi_get_network_name(enum FFINetwork network); void dash_spv_ffi_enable_test_mode(void); - -struct FFIWatchItem *dash_spv_ffi_watch_item_address(const char *address); - -struct FFIWatchItem *dash_spv_ffi_watch_item_script(const char *script_hex); - -struct FFIWatchItem *dash_spv_ffi_watch_item_outpoint(const char *txid, uint32_t vout); - -void dash_spv_ffi_watch_item_destroy(struct FFIWatchItem *item); - -void dash_spv_ffi_balance_destroy(struct FFIBalance *balance); - -void dash_spv_ffi_utxo_destroy(struct FFIUtxo *utxo); - -void dash_spv_ffi_transaction_result_destroy(struct FFITransactionResult *tx); - -void dash_spv_ffi_block_result_destroy(struct FFIBlockResult *block); - -void dash_spv_ffi_filter_match_destroy(struct FFIFilterMatch *filter_match); - -void dash_spv_ffi_address_stats_destroy(struct FFIAddressStats *stats); - -int32_t dash_spv_ffi_validate_address(const char *address, enum FFINetwork network); - -struct FFIArray *dash_spv_ffi_wallet_get_monitored_addresses(struct FFIDashSpvClient *client, - enum FFINetwork network); - -struct FFIBalance *dash_spv_ffi_wallet_get_balance(struct FFIDashSpvClient *client, - const char *wallet_id_ptr); - -struct FFIArray dash_spv_ffi_wallet_get_utxos(struct FFIDashSpvClient *client, - const char *wallet_id_ptr); - -/** - * Create a new wallet from mnemonic phrase - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `mnemonic` - The mnemonic phrase as null-terminated C string - * * `passphrase` - Optional BIP39 passphrase (can be null/empty) - * * `network` - The network to use - * * `account_options` - Account creation options - * * `name` - Wallet name as null-terminated C string - * * `birth_height` - Optional birth height (can be 0 for none) - * - * # Returns - * * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) - * * Returns null on error (check last_error) - */ -struct FFIString *dash_spv_ffi_wallet_create_from_mnemonic(struct FFIDashSpvClient *client, - const char *mnemonic, - const char *passphrase, - enum FFINetwork network, - enum FFIWalletAccountCreationOptions account_options, - const char *name, - uint32_t birth_height); - -/** - * Create a new empty wallet (test wallet with fixed mnemonic) - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `network` - The network to use - * * `account_options` - Account creation options - * * `name` - Wallet name as null-terminated C string - * - * # Returns - * * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) - * * Returns null on error (check last_error) - */ -struct FFIString *dash_spv_ffi_wallet_create(struct FFIDashSpvClient *client, - enum FFINetwork network, - enum FFIWalletAccountCreationOptions account_options, - const char *name); - -/** - * Get a list of all wallet IDs - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * - * # Returns - * * FFIArray of FFIString objects containing hex-encoded WalletIds - */ -struct FFIArray dash_spv_ffi_wallet_list(struct FFIDashSpvClient *client); - -/** - * Import a wallet from an extended private key - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `xprv` - The extended private key string (base58check encoded) - * * `network` - The network to use - * * `account_options` - Account creation options - * * `name` - Wallet name as null-terminated C string - * - * # Returns - * * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) - * * Returns null on error (check last_error) - */ -struct FFIString *dash_spv_ffi_wallet_import_from_xprv(struct FFIDashSpvClient *client, - const char *xprv, - enum FFINetwork network, - enum FFIWalletAccountCreationOptions account_options, - const char *name); - -/** - * Import a watch-only wallet from an extended public key - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `xpub` - The extended public key string (base58check encoded) - * * `network` - The network to use - * * `name` - Wallet name as null-terminated C string - * - * # Returns - * * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) - * * Returns null on error (check last_error) - */ -struct FFIString *dash_spv_ffi_wallet_import_from_xpub(struct FFIDashSpvClient *client, - const char *xpub, - enum FFINetwork network, - const char *name); - -/** - * Add a new account to an existing wallet from an extended public key - * - * This creates a watch-only account that can monitor addresses and transactions - * but cannot sign them. - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) - * * `xpub` - The extended public key string (base58check encoded) - * * `account_type` - The type of account to create - * * `network` - The network for the account - * * `account_index` - Account index (required for BIP44, BIP32, CoinJoin) - * * `registration_index` - Registration index (required for IdentityTopUp) - * - * # Returns - * * FFIErrorCode::Success on success - * * FFIErrorCode::InvalidArgument on error (check last_error) - */ -int32_t dash_spv_ffi_wallet_add_account_from_xpub(struct FFIDashSpvClient *client, - const char *wallet_id_hex, - const char *xpub, - enum FFIAccountType account_type, - enum FFINetwork network, - uint32_t account_index, - uint32_t registration_index); - -/** - * Get wallet-wide mempool balance - * - * This returns the total unconfirmed balance (mempool transactions) across all - * accounts in the specified wallet. This represents the balance from transactions - * that have been broadcast but not yet confirmed in a block. - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `wallet_id_hex` - Hex-encoded wallet ID (64 characters), or null for all wallets - * * `network` - The network for which to get mempool balance - * - * # Returns - * * Total mempool balance in satoshis - * * Returns 0 if wallet not found or client not initialized (check last_error) - */ -uint64_t dash_spv_ffi_wallet_get_mempool_balance(struct FFIDashSpvClient *client, - const char *wallet_id_hex, - enum FFINetwork network); - -/** - * Get wallet-wide mempool transaction count - * - * This returns the total number of unconfirmed transactions (in mempool) across all - * accounts in the specified wallet. - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `wallet_id_hex` - Hex-encoded wallet ID (64 characters), or null for all wallets - * * `network` - The network for which to get mempool transaction count - * - * # Returns - * * Total mempool transaction count - * * Returns 0 if wallet not found or client not initialized (check last_error) - */ -uint32_t dash_spv_ffi_wallet_get_mempool_transaction_count(struct FFIDashSpvClient *client, - const char *wallet_id_hex, - enum FFINetwork network); - -/** - * Record a sent transaction in the wallet - * - * This records a transaction that was sent/broadcast by the client, updating the - * wallet state to reflect the outgoing transaction. The transaction will be tracked - * in mempool until it's confirmed in a block. - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `tx_hex` - Hex-encoded transaction data - * * `network` - The network for the transaction - * - * # Returns - * * FFIErrorCode::Success on success - * * FFIErrorCode::InvalidArgument on error (check last_error) - */ -int32_t dash_spv_ffi_wallet_record_sent_transaction(struct FFIDashSpvClient *client, - const char *tx_hex, - enum FFINetwork network); - -/** - * Get a receive address from a specific wallet and account - * - * This generates a new unused receive address (external chain) for the specified - * wallet and account. The address will be marked as used if mark_as_used is true. - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) - * * `network` - The network for the address - * * `account_index` - Account index (0 for first account) - * * `account_type_pref` - Account type preference (BIP44, BIP32, or preference) - * * `mark_as_used` - Whether to mark the address as used after generation - * - * # Returns - * * Pointer to FFIAddressGenerationResult containing the address and account type used - * * Returns null if address generation fails (check last_error) - */ -struct FFIAddressGenerationResult *dash_spv_ffi_wallet_get_receive_address(struct FFIDashSpvClient *client, - const char *wallet_id_hex, - enum FFINetwork network, - uint32_t account_index, - enum FFIAccountTypePreference account_type_pref, - bool mark_as_used); - -/** - * Get a change address from a specific wallet and account - * - * This generates a new unused change address (internal chain) for the specified - * wallet and account. The address will be marked as used if mark_as_used is true. - * - * # Arguments - * * `client` - Pointer to FFIDashSpvClient - * * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) - * * `network` - The network for the address - * * `account_index` - Account index (0 for first account) - * * `account_type_pref` - Account type preference (BIP44, BIP32, or preference) - * * `mark_as_used` - Whether to mark the address as used after generation - * - * # Returns - * * Pointer to FFIAddressGenerationResult containing the address and account type used - * * Returns null if address generation fails (check last_error) - */ -struct FFIAddressGenerationResult *dash_spv_ffi_wallet_get_change_address(struct FFIDashSpvClient *client, - const char *wallet_id_hex, - enum FFINetwork network, - uint32_t account_index, - enum FFIAccountTypePreference account_type_pref, - bool mark_as_used); - -/** - * Free an FFIAddressGenerationResult and its associated resources - * - * # Safety - * * `result` must be a valid pointer to an FFIAddressGenerationResult - * * The pointer must not be used after this function is called - * * This function should only be called once per FFIAddressGenerationResult - */ -void dash_spv_ffi_address_generation_result_destroy(struct FFIAddressGenerationResult *result); diff --git a/dash-spv-ffi/peer_reputation.json b/dash-spv-ffi/peer_reputation.json index fe16b9856..dfd7cb05e 100644 --- a/dash-spv-ffi/peer_reputation.json +++ b/dash-spv-ffi/peer_reputation.json @@ -1,6 +1,6 @@ [ [ - "34.220.134.30:19999", + "34.214.48.68:19999", { "score": 0, "ban_count": 0, @@ -11,7 +11,7 @@ } ], [ - "34.217.58.158:19999", + "34.222.21.14:19999", { "score": 0, "ban_count": 0, @@ -22,7 +22,7 @@ } ], [ - "34.222.21.14:19999", + "34.220.134.30:19999", { "score": 0, "ban_count": 0, @@ -33,18 +33,18 @@ } ], [ - "34.214.48.68:19999", + "18.237.170.32:19999", { "score": 0, "ban_count": 0, "positive_actions": 0, "negative_actions": 0, - "connection_attempts": 1, + "connection_attempts": 2, "successful_connections": 0 } ], [ - "18.237.170.32:19999", + "34.210.84.163:19999", { "score": 0, "ban_count": 0, @@ -55,7 +55,18 @@ } ], [ - "34.210.84.163:19999", + "34.217.58.158:19999", + { + "score": 0, + "ban_count": 0, + "positive_actions": 0, + "negative_actions": 0, + "connection_attempts": 2, + "successful_connections": 0 + } + ], + [ + "34.210.26.195:19999", { "score": 0, "ban_count": 0, diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs index ba00bdcf1..cf7799e41 100644 --- a/dash-spv-ffi/src/client.rs +++ b/dash-spv-ffi/src/client.rs @@ -1,8 +1,52 @@ use crate::{ - null_check, set_last_error, FFIArray, FFIBalance, FFIClientConfig, FFIDetailedSyncProgress, - FFIErrorCode, FFIEventCallbacks, FFIMempoolStrategy, FFISpvStats, FFIString, FFISyncProgress, - FFITransaction, FFIUtxo, FFIWatchItem, + null_check, set_last_error, FFIArray, FFIClientConfig, FFIDetailedSyncProgress, FFIErrorCode, + FFIEventCallbacks, FFIMempoolStrategy, FFISpvStats, FFIString, FFISyncProgress, FFITransaction, }; +// Import wallet types from key-wallet-ffi +use key_wallet_ffi::{FFIBalance, FFIUTXO as FFIUtxo}; + +// Helper function to convert AddressBalance to FFIBalance +fn address_balance_to_ffi(balance: dash_spv::types::AddressBalance) -> FFIBalance { + FFIBalance { + confirmed: balance.confirmed.to_sat(), + unconfirmed: balance.unconfirmed.to_sat(), + immature: 0, + total: balance.total().to_sat(), + } +} + +// Helper function to convert key_wallet::Utxo to FFIUtxo +fn wallet_utxo_to_ffi(utxo: key_wallet::Utxo) -> FFIUtxo { + use std::ffi::CString; + + // Convert txid to byte array + let mut txid_bytes = [0u8; 32]; + txid_bytes.copy_from_slice(&utxo.outpoint.txid[..]); + + // Create FFI UTXO + let address_str = utxo.address.to_string(); + let script_bytes = utxo.txout.script_pubkey.to_bytes(); + + FFIUtxo { + txid: txid_bytes, + vout: utxo.outpoint.vout, + amount: utxo.txout.value, + address: CString::new(address_str).unwrap_or_default().into_raw(), + script_pubkey: if script_bytes.is_empty() { + std::ptr::null_mut() + } else { + let script_box = script_bytes.into_boxed_slice(); + Box::into_raw(script_box) as *mut u8 + }, + script_len: utxo.txout.script_pubkey.len(), + height: utxo.height, + confirmations: if utxo.is_confirmed { + 1 + } else { + 0 + }, + } +} use dash_spv::types::SyncStage; use dash_spv::DashSpvClient; use dashcore::{Address, ScriptBuf, Txid}; @@ -917,52 +961,6 @@ pub unsafe extern "C" fn dash_spv_ffi_client_is_filter_sync_available( }) } -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_add_watch_item( - client: *mut FFIDashSpvClient, - item: *const FFIWatchItem, -) -> i32 { - null_check!(client); - null_check!(item); - - let _ = match (*item).to_watch_item() { - Ok(_) => (), - Err(e) => { - set_last_error(&e); - return FFIErrorCode::InvalidArgument as i32; - } - }; - - let client = &(*client); - let inner = client.inner.clone(); - - set_last_error("Watch API not implemented in current dash-spv version"); - FFIErrorCode::ConfigError as i32 -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_client_remove_watch_item( - client: *mut FFIDashSpvClient, - item: *const FFIWatchItem, -) -> i32 { - null_check!(client); - null_check!(item); - - let _ = match (*item).to_watch_item() { - Ok(_) => (), - Err(e) => { - set_last_error(&e); - return FFIErrorCode::InvalidArgument as i32; - } - }; - - let client = &(*client); - let inner = client.inner.clone(); - - set_last_error("Watch API not implemented in current dash-spv version"); - FFIErrorCode::ConfigError as i32 -} - #[no_mangle] pub unsafe extern "C" fn dash_spv_ffi_client_get_address_balance( client: *mut FFIDashSpvClient, @@ -1022,7 +1020,7 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_address_balance( }); match result { - Ok(balance) => Box::into_raw(Box::new(FFIBalance::from(balance))), + Ok(balance) => Box::into_raw(Box::new(address_balance_to_ffi(balance))), Err(e) => { set_last_error(&e.to_string()); std::ptr::null_mut() @@ -1050,7 +1048,8 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos(client: *mut FFIDashSpvCl let wallet = spv_client.wallet().clone(); let wallet = wallet.read().await; let utxos = wallet.base.get_all_utxos(); - let ffi_utxos: Vec = utxos.into_iter().cloned().map(FFIUtxo::from).collect(); + let ffi_utxos: Vec = + utxos.into_iter().cloned().map(wallet_utxo_to_ffi).collect(); Ok(FFIArray::new(ffi_utxos)) } else { Err(dash_spv::SpvError::Storage(dash_spv::StorageError::NotFound( @@ -1132,7 +1131,7 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_utxos_for_address( .into_iter() .filter(|u| u.address.to_string() == target) .cloned() - .map(FFIUtxo::from) + .map(wallet_utxo_to_ffi) .collect(); Ok(FFIArray::new(filtered)) } else { @@ -1606,10 +1605,8 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_total_balance( let total = wallet.base.get_total_balance(); Ok(FFIBalance { confirmed: total, - pending: 0, - instantlocked: 0, - mempool: 0, - mempool_instant: 0, + unconfirmed: 0, + immature: 0, total, }) } else { @@ -1848,27 +1845,26 @@ pub unsafe extern "C" fn dash_spv_ffi_client_get_mempool_balance( let client = &(*client); let inner = client.inner.clone(); - let result = client.runtime.block_on(async { - let guard = inner.lock().unwrap(); - if let Some(ref spv_client) = *guard { - spv_client.get_mempool_balance(&addr).await - } else { - Err(dash_spv::SpvError::Storage(dash_spv::StorageError::NotFound( - "Client not initialized".to_string(), - ))) - } - }); + let result: Result = + client.runtime.block_on(async { + let guard = inner.lock().unwrap(); + if let Some(ref spv_client) = *guard { + spv_client.get_mempool_balance(&addr).await + } else { + Err(dash_spv::SpvError::Storage(dash_spv::StorageError::NotFound( + "Client not initialized".to_string(), + ))) + } + }); match result { Ok(mempool_balance) => { // Convert MempoolBalance to FFIBalance let balance = FFIBalance { confirmed: 0, // No confirmed balance in mempool - pending: mempool_balance.pending.to_sat(), - instantlocked: 0, // No confirmed instantlocked in mempool - mempool: mempool_balance.pending.to_sat() + unconfirmed: mempool_balance.pending.to_sat() + mempool_balance.pending_instant.to_sat(), - mempool_instant: mempool_balance.pending_instant.to_sat(), + immature: 0, total: mempool_balance.pending.to_sat() + mempool_balance.pending_instant.to_sat(), }; Box::into_raw(Box::new(balance)) diff --git a/dash-spv-ffi/src/lib.rs b/dash-spv-ffi/src/lib.rs index 273af8b32..f825937ec 100644 --- a/dash-spv-ffi/src/lib.rs +++ b/dash-spv-ffi/src/lib.rs @@ -5,7 +5,6 @@ pub mod error; pub mod platform_integration; pub mod types; pub mod utils; -pub mod wallet; pub use callbacks::*; pub use client::*; @@ -14,7 +13,6 @@ pub use error::*; pub use platform_integration::*; pub use types::*; pub use utils::*; -pub use wallet::*; // Re-export commonly used types pub use types::FFINetwork; @@ -39,10 +37,6 @@ mod test_client_lifecycle; #[path = "../tests/unit/test_async_operations.rs"] mod test_async_operations; -#[cfg(test)] -#[path = "../tests/unit/test_wallet_operations.rs"] -mod test_wallet_operations; - #[cfg(test)] #[path = "../tests/unit/test_memory_management.rs"] mod test_memory_management; diff --git a/dash-spv-ffi/src/wallet.rs b/dash-spv-ffi/src/wallet.rs deleted file mode 100644 index 66de4f11c..000000000 --- a/dash-spv-ffi/src/wallet.rs +++ /dev/null @@ -1,1677 +0,0 @@ -use crate::client::FFIDashSpvClient; -use crate::types::FFINetwork; -use crate::{null_check, FFIArray}; -use crate::{set_last_error, FFIString}; -use dash_spv::FilterMatch; -use dashcore::{consensus, OutPoint, ScriptBuf, Txid}; -use key_wallet::account::StandardAccountType; -use key_wallet::wallet::initialization::WalletAccountCreationOptions; -use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference; -use key_wallet::AccountType; -use key_wallet::Utxo as KWUtxo; -use key_wallet::WalletBalance; -use key_wallet_manager::wallet_interface::WalletInterface; -use key_wallet_manager::wallet_manager::{AccountTypeUsed, WalletId}; -use std::ffi::CStr; -use std::os::raw::c_char; -use std::str::FromStr; - -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIWatchItemType { - Address = 0, - Script = 1, - Outpoint = 2, -} - -#[repr(C)] -pub struct FFIWatchItem { - pub item_type: FFIWatchItemType, - pub data: FFIString, -} - -impl FFIWatchItem { - pub unsafe fn to_watch_item(&self) -> Result<(), String> { - // Note: This method uses NetworkUnchecked for backward compatibility. - // Consider using to_watch_item_with_network for proper network validation. - let data_str = FFIString::from_ptr(self.data.ptr)?; - - match self.item_type { - FFIWatchItemType::Address => { - let _addr = - dashcore::Address::::from_str(&data_str) - .map_err(|e| format!("Invalid address: {}", e))? - .assume_checked(); - Ok(()) - } - FFIWatchItemType::Script => { - let script_bytes = - hex::decode(&data_str).map_err(|e| format!("Invalid script hex: {}", e))?; - let _script = ScriptBuf::from(script_bytes); - Ok(()) - } - FFIWatchItemType::Outpoint => { - let parts: Vec<&str> = data_str.split(':').collect(); - if parts.len() != 2 { - return Err("Invalid outpoint format (expected txid:vout)".to_string()); - } - let txid: Txid = parts[0].parse().map_err(|e| format!("Invalid txid: {}", e))?; - let vout: u32 = parts[1].parse().map_err(|e| format!("Invalid vout: {}", e))?; - let _ = OutPoint::new(txid, vout); - Ok(()) - } - } - } - - /// Convert FFIWatchItem to WatchItem with network validation - pub unsafe fn to_watch_item_with_network( - &self, - network: dashcore::Network, - ) -> Result<(), String> { - let data_str = FFIString::from_ptr(self.data.ptr)?; - - match self.item_type { - FFIWatchItemType::Address => { - let addr = - dashcore::Address::::from_str(&data_str) - .map_err(|e| format!("Invalid address: {}", e))?; - - // Validate that the address belongs to the expected network - let _checked_addr = addr.require_network(network).map_err(|_| { - format!("Address {} is not valid for network {:?}", data_str, network) - })?; - Ok(()) - } - FFIWatchItemType::Script => { - let script_bytes = - hex::decode(&data_str).map_err(|e| format!("Invalid script hex: {}", e))?; - let _script = ScriptBuf::from(script_bytes); - Ok(()) - } - FFIWatchItemType::Outpoint => { - let _outpoint = OutPoint::from_str(&data_str) - .map_err(|e| format!("Invalid outpoint: {}", e))?; - Ok(()) - } - } - } -} - -#[repr(C)] -#[derive(Debug, Clone, Copy)] -pub struct FFIBalance { - pub confirmed: u64, - pub pending: u64, - pub instantlocked: u64, - pub mempool: u64, - pub mempool_instant: u64, - pub total: u64, -} - -// Balance struct removed from dash-spv public API; use AddressBalance conversion below - -impl From for FFIBalance { - fn from(balance: dash_spv::types::AddressBalance) -> Self { - FFIBalance { - confirmed: balance.confirmed.to_sat(), - pending: balance.unconfirmed.to_sat(), - instantlocked: 0, // AddressBalance doesn't have instantlocked - mempool: balance.pending.to_sat(), - mempool_instant: balance.pending_instant.to_sat(), - total: balance.total().to_sat(), - } - } -} - -#[repr(C)] -pub struct FFIUtxo { - pub txid: FFIString, - pub vout: u32, - pub amount: u64, - pub script_pubkey: FFIString, - pub address: FFIString, - pub height: u32, - pub is_coinbase: bool, - pub is_confirmed: bool, - pub is_instantlocked: bool, -} - -impl From for FFIUtxo { - fn from(utxo: KWUtxo) -> Self { - FFIUtxo { - txid: FFIString::new(&utxo.outpoint.txid.to_string()), - vout: utxo.outpoint.vout, - amount: utxo.txout.value, - script_pubkey: FFIString::new(&hex::encode(utxo.txout.script_pubkey.to_bytes())), - address: FFIString::new(&utxo.address.to_string()), - height: utxo.height, - is_coinbase: utxo.is_coinbase, - is_confirmed: utxo.is_confirmed, - is_instantlocked: utxo.is_instantlocked, - } - } -} - -#[repr(C)] -pub struct FFITransactionResult { - pub txid: FFIString, - pub version: i32, - pub locktime: u32, - pub size: u32, - pub weight: u32, - pub fee: u64, - pub confirmation_time: u64, - pub confirmation_height: u32, -} - -// TransactionResult no longer available from dash-spv; conversion removed - -#[repr(C)] -pub struct FFIBlockResult { - pub hash: FFIString, - pub height: u32, - pub time: u32, - pub tx_count: u32, -} - -// BlockResult no longer available from dash-spv; conversion removed - -#[repr(C)] -pub struct FFIFilterMatch { - pub block_hash: FFIString, - pub height: u32, - pub block_requested: bool, -} - -impl From for FFIFilterMatch { - fn from(filter_match: FilterMatch) -> Self { - FFIFilterMatch { - block_hash: FFIString::new(&filter_match.block_hash.to_string()), - height: filter_match.height, - block_requested: filter_match.block_requested, - } - } -} - -#[repr(C)] -pub struct FFIAddressStats { - pub address: FFIString, - pub utxo_count: u32, - pub total_value: u64, - pub confirmed_value: u64, - pub pending_value: u64, - pub spendable_count: u32, - pub coinbase_count: u32, -} - -// AddressStats no longer available from dash-spv; conversion removed - -impl From for FFIBalance { - fn from(bal: WalletBalance) -> Self { - // Map confirmed/unconfirmed/locked; mempool fields are not tracked here - let confirmed = bal.confirmed; - let unconfirmed = bal.unconfirmed; - // "locked" is not exposed in FFIBalance directly; keep in total implicitly - FFIBalance { - confirmed, - pending: unconfirmed, - instantlocked: 0, - mempool: 0, - mempool_instant: 0, - total: bal.total, - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_watch_item_address( - address: *const c_char, -) -> *mut FFIWatchItem { - if address.is_null() { - set_last_error("Null address pointer"); - return std::ptr::null_mut(); - } - - match CStr::from_ptr(address).to_str() { - Ok(addr_str) => { - let item = FFIWatchItem { - item_type: FFIWatchItemType::Address, - data: FFIString::new(addr_str), - }; - Box::into_raw(Box::new(item)) - } - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in address: {}", e)); - std::ptr::null_mut() - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_watch_item_script( - script_hex: *const c_char, -) -> *mut FFIWatchItem { - if script_hex.is_null() { - set_last_error("Null script pointer"); - return std::ptr::null_mut(); - } - - match CStr::from_ptr(script_hex).to_str() { - Ok(script_str) => { - let item = FFIWatchItem { - item_type: FFIWatchItemType::Script, - data: FFIString::new(script_str), - }; - Box::into_raw(Box::new(item)) - } - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in script: {}", e)); - std::ptr::null_mut() - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_watch_item_outpoint( - txid: *const c_char, - vout: u32, -) -> *mut FFIWatchItem { - if txid.is_null() { - set_last_error("Null txid pointer"); - return std::ptr::null_mut(); - } - - match CStr::from_ptr(txid).to_str() { - Ok(txid_str) => { - let outpoint_str = format!("{}:{}", txid_str, vout); - let item = FFIWatchItem { - item_type: FFIWatchItemType::Outpoint, - data: FFIString::new(&outpoint_str), - }; - Box::into_raw(Box::new(item)) - } - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in txid: {}", e)); - std::ptr::null_mut() - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_watch_item_destroy(item: *mut FFIWatchItem) { - if !item.is_null() { - let item = Box::from_raw(item); - dash_spv_ffi_string_destroy(item.data); - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_balance_destroy(balance: *mut FFIBalance) { - if !balance.is_null() { - let _ = Box::from_raw(balance); - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_utxo_destroy(utxo: *mut FFIUtxo) { - if !utxo.is_null() { - let utxo = Box::from_raw(utxo); - dash_spv_ffi_string_destroy(utxo.txid); - dash_spv_ffi_string_destroy(utxo.script_pubkey); - dash_spv_ffi_string_destroy(utxo.address); - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_transaction_result_destroy(tx: *mut FFITransactionResult) { - if !tx.is_null() { - let tx = Box::from_raw(tx); - dash_spv_ffi_string_destroy(tx.txid); - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_block_result_destroy(block: *mut FFIBlockResult) { - if !block.is_null() { - let block = Box::from_raw(block); - dash_spv_ffi_string_destroy(block.hash); - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_filter_match_destroy(filter_match: *mut FFIFilterMatch) { - if !filter_match.is_null() { - let filter_match = Box::from_raw(filter_match); - dash_spv_ffi_string_destroy(filter_match.block_hash); - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_address_stats_destroy(stats: *mut FFIAddressStats) { - if !stats.is_null() { - let stats = Box::from_raw(stats); - dash_spv_ffi_string_destroy(stats.address); - } -} - -use crate::types::dash_spv_ffi_string_destroy; - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_validate_address( - address: *const c_char, - network: FFINetwork, -) -> i32 { - if address.is_null() { - return 0; - } - - let addr_str = match CStr::from_ptr(address).to_str() { - Ok(s) => s, - Err(_) => return 0, - }; - - // Convert FFI network to dashcore network - let net: dashcore::Network = network.into(); - - // Try to parse the address as unchecked first - match dashcore::Address::::from_str(addr_str) { - Ok(addr_unchecked) => { - // Check if the address is valid for the given network - match addr_unchecked.require_network(net) { - Ok(_) => 1, // Address is valid for the specified network - Err(_) => 0, // Address is for a different network - } - } - Err(_) => 0, - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_get_monitored_addresses( - client: *mut FFIDashSpvClient, - network: FFINetwork, -) -> *mut crate::FFIArray { - null_check!(client, std::ptr::null_mut()); - - let client = &(*client); - let inner = client.inner.clone(); - - let result = client.run_async(|| async { - let guard = inner.lock().unwrap(); - if let Some(ref spv_client) = *guard { - let wallet_manager = &spv_client.wallet().read().await.base; - let addresses = wallet_manager.monitored_addresses(network.into()); - - let ffi_strings: Vec<*mut FFIString> = addresses - .into_iter() - .map(|addr| Box::into_raw(Box::new(FFIString::new(&addr.to_string())))) - .collect(); - - Ok(crate::FFIArray::new(ffi_strings)) - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(array) => Box::into_raw(Box::new(array)), - Err(e) => { - set_last_error(&e); - std::ptr::null_mut() - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_get_balance( - client: *mut FFIDashSpvClient, - wallet_id_ptr: *const c_char, -) -> *mut crate::FFIBalance { - null_check!(client, std::ptr::null_mut()); - null_check!(wallet_id_ptr, std::ptr::null_mut()); - - // Parse wallet id as 64-char hex string - let wallet_id_hex = match CStr::from_ptr(wallet_id_ptr).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in wallet id: {}", e)); - return std::ptr::null_mut(); - } - }; - - let mut id: [u8; 32] = [0u8; 32]; - let bytes = hex::decode(wallet_id_hex).unwrap_or_default(); - if bytes.len() != 32 { - set_last_error("Wallet ID must be 32 bytes hex"); - return std::ptr::null_mut(); - } - id.copy_from_slice(&bytes); - - let client = &(*client); - let inner = client.inner.clone(); - - let result: Result = client.run_async(|| async { - let guard = inner.lock().unwrap(); - if let Some(ref spv_client) = *guard { - let wallet = spv_client.wallet().clone(); - let wallet = wallet.read().await; - match wallet.base.get_wallet_balance(&id) { - Ok(b) => Ok(crate::FFIBalance::from(b)), - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(bal) => Box::into_raw(Box::new(bal)), - Err(e) => { - set_last_error(&e); - std::ptr::null_mut() - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_get_utxos( - client: *mut FFIDashSpvClient, - wallet_id_ptr: *const c_char, -) -> FFIArray { - null_check!( - client, - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0 - } - ); - null_check!( - wallet_id_ptr, - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0 - } - ); - - let wallet_id_hex = match CStr::from_ptr(wallet_id_ptr).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in wallet id: {}", e)); - return FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0, - }; - } - }; - - let mut id: [u8; 32] = [0u8; 32]; - let bytes = hex::decode(wallet_id_hex).unwrap_or_default(); - if bytes.len() != 32 { - set_last_error("Wallet ID must be 32 bytes hex"); - return FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0, - }; - } - id.copy_from_slice(&bytes); - - let client = &(*client); - let inner = client.inner.clone(); - - let result: Result = client.run_async(|| async { - let guard = inner.lock().unwrap(); - if let Some(ref spv_client) = *guard { - let wallet = spv_client.wallet().clone(); - let wallet = wallet.read().await; - match wallet.base.wallet_utxos(&id) { - Ok(set) => { - let ffi: Vec = - set.into_iter().cloned().map(crate::FFIUtxo::from).collect(); - Ok(FFIArray::new(ffi)) - } - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(arr) => arr, - Err(e) => { - set_last_error(&e); - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0, - } - } - } -} - -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIWalletAccountCreationOptions { - /// Default account creation: Creates account 0 for BIP44, account 0 for CoinJoin, - /// and all special purpose accounts (Identity Registration, Identity Invitation, - /// Provider keys, etc.) - Default = 0, - /// Create only BIP44 accounts (no CoinJoin or special accounts) - BIP44AccountsOnly = 1, - /// Create no accounts at all - useful for tests that want to manually control account creation - None = 2, -} - -impl From for WalletAccountCreationOptions { - fn from(options: FFIWalletAccountCreationOptions) -> Self { - match options { - FFIWalletAccountCreationOptions::Default => WalletAccountCreationOptions::Default, - FFIWalletAccountCreationOptions::BIP44AccountsOnly => { - WalletAccountCreationOptions::BIP44AccountsOnly(Default::default()) - } - FFIWalletAccountCreationOptions::None => WalletAccountCreationOptions::None, - } - } -} - -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIAccountType { - /// Standard BIP44 account for regular transactions - BIP44 = 0, - /// Standard BIP32 account for regular transactions - BIP32 = 1, - /// CoinJoin account for private transactions - CoinJoin = 2, - /// Identity registration funding - IdentityRegistration = 3, - /// Identity top-up funding - IdentityTopUp = 4, - /// Identity invitation funding - IdentityInvitation = 5, - /// Provider voting keys (DIP-3) - ProviderVotingKeys = 6, - /// Provider owner keys (DIP-3) - ProviderOwnerKeys = 7, - /// Provider operator keys (DIP-3) - ProviderOperatorKeys = 8, - /// Provider platform P2P keys (DIP-3, ED25519) - ProviderPlatformKeys = 9, -} - -impl FFIAccountType { - /// Convert FFI account type to internal AccountType - /// - /// # Arguments - /// * `account_index` - Required for BIP44, BIP32, and CoinJoin account types - /// * `registration_index` - Required for IdentityTopUp account type - pub fn to_account_type( - self, - account_index: Option, - registration_index: Option, - ) -> Result { - use key_wallet::AccountType::*; - - match self { - FFIAccountType::BIP44 => { - let index = account_index.ok_or("Account index required for BIP44 accounts")?; - Ok(Standard { - index, - standard_account_type: StandardAccountType::BIP44Account, - }) - } - FFIAccountType::BIP32 => { - let index = account_index.ok_or("Account index required for BIP32 accounts")?; - Ok(Standard { - index, - standard_account_type: StandardAccountType::BIP32Account, - }) - } - FFIAccountType::CoinJoin => { - let index = account_index.ok_or("Account index required for CoinJoin accounts")?; - Ok(CoinJoin { - index, - }) - } - FFIAccountType::IdentityRegistration => Ok(IdentityRegistration), - FFIAccountType::IdentityTopUp => { - let registration_index = registration_index - .ok_or("Registration index required for IdentityTopUp accounts")?; - Ok(IdentityTopUp { - registration_index, - }) - } - FFIAccountType::IdentityInvitation => Ok(IdentityInvitation), - FFIAccountType::ProviderVotingKeys => Ok(ProviderVotingKeys), - FFIAccountType::ProviderOwnerKeys => Ok(ProviderOwnerKeys), - FFIAccountType::ProviderOperatorKeys => Ok(ProviderOperatorKeys), - FFIAccountType::ProviderPlatformKeys => Ok(ProviderPlatformKeys), - } - } -} - -/// Create a new wallet from mnemonic phrase -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `mnemonic` - The mnemonic phrase as null-terminated C string -/// * `passphrase` - Optional BIP39 passphrase (can be null/empty) -/// * `network` - The network to use -/// * `account_options` - Account creation options -/// * `name` - Wallet name as null-terminated C string -/// * `birth_height` - Optional birth height (can be 0 for none) -/// -/// # Returns -/// * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) -/// * Returns null on error (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_create_from_mnemonic( - client: *mut FFIDashSpvClient, - mnemonic: *const c_char, - passphrase: *const c_char, - network: FFINetwork, - account_options: FFIWalletAccountCreationOptions, - name: *const c_char, - birth_height: u32, -) -> *mut FFIString { - null_check!(client, std::ptr::null_mut()); - null_check!(mnemonic, std::ptr::null_mut()); - null_check!(name, std::ptr::null_mut()); - - let client = &(*client); - let inner = client.inner.clone(); - - let mnemonic_str = match CStr::from_ptr(mnemonic).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in mnemonic: {}", e)); - return std::ptr::null_mut(); - } - }; - - let passphrase_str = if passphrase.is_null() { - "" - } else { - match CStr::from_ptr(passphrase).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in passphrase: {}", e)); - return std::ptr::null_mut(); - } - } - }; - - let name_str = match CStr::from_ptr(name).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in name: {}", e)); - return std::ptr::null_mut(); - } - }; - - let result = client.run_async(|| async { - let mut guard = inner.lock().unwrap(); - if let Some(ref mut spv_client) = *guard { - let wallet_manager = &mut spv_client.wallet().write().await.base; - - // Generate a random WalletId - let wallet_id = WalletId::from(rand::random::<[u8; 32]>()); - - let network = network.into(); - let account_creation_options: WalletAccountCreationOptions = account_options.into(); - let birth_height_opt = if birth_height == 0 { - None - } else { - Some(birth_height) - }; - - match wallet_manager.create_wallet_from_mnemonic( - wallet_id, - name_str.to_string(), - mnemonic_str, - passphrase_str, - Some(network), - birth_height_opt, - account_creation_options, - ) { - Ok(_) => { - // Convert WalletId to hex string - Ok(hex::encode(wallet_id)) - } - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(wallet_id_hex) => Box::into_raw(Box::new(FFIString::new(&wallet_id_hex))), - Err(e) => { - set_last_error(&e); - std::ptr::null_mut() - } - } -} - -/// Create a new empty wallet (test wallet with fixed mnemonic) -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `network` - The network to use -/// * `account_options` - Account creation options -/// * `name` - Wallet name as null-terminated C string -/// -/// # Returns -/// * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) -/// * Returns null on error (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_create( - client: *mut FFIDashSpvClient, - network: FFINetwork, - account_options: FFIWalletAccountCreationOptions, - name: *const c_char, -) -> *mut FFIString { - null_check!(client, std::ptr::null_mut()); - null_check!(name, std::ptr::null_mut()); - - let client = &(*client); - let inner = client.inner.clone(); - - let name_str = match CStr::from_ptr(name).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in name: {}", e)); - return std::ptr::null_mut(); - } - }; - - let result = client.run_async(|| async { - let mut guard = inner.lock().unwrap(); - if let Some(ref mut spv_client) = *guard { - let wallet_manager = &mut spv_client.wallet().write().await.base; - - // Generate a random WalletId - let wallet_id = WalletId::from(rand::random::<[u8; 32]>()); - - let network = network.into(); - let account_creation_options: WalletAccountCreationOptions = account_options.into(); - - match wallet_manager.create_wallet( - wallet_id, - name_str.to_string(), - account_creation_options, - network, - ) { - Ok(_) => { - // Convert WalletId to hex string - Ok(hex::encode(wallet_id)) - } - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(wallet_id_hex) => Box::into_raw(Box::new(FFIString::new(&wallet_id_hex))), - Err(e) => { - set_last_error(&e); - std::ptr::null_mut() - } - } -} - -/// Get a list of all wallet IDs -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// -/// # Returns -/// * FFIArray of FFIString objects containing hex-encoded WalletIds -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_list(client: *mut FFIDashSpvClient) -> FFIArray { - null_check!( - client, - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0 - } - ); - - let client = &(*client); - let inner = client.inner.clone(); - - let result: Result = client.run_async(|| async { - let guard = inner.lock().unwrap(); - if let Some(ref spv_client) = *guard { - let wallet_manager = &spv_client.wallet().read().await.base; - let wallet_ids: Vec = wallet_manager - .list_wallets() - .iter() - .map(|id| FFIString::new(&hex::encode(id))) - .collect(); - - Ok(FFIArray::new(wallet_ids)) - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(arr) => arr, - Err(e) => { - set_last_error(&e); - FFIArray { - data: std::ptr::null_mut(), - len: 0, - capacity: 0, - } - } - } -} - -/// Import a wallet from an extended private key -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `xprv` - The extended private key string (base58check encoded) -/// * `network` - The network to use -/// * `account_options` - Account creation options -/// * `name` - Wallet name as null-terminated C string -/// -/// # Returns -/// * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) -/// * Returns null on error (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_import_from_xprv( - client: *mut FFIDashSpvClient, - xprv: *const c_char, - network: FFINetwork, - account_options: FFIWalletAccountCreationOptions, - name: *const c_char, -) -> *mut FFIString { - null_check!(client, std::ptr::null_mut()); - null_check!(xprv, std::ptr::null_mut()); - null_check!(name, std::ptr::null_mut()); - - let client = &(*client); - let inner = client.inner.clone(); - - let xprv_str = match CStr::from_ptr(xprv).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in xprv: {}", e)); - return std::ptr::null_mut(); - } - }; - - let name_str = match CStr::from_ptr(name).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in name: {}", e)); - return std::ptr::null_mut(); - } - }; - - let result = client.run_async(|| async { - let mut guard = inner.lock().unwrap(); - if let Some(ref mut spv_client) = *guard { - let wallet_manager = &mut spv_client.wallet().write().await.base; - - // Generate a random WalletId - let wallet_id = WalletId::from(rand::random::<[u8; 32]>()); - - let network = network.into(); - let account_creation_options: WalletAccountCreationOptions = account_options.into(); - - match wallet_manager.import_wallet_from_extended_priv_key( - wallet_id, - name_str.to_string(), - xprv_str, - network, - account_creation_options, - ) { - Ok(_) => { - // Convert WalletId to hex string - Ok(hex::encode(wallet_id)) - } - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(wallet_id_hex) => Box::into_raw(Box::new(FFIString::new(&wallet_id_hex))), - Err(e) => { - set_last_error(&e); - std::ptr::null_mut() - } - } -} - -/// Import a watch-only wallet from an extended public key -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `xpub` - The extended public key string (base58check encoded) -/// * `network` - The network to use -/// * `name` - Wallet name as null-terminated C string -/// -/// # Returns -/// * Pointer to FFIString containing hex-encoded WalletId (32 bytes as 64-char hex) -/// * Returns null on error (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_import_from_xpub( - client: *mut FFIDashSpvClient, - xpub: *const c_char, - network: FFINetwork, - name: *const c_char, -) -> *mut FFIString { - null_check!(client, std::ptr::null_mut()); - null_check!(xpub, std::ptr::null_mut()); - null_check!(name, std::ptr::null_mut()); - - let client = &(*client); - let inner = client.inner.clone(); - - let xpub_str = match CStr::from_ptr(xpub).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in xpub: {}", e)); - return std::ptr::null_mut(); - } - }; - - let name_str = match CStr::from_ptr(name).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in name: {}", e)); - return std::ptr::null_mut(); - } - }; - - let result = client.run_async(|| async { - let mut guard = inner.lock().unwrap(); - if let Some(ref mut spv_client) = *guard { - let wallet_manager = &mut spv_client.wallet().write().await.base; - - // Generate a random WalletId - let wallet_id = WalletId::from(rand::random::<[u8; 32]>()); - - let network = network.into(); - - match wallet_manager.import_wallet_from_xpub( - wallet_id, - name_str.to_string(), - xpub_str, - network, - ) { - Ok(_) => { - // Convert WalletId to hex string - Ok(hex::encode(wallet_id)) - } - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(wallet_id_hex) => Box::into_raw(Box::new(FFIString::new(&wallet_id_hex))), - Err(e) => { - set_last_error(&e); - std::ptr::null_mut() - } - } -} - -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIAccountTypePreference { - /// Use BIP44 account only - BIP44 = 0, - /// Use BIP32 account only - BIP32 = 1, - /// Prefer BIP44, fallback to BIP32 - PreferBIP44 = 2, - /// Prefer BIP32, fallback to BIP44 - PreferBIP32 = 3, -} - -impl From for AccountTypePreference { - fn from(pref: FFIAccountTypePreference) -> Self { - match pref { - FFIAccountTypePreference::BIP44 => AccountTypePreference::BIP44, - FFIAccountTypePreference::BIP32 => AccountTypePreference::BIP32, - FFIAccountTypePreference::PreferBIP44 => AccountTypePreference::PreferBIP44, - FFIAccountTypePreference::PreferBIP32 => AccountTypePreference::PreferBIP32, - } - } -} - -#[repr(C)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FFIAccountTypeUsed { - /// BIP44 account was used - BIP44 = 0, - /// BIP32 account was used - BIP32 = 1, -} - -#[repr(C)] -pub struct FFIAddressGenerationResult { - pub address: *mut FFIString, - pub account_type_used: FFIAccountTypeUsed, -} - -/// Add a new account to an existing wallet from an extended public key -/// -/// This creates a watch-only account that can monitor addresses and transactions -/// but cannot sign them. -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) -/// * `xpub` - The extended public key string (base58check encoded) -/// * `account_type` - The type of account to create -/// * `network` - The network for the account -/// * `account_index` - Account index (required for BIP44, BIP32, CoinJoin) -/// * `registration_index` - Registration index (required for IdentityTopUp) -/// -/// # Returns -/// * FFIErrorCode::Success on success -/// * FFIErrorCode::InvalidArgument on error (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_add_account_from_xpub( - client: *mut FFIDashSpvClient, - wallet_id_hex: *const c_char, - xpub: *const c_char, - account_type: FFIAccountType, - network: FFINetwork, - account_index: u32, - registration_index: u32, -) -> i32 { - null_check!(client, crate::FFIErrorCode::InvalidArgument as i32); - null_check!(wallet_id_hex, crate::FFIErrorCode::InvalidArgument as i32); - null_check!(xpub, crate::FFIErrorCode::InvalidArgument as i32); - - let client = &(*client); - let inner = client.inner.clone(); - - let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); - return crate::FFIErrorCode::InvalidArgument as i32; - } - }; - - let xpub_str = match CStr::from_ptr(xpub).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in xpub: {}", e)); - return crate::FFIErrorCode::InvalidArgument as i32; - } - }; - - // Parse wallet ID - let mut wallet_id: [u8; 32] = [0u8; 32]; - let bytes = match hex::decode(wallet_id_hex_str) { - Ok(b) => b, - Err(e) => { - set_last_error(&format!("Invalid hex wallet ID: {}", e)); - return crate::FFIErrorCode::InvalidArgument as i32; - } - }; - if bytes.len() != 32 { - set_last_error("Wallet ID must be 32 bytes hex"); - return crate::FFIErrorCode::InvalidArgument as i32; - } - wallet_id.copy_from_slice(&bytes); - - // Convert account type with parameters - let account_index_opt = if matches!( - account_type, - FFIAccountType::BIP44 | FFIAccountType::BIP32 | FFIAccountType::CoinJoin - ) { - Some(account_index) - } else { - None - }; - - let registration_index_opt = if matches!(account_type, FFIAccountType::IdentityTopUp) { - Some(registration_index) - } else { - None - }; - - let account_type_internal = - match account_type.to_account_type(account_index_opt, registration_index_opt) { - Ok(at) => at, - Err(e) => { - set_last_error(&e); - return crate::FFIErrorCode::InvalidArgument as i32; - } - }; - - let result: Result<(), String> = client.run_async(|| async { - let mut guard = inner.lock().unwrap(); - if let Some(ref mut spv_client) = *guard { - let wallet_manager = &mut spv_client.wallet().write().await.base; - - // Parse the extended public key - let extended_pub_key = key_wallet::ExtendedPubKey::from_str(xpub_str) - .map_err(|e| format!("Invalid xpub: {}", e))?; - - match wallet_manager.create_account( - &wallet_id, - account_type_internal, - network.into(), - Some(extended_pub_key), - ) { - Ok(()) => Ok(()), - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(()) => crate::FFIErrorCode::Success as i32, - Err(e) => { - set_last_error(&e); - crate::FFIErrorCode::InvalidArgument as i32 - } - } -} - -/// Get wallet-wide mempool balance -/// -/// This returns the total unconfirmed balance (mempool transactions) across all -/// accounts in the specified wallet. This represents the balance from transactions -/// that have been broadcast but not yet confirmed in a block. -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters), or null for all wallets -/// * `network` - The network for which to get mempool balance -/// -/// # Returns -/// * Total mempool balance in satoshis -/// * Returns 0 if wallet not found or client not initialized (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_get_mempool_balance( - client: *mut FFIDashSpvClient, - wallet_id_hex: *const c_char, - network: FFINetwork, -) -> u64 { - null_check!(client, 0); - - let client = &(*client); - let inner = client.inner.clone(); - - // Parse wallet ID if provided - let wallet_id_opt = if wallet_id_hex.is_null() { - None - } else { - let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); - return 0; - } - }; - - let mut wallet_id: [u8; 32] = [0u8; 32]; - let bytes = match hex::decode(wallet_id_hex_str) { - Ok(b) => b, - Err(e) => { - set_last_error(&format!("Invalid hex wallet ID: {}", e)); - return 0; - } - }; - if bytes.len() != 32 { - set_last_error("Wallet ID must be 32 bytes hex"); - return 0; - } - wallet_id.copy_from_slice(&bytes); - Some(wallet_id) - }; - - let result = client.run_async(|| async { - let guard = inner.lock().unwrap(); - if let Some(ref spv_client) = *guard { - let wallet_manager = &spv_client.wallet().read().await.base; - - if let Some(wallet_id) = wallet_id_opt { - // Get mempool balance for specific wallet - match wallet_manager.get_wallet_balance(&wallet_id) { - Ok(balance) => Ok(balance.unconfirmed), - Err(e) => Err(e.to_string()), - } - } else { - // Get total mempool balance across all wallets - let mut total_mempool_balance = 0u64; - for wallet_id in wallet_manager.list_wallets() { - if let Ok(balance) = wallet_manager.get_wallet_balance(wallet_id) { - total_mempool_balance = - total_mempool_balance.saturating_add(balance.unconfirmed); - } - } - Ok(total_mempool_balance) - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(balance) => balance, - Err(e) => { - set_last_error(&e); - 0 - } - } -} - -/// Get wallet-wide mempool transaction count -/// -/// This returns the total number of unconfirmed transactions (in mempool) across all -/// accounts in the specified wallet. -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters), or null for all wallets -/// * `network` - The network for which to get mempool transaction count -/// -/// # Returns -/// * Total mempool transaction count -/// * Returns 0 if wallet not found or client not initialized (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_get_mempool_transaction_count( - client: *mut FFIDashSpvClient, - wallet_id_hex: *const c_char, - network: FFINetwork, -) -> u32 { - null_check!(client, 0); - - let client = &(*client); - let inner = client.inner.clone(); - - // Parse wallet ID if provided - let wallet_id_opt = if wallet_id_hex.is_null() { - None - } else { - let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); - return 0; - } - }; - - let mut wallet_id: [u8; 32] = [0u8; 32]; - let bytes = match hex::decode(wallet_id_hex_str) { - Ok(b) => b, - Err(e) => { - set_last_error(&format!("Invalid hex wallet ID: {}", e)); - return 0; - } - }; - if bytes.len() != 32 { - set_last_error("Wallet ID must be 32 bytes hex"); - return 0; - } - wallet_id.copy_from_slice(&bytes); - Some(wallet_id) - }; - - let result = client.run_async(|| async { - let guard = inner.lock().unwrap(); - if let Some(ref spv_client) = *guard { - let wallet_manager = &spv_client.wallet().read().await.base; - - if let Some(wallet_id) = wallet_id_opt { - // Get mempool transaction count for specific wallet - match wallet_manager.wallet_transaction_history(&wallet_id) { - Ok(txs) => { - let mempool_count = - txs.iter().filter(|tx_record| tx_record.height.is_none()).count(); - Ok(mempool_count as u32) - } - Err(e) => Err(e.to_string()), - } - } else { - // Get total mempool transaction count across all wallets - let mut total_mempool_count = 0u32; - for wallet_id in wallet_manager.list_wallets() { - if let Ok(txs) = wallet_manager.wallet_transaction_history(wallet_id) { - let wallet_mempool_count = - txs.iter().filter(|tx_record| tx_record.height.is_none()).count() - as u32; - total_mempool_count = - total_mempool_count.saturating_add(wallet_mempool_count); - } - } - Ok(total_mempool_count) - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(count) => count, - Err(e) => { - set_last_error(&e); - 0 - } - } -} - -/// Record a sent transaction in the wallet -/// -/// This records a transaction that was sent/broadcast by the client, updating the -/// wallet state to reflect the outgoing transaction. The transaction will be tracked -/// in mempool until it's confirmed in a block. -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `tx_hex` - Hex-encoded transaction data -/// * `network` - The network for the transaction -/// -/// # Returns -/// * FFIErrorCode::Success on success -/// * FFIErrorCode::InvalidArgument on error (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_record_sent_transaction( - client: *mut FFIDashSpvClient, - tx_hex: *const c_char, - network: FFINetwork, -) -> i32 { - null_check!(client, crate::FFIErrorCode::InvalidArgument as i32); - null_check!(tx_hex, crate::FFIErrorCode::InvalidArgument as i32); - - let client = &(*client); - let inner = client.inner.clone(); - - let tx_hex_str = match CStr::from_ptr(tx_hex).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in transaction hex: {}", e)); - return crate::FFIErrorCode::InvalidArgument as i32; - } - }; - - // Parse transaction from hex - let tx_bytes = match hex::decode(tx_hex_str) { - Ok(bytes) => bytes, - Err(e) => { - set_last_error(&format!("Invalid hex transaction: {}", e)); - return crate::FFIErrorCode::InvalidArgument as i32; - } - }; - - let transaction: dashcore::Transaction = match consensus::deserialize(&tx_bytes) { - Ok(tx) => tx, - Err(e) => { - set_last_error(&format!("Invalid transaction format: {}", e)); - return crate::FFIErrorCode::InvalidArgument as i32; - } - }; - - let result = client.run_async(|| async { - let mut guard = inner.lock().unwrap(); - if let Some(ref mut spv_client) = *guard { - // Record the sent transaction by processing it as a mempool transaction - // This will update the wallet state to reflect the outgoing transaction - spv_client - .wallet() - .write() - .await - .process_mempool_transaction(&transaction, network.into()) - .await; - Ok(()) - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(()) => crate::FFIErrorCode::Success as i32, - Err(e) => { - set_last_error(&e); - crate::FFIErrorCode::InvalidArgument as i32 - } - } -} - -/// Get a receive address from a specific wallet and account -/// -/// This generates a new unused receive address (external chain) for the specified -/// wallet and account. The address will be marked as used if mark_as_used is true. -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) -/// * `network` - The network for the address -/// * `account_index` - Account index (0 for first account) -/// * `account_type_pref` - Account type preference (BIP44, BIP32, or preference) -/// * `mark_as_used` - Whether to mark the address as used after generation -/// -/// # Returns -/// * Pointer to FFIAddressGenerationResult containing the address and account type used -/// * Returns null if address generation fails (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_get_receive_address( - client: *mut FFIDashSpvClient, - wallet_id_hex: *const c_char, - network: FFINetwork, - account_index: u32, - account_type_pref: FFIAccountTypePreference, - mark_as_used: bool, -) -> *mut FFIAddressGenerationResult { - null_check!(client, std::ptr::null_mut()); - null_check!(wallet_id_hex, std::ptr::null_mut()); - - let client = &(*client); - let inner = client.inner.clone(); - - let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); - return std::ptr::null_mut(); - } - }; - - // Parse wallet ID - let mut wallet_id: [u8; 32] = [0u8; 32]; - let bytes = match hex::decode(wallet_id_hex_str) { - Ok(b) => b, - Err(e) => { - set_last_error(&format!("Invalid hex wallet ID: {}", e)); - return std::ptr::null_mut(); - } - }; - if bytes.len() != 32 { - set_last_error("Wallet ID must be 32 bytes hex"); - return std::ptr::null_mut(); - } - wallet_id.copy_from_slice(&bytes); - - let result: Result = client.run_async(|| async { - let mut guard = inner.lock().unwrap(); - if let Some(ref mut spv_client) = *guard { - let wallet_manager = &mut spv_client.wallet().write().await.base; - - match wallet_manager.get_receive_address( - &wallet_id, - network.into(), - account_index, - account_type_pref.into(), - mark_as_used, - ) { - Ok(addr_result) => { - if let (Some(address), Some(account_type_used)) = - (addr_result.address, addr_result.account_type_used) - { - let ffi_account_type = match account_type_used { - AccountTypeUsed::BIP44 => FFIAccountTypeUsed::BIP44, - AccountTypeUsed::BIP32 => FFIAccountTypeUsed::BIP32, - }; - - Ok(FFIAddressGenerationResult { - address: Box::into_raw(Box::new(FFIString::new(&address.to_string()))), - account_type_used: ffi_account_type, - }) - } else { - Err("No address could be generated".to_string()) - } - } - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(result) => Box::into_raw(Box::new(result)), - Err(e) => { - set_last_error(&e); - std::ptr::null_mut() - } - } -} - -/// Get a change address from a specific wallet and account -/// -/// This generates a new unused change address (internal chain) for the specified -/// wallet and account. The address will be marked as used if mark_as_used is true. -/// -/// # Arguments -/// * `client` - Pointer to FFIDashSpvClient -/// * `wallet_id_hex` - Hex-encoded wallet ID (64 characters) -/// * `network` - The network for the address -/// * `account_index` - Account index (0 for first account) -/// * `account_type_pref` - Account type preference (BIP44, BIP32, or preference) -/// * `mark_as_used` - Whether to mark the address as used after generation -/// -/// # Returns -/// * Pointer to FFIAddressGenerationResult containing the address and account type used -/// * Returns null if address generation fails (check last_error) -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_wallet_get_change_address( - client: *mut FFIDashSpvClient, - wallet_id_hex: *const c_char, - network: FFINetwork, - account_index: u32, - account_type_pref: FFIAccountTypePreference, - mark_as_used: bool, -) -> *mut FFIAddressGenerationResult { - null_check!(client, std::ptr::null_mut()); - null_check!(wallet_id_hex, std::ptr::null_mut()); - - let client = &(*client); - let inner = client.inner.clone(); - - let wallet_id_hex_str = match CStr::from_ptr(wallet_id_hex).to_str() { - Ok(s) => s, - Err(e) => { - set_last_error(&format!("Invalid UTF-8 in wallet ID: {}", e)); - return std::ptr::null_mut(); - } - }; - - // Parse wallet ID - let mut wallet_id: [u8; 32] = [0u8; 32]; - let bytes = match hex::decode(wallet_id_hex_str) { - Ok(b) => b, - Err(e) => { - set_last_error(&format!("Invalid hex wallet ID: {}", e)); - return std::ptr::null_mut(); - } - }; - if bytes.len() != 32 { - set_last_error("Wallet ID must be 32 bytes hex"); - return std::ptr::null_mut(); - } - wallet_id.copy_from_slice(&bytes); - - let result: Result = client.run_async(|| async { - let mut guard = inner.lock().unwrap(); - if let Some(ref mut spv_client) = *guard { - let wallet_manager = &mut spv_client.wallet().write().await.base; - - match wallet_manager.get_change_address( - &wallet_id, - network.into(), - account_index, - account_type_pref.into(), - mark_as_used, - ) { - Ok(addr_result) => { - if let (Some(address), Some(account_type_used)) = - (addr_result.address, addr_result.account_type_used) - { - let ffi_account_type = match account_type_used { - AccountTypeUsed::BIP44 => FFIAccountTypeUsed::BIP44, - AccountTypeUsed::BIP32 => FFIAccountTypeUsed::BIP32, - }; - - Ok(FFIAddressGenerationResult { - address: Box::into_raw(Box::new(FFIString::new(&address.to_string()))), - account_type_used: ffi_account_type, - }) - } else { - Err("No address could be generated".to_string()) - } - } - Err(e) => Err(e.to_string()), - } - } else { - Err("Client not initialized".to_string()) - } - }); - - match result { - Ok(result) => Box::into_raw(Box::new(result)), - Err(e) => { - set_last_error(&e); - std::ptr::null_mut() - } - } -} - -/// Free an FFIAddressGenerationResult and its associated resources -/// -/// # Safety -/// * `result` must be a valid pointer to an FFIAddressGenerationResult -/// * The pointer must not be used after this function is called -/// * This function should only be called once per FFIAddressGenerationResult -#[no_mangle] -pub unsafe extern "C" fn dash_spv_ffi_address_generation_result_destroy( - result: *mut FFIAddressGenerationResult, -) { - if !result.is_null() { - let result = Box::from_raw(result); - if !result.address.is_null() { - let addr_ptr = result.address; - // Read the FFIString from the raw pointer and destroy it - let addr_string = unsafe { *Box::from_raw(addr_ptr) }; - dash_spv_ffi_string_destroy(addr_string); - } - } -} diff --git a/dash-spv-ffi/tests/c_tests/test_advanced.c b/dash-spv-ffi/tests/c_tests/test_advanced.c index 63cda5c0b..a82a41d5c 100644 --- a/dash-spv-ffi/tests/c_tests/test_advanced.c +++ b/dash-spv-ffi/tests/c_tests/test_advanced.c @@ -264,32 +264,6 @@ void test_error_conditions() { TEST_SUCCESS("test_error_conditions"); } -// Test watch items -void test_watch_items() { - TEST_START("test_watch_items"); - - // Test creating watch items - FFIWatchItem* addr_item = dash_spv_ffi_watch_item_address("XjSgy6PaVCB3V4KhCiCDkaVbx9ewxe9R1E"); - TEST_ASSERT(addr_item != NULL); - TEST_ASSERT(addr_item->item_type == FFIWatchItemType_Address); - dash_spv_ffi_watch_item_destroy(addr_item); - - FFIWatchItem* script_item = dash_spv_ffi_watch_item_script("76a91488ac"); - TEST_ASSERT(script_item != NULL); - TEST_ASSERT(script_item->item_type == FFIWatchItemType_Script); - dash_spv_ffi_watch_item_destroy(script_item); - - FFIWatchItem* outpoint_item = dash_spv_ffi_watch_item_outpoint( - "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", - 0 - ); - TEST_ASSERT(outpoint_item != NULL); - TEST_ASSERT(outpoint_item->item_type == FFIWatchItemType_Outpoint); - dash_spv_ffi_watch_item_destroy(outpoint_item); - - TEST_SUCCESS("test_watch_items"); -} - // Test callbacks with real operations typedef struct { int progress_count; @@ -360,7 +334,6 @@ int main() { test_concurrent_access(); test_memory_management(); test_error_conditions(); - test_watch_items(); test_callbacks_with_operations(); printf("\n=====================================\n"); diff --git a/dash-spv-ffi/tests/test_client.rs b/dash-spv-ffi/tests/test_client.rs index f2cf11591..5afd30815 100644 --- a/dash-spv-ffi/tests/test_client.rs +++ b/dash-spv-ffi/tests/test_client.rs @@ -102,32 +102,6 @@ mod tests { } } - #[test] - #[serial] - fn test_watch_items() { - unsafe { - let (config, _temp_dir) = create_test_config(); - let client = dash_spv_ffi_client_new(config); - - let addr = CString::new("XjSgy6PaVCB3V4KhCiCDkaVbx9ewxe9R1E").unwrap(); - let item = dash_spv_ffi_watch_item_address(addr.as_ptr()); - - let result = dash_spv_ffi_client_add_watch_item(client, item); - // Client is not started, so we expect either Success (queued), NetworkError, or InvalidArgument - assert!( - result == FFIErrorCode::Success as i32 - || result == FFIErrorCode::NetworkError as i32 - || result == FFIErrorCode::InvalidArgument as i32, - "Expected Success, NetworkError, or InvalidArgument, got error code: {}", - result - ); - - dash_spv_ffi_watch_item_destroy(item); - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - #[test] #[serial] fn test_sync_progress() { @@ -180,9 +154,10 @@ mod tests { let balance_ref = &*balance; assert_eq!( balance_ref.total, - balance_ref.confirmed + balance_ref.pending + balance_ref.instantlocked + balance_ref.confirmed + balance_ref.unconfirmed + balance_ref.immature ); - dash_spv_ffi_balance_destroy(balance); + // FFIBalance from key-wallet-ffi doesn't need explicit destruction + // since it's a simple struct without heap allocations } dash_spv_ffi_client_destroy(client); diff --git a/dash-spv-ffi/tests/test_event_callbacks.rs b/dash-spv-ffi/tests/test_event_callbacks.rs index 7d475d917..8ab81eb99 100644 --- a/dash-spv-ffi/tests/test_event_callbacks.rs +++ b/dash-spv-ffi/tests/test_event_callbacks.rs @@ -276,11 +276,12 @@ fn test_get_total_balance() { if !balance_ptr.is_null() { let balance = &*balance_ptr; println!( - "Total balance - Confirmed: {}, Pending: {}, Total: {}", - balance.confirmed, balance.pending, balance.total + "Total balance - Confirmed: {}, Unconfirmed: {}, Total: {}", + balance.confirmed, balance.unconfirmed, balance.total ); - dash_spv_ffi_balance_destroy(balance_ptr); + // FFIBalance from key-wallet-ffi doesn't need explicit destruction + // dash_spv_ffi_balance_destroy(balance_ptr); println!("✅ Get total balance works!"); } else { println!("⚠️ Failed to get total balance (may need sync first)"); @@ -336,87 +337,10 @@ fn test_enhanced_event_callbacks() { "Failed to set enhanced event callbacks" ); - // Test wallet creation to trigger some events - let wallet_name = CString::new("test_wallet").unwrap(); - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - wallet_name.as_ptr(), - ); - - if !wallet_id.is_null() { - println!("✅ Wallet created successfully"); - - // Test address generation - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr).to_str().unwrap(); - let wallet_id_cstr = CString::new(wallet_id_str).unwrap(); - - let receive_address = dash_spv_ffi_wallet_get_receive_address( - client, - wallet_id_cstr.as_ptr(), - FFINetwork::Regtest, - 0, // account_index - FFIAccountTypePreference::BIP44, - false, // mark_as_used - ); - - if !receive_address.is_null() { - println!("✅ Receive address generated successfully"); - dash_spv_ffi_address_generation_result_destroy(receive_address); - } - - // Test monitored addresses - let addresses = - dash_spv_ffi_wallet_get_monitored_addresses(client, FFINetwork::Regtest); - if !addresses.is_null() { - println!("✅ Monitored addresses retrieved successfully"); - dash_spv_ffi_array_destroy(addresses); - } - - // Test mempool operations - let mempool_balance = dash_spv_ffi_wallet_get_mempool_balance( - client, - wallet_id_cstr.as_ptr(), - FFINetwork::Regtest, - ); - println!("✅ Mempool balance retrieved: {} satoshis", mempool_balance); - - let mempool_tx_count = dash_spv_ffi_wallet_get_mempool_transaction_count( - client, - wallet_id_cstr.as_ptr(), - FFINetwork::Regtest, - ); - println!("✅ Mempool transaction count retrieved: {}", mempool_tx_count); - - // Test account operations - let xpub = - CString::new("tpubD6NzVbkrYhZ4X4rJGpM7KfxYFkGdJKjgGJGJZ7JXmT8yPzJzKQh8xkJfL") - .unwrap(); - let account_result = dash_spv_ffi_wallet_add_account_from_xpub( - client, - wallet_id_cstr.as_ptr(), - xpub.as_ptr(), - FFIAccountType::BIP44, - FFINetwork::Regtest, - 1, // account_index - 0, // registration_index - ); - println!("✅ Account addition result: {}", account_result); - - // Clean up wallet - if !wallet_id.is_null() { - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - } else { - println!("⚠️ Wallet creation failed (may be expected in test environment)"); - } - - // Test error handling - let invalid_wallet_id = CString::new("invalid_wallet_id").unwrap(); - let balance = dash_spv_ffi_wallet_get_balance(client, invalid_wallet_id.as_ptr()); - assert!(balance.is_null(), "Should return null for invalid wallet ID"); + // Note: Wallet-specific tests have been moved to key-wallet-ffi + // The wallet functionality is no longer part of dash-spv-ffi + // dash-spv-ffi now focuses purely on SPV network operations + println!("⚠️ Wallet tests have been moved to key-wallet-ffi"); // Clean up dash_spv_ffi_client_destroy(client); diff --git a/dash-spv-ffi/tests/test_mempool_tracking.rs b/dash-spv-ffi/tests/test_mempool_tracking.rs index cabcaf8e9..7d1061584 100644 --- a/dash-spv-ffi/tests/test_mempool_tracking.rs +++ b/dash-spv-ffi/tests/test_mempool_tracking.rs @@ -170,10 +170,11 @@ fn test_mempool_balance_query() { let address = CString::new("yXdxAYfAkQnrFZNxdVfqwJMRpDcCuC6YLi").unwrap(); let balance = dash_spv_ffi_client_get_mempool_balance(client, address.as_ptr()); if !balance.is_null() { - let balance_data = (*balance); - assert_eq!(balance_data.confirmed, 0); // No confirmed balance in mempool - // mempool and mempool_instant fields contain the actual mempool balance - dash_spv_ffi_balance_destroy(balance); + let balance_ref = &(*balance); + assert_eq!(balance_ref.confirmed, 0); // No confirmed balance in mempool + // mempool and mempool_instant fields contain the actual mempool balance + // FFIBalance from key-wallet-ffi doesn't need explicit destruction + // dash_spv_ffi_balance_destroy(balance); } // Stop client diff --git a/dash-spv-ffi/tests/test_wallet.rs b/dash-spv-ffi/tests/test_wallet.rs deleted file mode 100644 index f1d7a36f4..000000000 --- a/dash-spv-ffi/tests/test_wallet.rs +++ /dev/null @@ -1,127 +0,0 @@ -#[cfg(test)] -mod tests { - use dash_spv_ffi::*; - use key_wallet; - use key_wallet_manager; - use serial_test::serial; - use std::ffi::CString; - - #[test] - #[serial] - fn test_watch_item_address() { - unsafe { - let addr = CString::new("XjSgy6PaVCB3V4KhCiCDkaVbx9ewxe9R1E").unwrap(); - let item = dash_spv_ffi_watch_item_address(addr.as_ptr()); - assert!(!item.is_null()); - - let item_ref = &*item; - assert_eq!(item_ref.item_type as i32, FFIWatchItemType::Address as i32); - - dash_spv_ffi_watch_item_destroy(item); - } - } - - #[test] - #[serial] - fn test_watch_item_script() { - unsafe { - // Valid P2PKH script: OP_DUP OP_HASH160 <20-byte pubkey hash> OP_EQUALVERIFY OP_CHECKSIG - let script_hex = - CString::new("76a914b7c94b7c365c71dd476329c9e5205a0a39cf8e2c88ac").unwrap(); - let item = dash_spv_ffi_watch_item_script(script_hex.as_ptr()); - assert!(!item.is_null()); - - let item_ref = &*item; - assert_eq!(item_ref.item_type as i32, FFIWatchItemType::Script as i32); - - dash_spv_ffi_watch_item_destroy(item); - } - } - - #[test] - #[serial] - fn test_watch_item_outpoint() { - unsafe { - let txid = - CString::new("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - .unwrap(); - let item = dash_spv_ffi_watch_item_outpoint(txid.as_ptr(), 0); - assert!(!item.is_null()); - - let item_ref = &*item; - assert_eq!(item_ref.item_type as i32, FFIWatchItemType::Outpoint as i32); - - dash_spv_ffi_watch_item_destroy(item); - } - } - - #[test] - #[serial] - fn test_watch_item_null_handling() { - unsafe { - let item = dash_spv_ffi_watch_item_address(std::ptr::null()); - assert!(item.is_null()); - - let item = dash_spv_ffi_watch_item_script(std::ptr::null()); - assert!(item.is_null()); - - let item = dash_spv_ffi_watch_item_outpoint(std::ptr::null(), 0); - assert!(item.is_null()); - } - } - - #[test] - #[serial] - fn test_balance_conversion() { - // Skip this test for now - it has dependency issues - // This test would validate FFI balance conversion but requires - // proper Balance type imports which are complex to resolve - println!("Balance conversion test skipped - focus on new wallet functionality"); - assert!(true); - } - - #[test] - #[serial] - fn test_utxo_conversion() { - use dashcore::{Address, OutPoint, TxOut, Txid}; - use std::str::FromStr; - - let outpoint = OutPoint::new( - Txid::from_str("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - .unwrap(), - 0, - ); - let address = Address::::from_str( - "Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dge", - ) - .unwrap() - .assume_checked(); - let txout = TxOut { - value: 100000, - script_pubkey: address.script_pubkey(), - }; - - let utxo = key_wallet::Utxo { - outpoint, - txout, - address, - height: 12345, - is_coinbase: false, - is_locked: false, - is_confirmed: true, - is_instantlocked: false, - }; - - let ffi_utxo = FFIUtxo::from(utxo); - assert_eq!(ffi_utxo.vout, 0); - assert_eq!(ffi_utxo.amount, 100000); - assert_eq!(ffi_utxo.height, 12345); - assert_eq!(ffi_utxo.is_coinbase, false); - assert_eq!(ffi_utxo.is_confirmed, true); - assert_eq!(ffi_utxo.is_instantlocked, false); - - unsafe { - dash_spv_ffi_utxo_destroy(Box::into_raw(Box::new(ffi_utxo))); - } - } -} diff --git a/dash-spv-ffi/tests/unit/test_client_lifecycle.rs b/dash-spv-ffi/tests/unit/test_client_lifecycle.rs index d7bdb9052..0838cb54e 100644 --- a/dash-spv-ffi/tests/unit/test_client_lifecycle.rs +++ b/dash-spv-ffi/tests/unit/test_client_lifecycle.rs @@ -201,7 +201,8 @@ mod tests { let balance = dash_spv_ffi_client_get_address_balance(client, addr.as_ptr()); if !balance.is_null() { - dash_spv_ffi_balance_destroy(balance); + // FFIBalance from key-wallet-ffi doesn't need explicit destruction + // dash_spv_ffi_balance_destroy(balance); } } _ => {} diff --git a/dash-spv-ffi/tests/unit/test_wallet_operations.rs b/dash-spv-ffi/tests/unit/test_wallet_operations.rs deleted file mode 100644 index ff6a29a95..000000000 --- a/dash-spv-ffi/tests/unit/test_wallet_operations.rs +++ /dev/null @@ -1,1113 +0,0 @@ -#[cfg(test)] -mod tests { - use crate::*; - use serial_test::serial; - use std::ffi::{CStr, CString}; - - use std::sync::{Arc, Mutex}; - use std::thread; - - use tempfile::TempDir; - - fn create_test_wallet() -> (*mut FFIDashSpvClient, *mut FFIClientConfig, TempDir) { - let temp_dir = TempDir::new().unwrap(); - unsafe { - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - let path = CString::new(temp_dir.path().to_str().unwrap()).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - dash_spv_ffi_config_set_validation_mode(config, FFIValidationMode::None); - - let client = dash_spv_ffi_client_new(config); - (client, config, temp_dir) - } - } - - #[test] - #[serial] - fn test_address_validation() { - unsafe { - // Valid mainnet addresses - let valid_mainnet = - ["Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dge", "XasTb9LP4wwsvtqXG6ZUZEggpiRFot8E4F"]; - - for addr in &valid_mainnet { - let c_addr = CString::new(*addr).unwrap(); - let result = dash_spv_ffi_validate_address(c_addr.as_ptr(), FFINetwork::Dash); - assert_eq!(result, 1, "Address {} should be valid", addr); - } - - // Valid testnet addresses - let valid_testnet = ["yLbNV3FZZcU6f7P32Yzzwcbz6gpudmWgkx"]; - - for addr in &valid_testnet { - let c_addr = CString::new(*addr).unwrap(); - let result = dash_spv_ffi_validate_address(c_addr.as_ptr(), FFINetwork::Testnet); - assert_eq!(result, 1, "Address {} should be valid", addr); - } - - // Invalid addresses - let invalid = [ - "", - "invalid", - "1BitcoinAddress", - "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", // Bitcoin bech32 - "Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dg", // Missing character - "Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dgee", // Extra character - ]; - - for addr in &invalid { - let c_addr = CString::new(*addr).unwrap(); - let result = dash_spv_ffi_validate_address(c_addr.as_ptr(), FFINetwork::Dash); - assert_eq!(result, 0, "Address {} should be invalid", addr); - } - - // Test null address - let result = dash_spv_ffi_validate_address(std::ptr::null(), FFINetwork::Dash); - assert_eq!(result, 0); - } - } - - #[test] - #[serial] - fn test_watch_address_operations() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test adding valid address - let addr = CString::new("Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dge").unwrap(); - let result = dash_spv_ffi_client_watch_address(client, addr.as_ptr()); - assert_eq!(result, FFIErrorCode::ConfigError as i32); // Not implemented - - // Test adding same address again (should succeed) - let result = dash_spv_ffi_client_watch_address(client, addr.as_ptr()); - assert_eq!(result, FFIErrorCode::ConfigError as i32); // Not implemented - - // Test unwatching address - let result = dash_spv_ffi_client_unwatch_address(client, addr.as_ptr()); - assert_eq!(result, FFIErrorCode::ConfigError as i32); // Not implemented - - // Test unwatching non-watched address (should succeed) - let result = dash_spv_ffi_client_unwatch_address(client, addr.as_ptr()); - assert_eq!(result, FFIErrorCode::ConfigError as i32); // Not implemented - - // Test with invalid address - let invalid = CString::new("invalid_address").unwrap(); - let result = dash_spv_ffi_client_watch_address(client, invalid.as_ptr()); - assert_eq!(result, FFIErrorCode::InvalidArgument as i32); - - // Test with null - let result = dash_spv_ffi_client_watch_address(client, std::ptr::null()); - assert_eq!(result, FFIErrorCode::NullPointer as i32); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_watch_script_operations() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test adding valid script (P2PKH scriptPubKey) - let script_hex = "76a9146b8cc98ec5080b0b7adb10d040fb1572be9c35f888ac"; - let c_script = CString::new(script_hex).unwrap(); - let result = dash_spv_ffi_client_watch_script(client, c_script.as_ptr()); - assert_eq!(result, FFIErrorCode::ConfigError as i32); // Not implemented - - // Test with invalid hex - let invalid_hex = CString::new("not_hex").unwrap(); - let result = dash_spv_ffi_client_watch_script(client, invalid_hex.as_ptr()); - assert_eq!(result, FFIErrorCode::InvalidArgument as i32); - - // Test with odd-length hex - let odd_hex = CString::new("76a9").unwrap(); - let result = dash_spv_ffi_client_watch_script(client, odd_hex.as_ptr()); - assert_eq!(result, FFIErrorCode::InvalidArgument as i32); - - // Test empty script - let empty = CString::new("").unwrap(); - let result = dash_spv_ffi_client_watch_script(client, empty.as_ptr()); - assert_eq!(result, FFIErrorCode::InvalidArgument as i32); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_get_address_balance() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test getting balance for unwatched address - let addr = CString::new("XjSgy6PaVCB3V4KhCiCDkaVbx9ewxe9R1E").unwrap(); - let balance = dash_spv_ffi_client_get_address_balance(client, addr.as_ptr()); - - if !balance.is_null() { - let bal = &*balance; - // New wallet should have zero balance - assert_eq!(bal.confirmed, 0); - assert_eq!(bal.pending, 0); - assert_eq!(bal.instantlocked, 0); - - dash_spv_ffi_balance_destroy(balance); - } - - // Test with invalid address - let invalid = CString::new("invalid_address").unwrap(); - let balance = dash_spv_ffi_client_get_address_balance(client, invalid.as_ptr()); - assert!(balance.is_null()); - - // Check error was set - let error_ptr = dash_spv_ffi_get_last_error(); - assert!(!error_ptr.is_null()); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_get_address_utxos() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test getting UTXOs for address - let addr = CString::new("XjSgy6PaVCB3V4KhCiCDkaVbx9ewxe9R1E").unwrap(); - let mut utxos = dash_spv_ffi_client_get_address_utxos(client, addr.as_ptr()); - - // New wallet should have no UTXOs - assert_eq!(utxos.len, 0); - if !utxos.data.is_null() { - dash_spv_ffi_array_destroy(&mut utxos as *mut FFIArray); - } - - // Test with invalid address - let invalid = CString::new("invalid_address").unwrap(); - let utxos = dash_spv_ffi_client_get_address_utxos(client, invalid.as_ptr()); - assert!(utxos.data.is_null()); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_get_address_history() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test getting history for address - let addr = CString::new("XjSgy6PaVCB3V4KhCiCDkaVbx9ewxe9R1E").unwrap(); - let mut history = dash_spv_ffi_client_get_address_history(client, addr.as_ptr()); - - // New wallet should have no history - assert_eq!(history.len, 0); - if !history.data.is_null() { - dash_spv_ffi_array_destroy(&mut history as *mut FFIArray); - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_creation_from_mnemonic() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test creating a wallet from mnemonic - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let passphrase = CString::new("").unwrap(); - let name = CString::new("test_wallet").unwrap(); - - let wallet_id = dash_spv_ffi_wallet_create_from_mnemonic( - client, - mnemonic.as_ptr(), - passphrase.as_ptr(), - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - 0, // birth_height - ); - - assert!(!wallet_id.is_null()); - - // Verify we got a valid wallet ID string - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr); - assert!(!wallet_id_str.to_str().unwrap().is_empty()); - - // Clean up - if !wallet_id.is_null() { - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_creation_simple() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - let name = CString::new("simple_wallet").unwrap(); - - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - ); - - assert!(!wallet_id.is_null()); - - // Verify we got a valid wallet ID string - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr); - assert!(!wallet_id_str.to_str().unwrap().is_empty()); - - // Clean up - if !wallet_id.is_null() { - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_import_from_xprv() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test importing from extended private key - let xprv = CString::new("tprv8ZgxMBicQKsPdQXJz5N4j6 deviation squirrel supreme raw honey junk journey toddler impulse").unwrap(); - let name = CString::new("imported_wallet").unwrap(); - - let wallet_id = dash_spv_ffi_wallet_import_from_xprv( - client, - xprv.as_ptr(), - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - ); - - // Import might fail in test environment, so just check that we get a valid response - // (either success with non-null wallet_id, or failure with null) - if !wallet_id.is_null() { - // Verify we got a valid wallet ID string - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr); - assert!(!wallet_id_str.to_str().unwrap().is_empty()); - - // Clean up - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - // If null, that's also acceptable (import failed) - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_import_from_xpub() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test importing from extended public key - let xpub = - CString::new("tpubD6NzVbkrYhZ4X4rJGpM7KfxYFkGdJKjgGJGJZ7JXmT8yPzJzKQh8xkJfL") - .unwrap(); - let name = CString::new("watch_wallet").unwrap(); - - let wallet_id = dash_spv_ffi_wallet_import_from_xpub( - client, - xpub.as_ptr(), - FFINetwork::Regtest, - name.as_ptr(), - ); - - // Import might fail in test environment, so just check that we get a valid response - if !wallet_id.is_null() { - // Verify we got a valid wallet ID string - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr); - assert!(!wallet_id_str.to_str().unwrap().is_empty()); - - // Clean up - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - // If null, that's also acceptable (import failed) - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_balance_operations() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Create a test wallet first - let name = CString::new("balance_test_wallet").unwrap(); - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - ); - assert!(!wallet_id.is_null()); - - // Get wallet ID string for balance operations - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr).to_str().unwrap(); - - // Test getting balance - let wallet_id_cstr = CString::new(wallet_id_str).unwrap(); - let balance = dash_spv_ffi_wallet_get_balance(client, wallet_id_cstr.as_ptr()); - assert!(!balance.is_null()); - - // Verify balance structure - let balance_ref = &*balance; - assert_eq!(balance_ref.confirmed, 0); // New wallet should have 0 balance - assert_eq!(balance_ref.mempool, 0); - - // Clean up - dash_spv_ffi_balance_destroy(balance); - if !wallet_id.is_null() { - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_utxo_operations() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Create a test wallet first - let name = CString::new("utxo_test_wallet").unwrap(); - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - ); - assert!(!wallet_id.is_null()); - - // Get wallet ID string for UTXO operations - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr).to_str().unwrap(); - - // Test getting UTXOs - simplified to avoid memory issues - let wallet_id_cstr = CString::new(wallet_id_str).unwrap(); - let utxos = dash_spv_ffi_wallet_get_utxos(client, wallet_id_cstr.as_ptr()); - - // New wallet should have no UTXOs - // Note: utxos is FFIArray directly, not a pointer - assert_eq!(utxos.len, 0); - - // Skip array destruction for now to avoid memory corruption - if !wallet_id.is_null() { - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_listing() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Create a few test wallets - let names = ["wallet1", "wallet2", "wallet3"]; - let mut wallet_ids = Vec::new(); - - for name in &names { - let name_cstr = CString::new(*name).unwrap(); - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name_cstr.as_ptr(), - ); - assert!(!wallet_id.is_null()); - wallet_ids.push(wallet_id); - } - - // Test listing wallets - simplified to avoid memory issues - let wallet_list = dash_spv_ffi_wallet_list(client); - // Just ensure we can call the function without crashing - println!("Wallet list function called successfully"); - - // Clean up wallets only - for wallet_id in wallet_ids { - if !wallet_id.is_null() { - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - } - // Skip array destruction for now to avoid memory corruption - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_address_generation() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Create a test wallet first - let name = CString::new("address_test_wallet").unwrap(); - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - ); - - if !wallet_id.is_null() { - // Get wallet ID string for address operations - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr).to_str().unwrap(); - let wallet_id_cstr = CString::new(wallet_id_str).unwrap(); - - // Test receive address generation - let receive_address = dash_spv_ffi_wallet_get_receive_address( - client, - wallet_id_cstr.as_ptr(), - FFINetwork::Regtest, - 0, // account_index - FFIAccountTypePreference::BIP44, - false, // mark_as_used - ); - - if !receive_address.is_null() { - // Verify address generation result - let address_ref = &*receive_address; - if !address_ref.address.is_null() { - let address_str = - CStr::from_ptr((*address_ref.address).ptr).to_str().unwrap(); - assert!(!address_str.is_empty()); - } - - // Test change address generation - let change_address = dash_spv_ffi_wallet_get_change_address( - client, - wallet_id_cstr.as_ptr(), - FFINetwork::Regtest, - 0, // account_index - FFIAccountTypePreference::BIP44, - false, // mark_as_used - ); - - if !change_address.is_null() { - // Verify change address result - let change_address_ref = &*change_address; - if !change_address_ref.address.is_null() { - let change_address_str = - CStr::from_ptr((*change_address_ref.address).ptr).to_str().unwrap(); - assert!(!change_address_str.is_empty()); - } - - dash_spv_ffi_address_generation_result_destroy(change_address); - } - - dash_spv_ffi_address_generation_result_destroy(receive_address); - } - - // Clean up wallet - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } else { - println!("Wallet creation failed, skipping address generation test"); - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_mempool_balance() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Create a test wallet first - let name = CString::new("mempool_test_wallet").unwrap(); - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - ); - assert!(!wallet_id.is_null()); - - // Get wallet ID string for mempool operations - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr).to_str().unwrap(); - - // Test getting mempool balance for specific wallet - let wallet_id_cstr = CString::new(wallet_id_str).unwrap(); - let mempool_balance = dash_spv_ffi_wallet_get_mempool_balance( - client, - wallet_id_cstr.as_ptr(), - FFINetwork::Regtest, - ); - - // New wallet should have 0 mempool balance - assert_eq!(mempool_balance, 0); - - // Test getting total mempool balance across all wallets - let total_mempool_balance = dash_spv_ffi_wallet_get_mempool_balance( - client, - std::ptr::null(), // null means all wallets - FFINetwork::Regtest, - ); - - // Should also be 0 for new wallets - assert_eq!(total_mempool_balance, 0); - - // Clean up - if !wallet_id.is_null() { - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_mempool_transaction_count() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Create a test wallet first - let name = CString::new("mempool_tx_test_wallet").unwrap(); - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - ); - assert!(!wallet_id.is_null()); - - // Get wallet ID string for mempool operations - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr).to_str().unwrap(); - - // Test getting mempool transaction count for specific wallet - let wallet_id_cstr = CString::new(wallet_id_str).unwrap(); - let mempool_tx_count = dash_spv_ffi_wallet_get_mempool_transaction_count( - client, - wallet_id_cstr.as_ptr(), - FFINetwork::Regtest, - ); - - // New wallet should have 0 mempool transactions - assert_eq!(mempool_tx_count, 0); - - // Test getting total mempool transaction count across all wallets - let total_mempool_tx_count = dash_spv_ffi_wallet_get_mempool_transaction_count( - client, - std::ptr::null(), // null means all wallets - FFINetwork::Regtest, - ); - - // Should also be 0 for new wallets - assert_eq!(total_mempool_tx_count, 0); - - // Clean up - if !wallet_id.is_null() { - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_monitored_addresses() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test getting monitored addresses (should be empty for new client) - let addresses = - dash_spv_ffi_wallet_get_monitored_addresses(client, FFINetwork::Regtest); - - // New client should have no monitored addresses - assert!(!addresses.is_null()); - let addresses_ref = &*addresses; - assert_eq!(addresses_ref.len, 0); - - // Clean up - dash_spv_ffi_array_destroy(addresses); - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_account_operations() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Create a test wallet first - let name = CString::new("account_test_wallet").unwrap(); - let wallet_id = dash_spv_ffi_wallet_create( - client, - FFINetwork::Regtest, - FFIWalletAccountCreationOptions::BIP44AccountsOnly, - name.as_ptr(), - ); - - if !wallet_id.is_null() { - // Get wallet ID string for account operations - let wallet_id_str = CStr::from_ptr((*wallet_id).ptr).to_str().unwrap(); - let wallet_id_cstr = CString::new(wallet_id_str).unwrap(); - - // Test adding an account from extended public key - let xpub = - CString::new("tpubD6NzVbkrYhZ4X4rJGpM7KfxYFkGdJKjgGJGJZ7JXmT8yPzJzKQh8xkJfL") - .unwrap(); - - let result = dash_spv_ffi_wallet_add_account_from_xpub( - client, - wallet_id_cstr.as_ptr(), - xpub.as_ptr(), - FFIAccountType::BIP44, - FFINetwork::Regtest, - 0, // account_index - 0, // registration_index (not used for BIP44) - ); - - // Result may vary - just ensure we don't crash - println!("Account addition result: {}", result); - - // Clean up wallet - let string_struct = unsafe { Box::from_raw(wallet_id) }; - dash_spv_ffi_string_destroy(*string_struct); - } else { - println!("Wallet creation failed, skipping account operations test"); - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_error_handling() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test invalid wallet ID - let invalid_wallet_id = CString::new("invalid_wallet_id").unwrap(); - let balance = dash_spv_ffi_wallet_get_balance(client, invalid_wallet_id.as_ptr()); - - // Balance query behavior may vary - just ensure we don't crash - if !balance.is_null() { - dash_spv_ffi_balance_destroy(balance); - } - - // Test null wallet ID for balance - this might return null or a valid balance - let balance_null = dash_spv_ffi_wallet_get_balance(client, std::ptr::null()); - // Either result is acceptable - the important thing is no crash - if !balance_null.is_null() { - dash_spv_ffi_balance_destroy(balance_null); - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_transaction_operations() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test getting transaction with valid format but non-existent txid - let txid = - CString::new("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - .unwrap(); - let tx = dash_spv_ffi_client_get_transaction(client, txid.as_ptr()); - assert!(tx.is_null()); // Not found - - // Test with invalid txid format - let invalid_txid = CString::new("not_a_txid").unwrap(); - let tx = dash_spv_ffi_client_get_transaction(client, invalid_txid.as_ptr()); - assert!(tx.is_null()); - - // Test with wrong length txid - let short_txid = CString::new("0123456789abcdef").unwrap(); - let tx = dash_spv_ffi_client_get_transaction(client, short_txid.as_ptr()); - assert!(tx.is_null()); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_broadcast_transaction() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Create a minimal valid transaction hex (empty tx for testing) - // Version (4 bytes) + tx_in count (1 byte) + tx_out count (1 byte) + locktime (4 bytes) - let tx_hex = CString::new("0100000000000000000").unwrap(); - let result = dash_spv_ffi_client_broadcast_transaction(client, tx_hex.as_ptr()); - // Will likely fail due to invalid tx, but should handle gracefully - assert_ne!(result, FFIErrorCode::Success as i32); - - // Test with invalid hex - let invalid_hex = CString::new("not_hex").unwrap(); - let result = dash_spv_ffi_client_broadcast_transaction(client, invalid_hex.as_ptr()); - assert_eq!(result, FFIErrorCode::InvalidArgument as i32); - - // Test with null - let result = dash_spv_ffi_client_broadcast_transaction(client, std::ptr::null()); - assert_eq!(result, FFIErrorCode::NullPointer as i32); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - // Wrapper to make pointer Send - struct SendableClient(*mut FFIDashSpvClient); - unsafe impl Send for SendableClient {} - - #[test] - #[serial] - fn test_concurrent_wallet_operations() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - let client_ptr = Arc::new(Mutex::new(SendableClient(client))); - let mut handles = vec![]; - - // Multiple threads performing wallet operations - for i in 0..5 { - let client_clone = client_ptr.clone(); - let handle = thread::spawn(move || { - let client = client_clone.lock().unwrap().0; - - // Each thread watches different addresses - let addr = format!("XjSgy6PaVCB3V4KhCiCDkaVbx9ewxe9R{:02}", i); - let c_addr = CString::new(addr).unwrap(); - - // Try to watch address - let _ = dash_spv_ffi_client_watch_address(client, c_addr.as_ptr()); - - // Get balance - let balance = dash_spv_ffi_client_get_address_balance(client, c_addr.as_ptr()); - if !balance.is_null() { - dash_spv_ffi_balance_destroy(balance); - } - - // Get UTXOs - let mut utxos = dash_spv_ffi_client_get_address_utxos(client, c_addr.as_ptr()); - if !utxos.data.is_null() { - dash_spv_ffi_array_destroy(&mut utxos as *mut FFIArray); - } - }); - handles.push(handle); - } - - for handle in handles { - handle.join().unwrap(); - } - - let client = client_ptr.lock().unwrap().0; - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_error_recovery() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Clear any previous errors - dash_spv_ffi_clear_error(); - - // Trigger an error - let invalid = CString::new("invalid_address").unwrap(); - let result = dash_spv_ffi_client_watch_address(client, invalid.as_ptr()); - assert_eq!(result, FFIErrorCode::InvalidArgument as i32); - - // Verify error was set - let error1 = dash_spv_ffi_get_last_error(); - assert!(!error1.is_null()); - - // Perform successful operation - let valid = CString::new("Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dge").unwrap(); - let result = dash_spv_ffi_client_watch_address(client, valid.as_ptr()); - assert_eq!(result, FFIErrorCode::ConfigError as i32); // Not implemented - - // Error should still be the old one (success doesn't clear errors) - let error2 = dash_spv_ffi_get_last_error(); - assert!(!error2.is_null()); - - // Clear error - dash_spv_ffi_clear_error(); - let error3 = dash_spv_ffi_get_last_error(); - assert!(error3.is_null()); - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_empty_wallet_state() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test getting watched addresses (should be empty) - let mut addresses = dash_spv_ffi_client_get_watched_addresses(client); - assert_eq!(addresses.len, 0); - if !addresses.data.is_null() { - dash_spv_ffi_array_destroy(&mut addresses as *mut FFIArray); - } - - // Test getting watched scripts (should be empty) - let mut scripts = dash_spv_ffi_client_get_watched_scripts(client); - assert_eq!(scripts.len, 0); - if !scripts.data.is_null() { - dash_spv_ffi_array_destroy(&mut scripts as *mut FFIArray); - } - - // Test total balance (should be zero) - let balance = dash_spv_ffi_client_get_total_balance(client); - if !balance.is_null() { - let bal = &*balance; - assert_eq!(bal.confirmed, 0); - assert_eq!(bal.pending, 0); - assert_eq!(bal.instantlocked, 0); - dash_spv_ffi_balance_destroy(balance); - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_rescan_blockchain() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Add some addresses to watch - let addrs = - ["Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dge", "XasTb9LP4wwsvtqXG6ZUZEggpiRFot8E4F"]; - - for addr in &addrs { - let c_addr = CString::new(*addr).unwrap(); - let result = dash_spv_ffi_client_watch_address(client, c_addr.as_ptr()); - assert_eq!(result, FFIErrorCode::ConfigError as i32); // Not implemented - } - - // Test rescan from height 0 - let _result = dash_spv_ffi_client_rescan_blockchain(client, 0); - assert_eq!(_result, FFIErrorCode::ConfigError as i32); // Not implemented - - // Test rescan from specific height - let _result = dash_spv_ffi_client_rescan_blockchain(client, 100000); - assert_eq!(_result, FFIErrorCode::ConfigError as i32); // Not implemented - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_transaction_confirmation_status() { - unsafe { - let (client, config, _temp_dir) = create_test_wallet(); - assert!(!client.is_null()); - - // Test with non-existent transaction - let txid = - CString::new("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") - .unwrap(); - let confirmations = - dash_spv_ffi_client_get_transaction_confirmations(client, txid.as_ptr()); - assert_eq!(confirmations, -1); // Not found - - // Test is_transaction_confirmed - let confirmed = dash_spv_ffi_client_is_transaction_confirmed(client, txid.as_ptr()); - assert_eq!(confirmed, 0); // False - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - - #[test] - #[serial] - fn test_wallet_persistence() { - let temp_dir = TempDir::new().unwrap(); - let data_path = temp_dir.path().to_str().unwrap(); - - unsafe { - // Create wallet and add watched addresses - { - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - let path = CString::new(data_path).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - - let client = dash_spv_ffi_client_new(config); - assert!(!client.is_null()); - - // Add addresses to watch - let addrs = - ["Xan9iCVe1q5jYRDZ4VSMCtBjq2VyQA3Dge", "XasTb9LP4wwsvtqXG6ZUZEggpiRFot8E4F"]; - - for addr in &addrs { - let c_addr = CString::new(*addr).unwrap(); - let result = dash_spv_ffi_client_watch_address(client, c_addr.as_ptr()); - assert_eq!(result, FFIErrorCode::ConfigError as i32); // Not implemented - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - - // Create new wallet with same data dir - { - let config = dash_spv_ffi_config_new(FFINetwork::Regtest); - let path = CString::new(data_path).unwrap(); - dash_spv_ffi_config_set_data_dir(config, path.as_ptr()); - - let client = dash_spv_ffi_client_new(config); - assert!(!client.is_null()); - - // Check if watched addresses were persisted - let mut addresses = dash_spv_ffi_client_get_watched_addresses(client); - // Depending on implementation, addresses may or may not persist - if !addresses.data.is_null() { - dash_spv_ffi_array_destroy(&mut addresses as *mut FFIArray); - } - - dash_spv_ffi_client_destroy(client); - dash_spv_ffi_config_destroy(config); - } - } - } - - #[test] - #[serial] - fn test_wallet_null_operations() { - unsafe { - // Test all wallet operations with null client - let addr = CString::new("XjSgy6PaVCB3V4KhCiCDkaVbx9ewxe9R1E").unwrap(); - - assert_eq!( - dash_spv_ffi_client_watch_address(std::ptr::null_mut(), addr.as_ptr()), - FFIErrorCode::NullPointer as i32 - ); - - assert_eq!( - dash_spv_ffi_client_unwatch_address(std::ptr::null_mut(), addr.as_ptr()), - FFIErrorCode::NullPointer as i32 - ); - - assert_eq!( - dash_spv_ffi_client_watch_script(std::ptr::null_mut(), addr.as_ptr()), - FFIErrorCode::NullPointer as i32 - ); - - assert_eq!( - dash_spv_ffi_client_unwatch_script(std::ptr::null_mut(), addr.as_ptr()), - FFIErrorCode::NullPointer as i32 - ); - - assert!(dash_spv_ffi_client_get_address_balance(std::ptr::null_mut(), addr.as_ptr()) - .is_null()); - assert!(dash_spv_ffi_client_get_address_utxos(std::ptr::null_mut(), addr.as_ptr()) - .data - .is_null()); - assert!(dash_spv_ffi_client_get_address_history(std::ptr::null_mut(), addr.as_ptr()) - .data - .is_null()); - assert!( - dash_spv_ffi_client_get_transaction(std::ptr::null_mut(), addr.as_ptr()).is_null() - ); - - assert_eq!( - dash_spv_ffi_client_broadcast_transaction(std::ptr::null_mut(), addr.as_ptr()), - FFIErrorCode::NullPointer as i32 - ); - - assert!(dash_spv_ffi_client_get_watched_addresses(std::ptr::null_mut()).data.is_null()); - assert!(dash_spv_ffi_client_get_watched_scripts(std::ptr::null_mut()).data.is_null()); - assert!(dash_spv_ffi_client_get_total_balance(std::ptr::null_mut()).is_null()); - - assert_eq!( - dash_spv_ffi_client_rescan_blockchain(std::ptr::null_mut(), 0), - FFIErrorCode::NullPointer as i32 - ); - } - } -} diff --git a/dash-spv/peer_reputation.json b/dash-spv/peer_reputation.json index 344335c7d..680fb2b19 100644 --- a/dash-spv/peer_reputation.json +++ b/dash-spv/peer_reputation.json @@ -11,18 +11,40 @@ } ], [ - "18.237.170.32:19999", + "34.214.48.68:19999", { "score": 0, "ban_count": 0, "positive_actions": 0, "negative_actions": 0, - "connection_attempts": 1, + "connection_attempts": 2, "successful_connections": 0 } ], [ "34.210.84.163:19999", + { + "score": 0, + "ban_count": 0, + "positive_actions": 0, + "negative_actions": 0, + "connection_attempts": 3, + "successful_connections": 0 + } + ], + [ + "34.217.58.158:19999", + { + "score": 0, + "ban_count": 0, + "positive_actions": 0, + "negative_actions": 0, + "connection_attempts": 2, + "successful_connections": 0 + } + ], + [ + "18.237.170.32:19999", { "score": 0, "ban_count": 0, diff --git a/docs/implementation-notes/WALLET_SPV_INTEGRATION.md b/docs/implementation-notes/WALLET_SPV_INTEGRATION.md index 503ece47a..b1b10b4a6 100644 --- a/docs/implementation-notes/WALLET_SPV_INTEGRATION.md +++ b/docs/implementation-notes/WALLET_SPV_INTEGRATION.md @@ -67,13 +67,6 @@ Updated `DashSDK.connect()`: ## FFI Functions Used -- `dash_spv_ffi_watch_item_address()` - Create watch item for address -- `dash_spv_ffi_watch_item_script()` - Create watch item for script -- `dash_spv_ffi_watch_item_outpoint()` - Create watch item for outpoint -- `dash_spv_ffi_client_add_watch_item()` - Add watch item to client -- `dash_spv_ffi_client_remove_watch_item()` - Remove watch item from client -- `dash_spv_ffi_watch_item_destroy()` - Clean up watch item memory - ## Testing To test the integration: diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 7741540fa..6d12acc7d 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -130,23 +130,23 @@ typedef enum { Derivation path type for DIP9 */ typedef enum { - UNKNOWN = 0, - BIP32 = 1, - BIP44 = 2, - BLOCKCHAIN_IDENTITIES = 3, - PROVIDER_FUNDS = 4, - PROVIDER_VOTING_KEYS = 5, - PROVIDER_OPERATOR_KEYS = 6, - PROVIDER_OWNER_KEYS = 7, - CONTACT_BASED_FUNDS = 8, - CONTACT_BASED_FUNDS_ROOT = 9, - CONTACT_BASED_FUNDS_EXTERNAL = 10, - BLOCKCHAIN_IDENTITY_CREDIT_REGISTRATION_FUNDING = 11, - BLOCKCHAIN_IDENTITY_CREDIT_TOPUP_FUNDING = 12, - BLOCKCHAIN_IDENTITY_CREDIT_INVITATION_FUNDING = 13, - PROVIDER_PLATFORM_NODE_KEYS = 14, - COIN_JOIN = 15, - ROOT = 255, + PATH_UNKNOWN = 0, + PATH_BIP32 = 1, + PATH_BIP44 = 2, + PATH_BLOCKCHAIN_IDENTITIES = 3, + PATH_PROVIDER_FUNDS = 4, + PATH_PROVIDER_VOTING_KEYS = 5, + PATH_PROVIDER_OPERATOR_KEYS = 6, + PATH_PROVIDER_OWNER_KEYS = 7, + PATH_CONTACT_BASED_FUNDS = 8, + PATH_CONTACT_BASED_FUNDS_ROOT = 9, + PATH_CONTACT_BASED_FUNDS_EXTERNAL = 10, + PATH_BLOCKCHAIN_IDENTITY_CREDIT_REGISTRATION_FUNDING = 11, + PATH_BLOCKCHAIN_IDENTITY_CREDIT_TOPUP_FUNDING = 12, + PATH_BLOCKCHAIN_IDENTITY_CREDIT_INVITATION_FUNDING = 13, + PATH_PROVIDER_PLATFORM_NODE_KEYS = 14, + PATH_COIN_JOIN = 15, + PATH_ROOT = 255, } FFIDerivationPathType; /* diff --git a/key-wallet-ffi/src/derivation.rs b/key-wallet-ffi/src/derivation.rs index 05e40e91f..6df61734b 100644 --- a/key-wallet-ffi/src/derivation.rs +++ b/key-wallet-ffi/src/derivation.rs @@ -12,23 +12,23 @@ use crate::types::FFINetwork; #[repr(C)] #[derive(Clone, Copy)] pub enum FFIDerivationPathType { - Unknown = 0, - BIP32 = 1, - BIP44 = 2, - BlockchainIdentities = 3, - ProviderFunds = 4, - ProviderVotingKeys = 5, - ProviderOperatorKeys = 6, - ProviderOwnerKeys = 7, - ContactBasedFunds = 8, - ContactBasedFundsRoot = 9, - ContactBasedFundsExternal = 10, - BlockchainIdentityCreditRegistrationFunding = 11, - BlockchainIdentityCreditTopupFunding = 12, - BlockchainIdentityCreditInvitationFunding = 13, - ProviderPlatformNodeKeys = 14, - CoinJoin = 15, - Root = 255, + PathUnknown = 0, + PathBIP32 = 1, + PathBIP44 = 2, + PathBlockchainIdentities = 3, + PathProviderFunds = 4, + PathProviderVotingKeys = 5, + PathProviderOperatorKeys = 6, + PathProviderOwnerKeys = 7, + PathContactBasedFunds = 8, + PathContactBasedFundsRoot = 9, + PathContactBasedFundsExternal = 10, + PathBlockchainIdentityCreditRegistrationFunding = 11, + PathBlockchainIdentityCreditTopupFunding = 12, + PathBlockchainIdentityCreditInvitationFunding = 13, + PathProviderPlatformNodeKeys = 14, + PathCoinJoin = 15, + PathRoot = 255, } /// Extended private key structure @@ -730,34 +730,34 @@ pub unsafe extern "C" fn dip9_derive_identity_key( }; let base_path = match (network_rust, key_type) { - (key_wallet::Network::Dash, FFIDerivationPathType::BlockchainIdentities) => { + (key_wallet::Network::Dash, FFIDerivationPathType::PathBlockchainIdentities) => { IDENTITY_AUTHENTICATION_PATH_MAINNET } ( key_wallet::Network::Testnet | key_wallet::Network::Devnet | key_wallet::Network::Regtest, - FFIDerivationPathType::BlockchainIdentities, + FFIDerivationPathType::PathBlockchainIdentities, ) => IDENTITY_AUTHENTICATION_PATH_TESTNET, ( key_wallet::Network::Dash, - FFIDerivationPathType::BlockchainIdentityCreditRegistrationFunding, + FFIDerivationPathType::PathBlockchainIdentityCreditRegistrationFunding, ) => IDENTITY_REGISTRATION_PATH_MAINNET, ( key_wallet::Network::Testnet | key_wallet::Network::Devnet | key_wallet::Network::Regtest, - FFIDerivationPathType::BlockchainIdentityCreditRegistrationFunding, + FFIDerivationPathType::PathBlockchainIdentityCreditRegistrationFunding, ) => IDENTITY_REGISTRATION_PATH_TESTNET, ( key_wallet::Network::Dash, - FFIDerivationPathType::BlockchainIdentityCreditTopupFunding, + FFIDerivationPathType::PathBlockchainIdentityCreditTopupFunding, ) => IDENTITY_TOPUP_PATH_MAINNET, ( key_wallet::Network::Testnet | key_wallet::Network::Devnet | key_wallet::Network::Regtest, - FFIDerivationPathType::BlockchainIdentityCreditTopupFunding, + FFIDerivationPathType::PathBlockchainIdentityCreditTopupFunding, ) => IDENTITY_TOPUP_PATH_TESTNET, _ => { FFIError::set_error( @@ -771,7 +771,7 @@ pub unsafe extern "C" fn dip9_derive_identity_key( // Build additional path based on key type let additional_path = match key_type { - FFIDerivationPathType::BlockchainIdentities => { + FFIDerivationPathType::PathBlockchainIdentities => { // Authentication: identity_index'/key_index' let cn1 = match ChildNumber::from_hardened_idx(identity_index) { Ok(v) => v, @@ -797,7 +797,7 @@ pub unsafe extern "C" fn dip9_derive_identity_key( }; DerivationPath::from(vec![cn1, cn2]) } - FFIDerivationPathType::BlockchainIdentityCreditRegistrationFunding => { + FFIDerivationPathType::PathBlockchainIdentityCreditRegistrationFunding => { // Registration: index' let cn = match ChildNumber::from_hardened_idx(identity_index) { Ok(v) => v, @@ -812,7 +812,7 @@ pub unsafe extern "C" fn dip9_derive_identity_key( }; DerivationPath::from(vec![cn]) } - FFIDerivationPathType::BlockchainIdentityCreditTopupFunding => { + FFIDerivationPathType::PathBlockchainIdentityCreditTopupFunding => { // Top-up: identity_index'/topup_index' let cn1 = match ChildNumber::from_hardened_idx(identity_index) { Ok(v) => v, diff --git a/key-wallet-ffi/src/derivation_tests.rs b/key-wallet-ffi/src/derivation_tests.rs index 15a1a026e..1a7c41841 100644 --- a/key-wallet-ffi/src/derivation_tests.rs +++ b/key-wallet-ffi/src/derivation_tests.rs @@ -307,9 +307,9 @@ mod tests { seed.as_ptr(), seed.len(), FFINetwork::Testnet, - 0, // identity index - 0, // key index - FFIDerivationPathType::BlockchainIdentities, // key_type + 0, // identity index + 0, // key index + FFIDerivationPathType::PathBlockchainIdentities, // key_type &mut error, ) }; @@ -770,7 +770,7 @@ mod tests { FFINetwork::Testnet, 0, 0, - FFIDerivationPathType::BlockchainIdentities, + FFIDerivationPathType::PathBlockchainIdentities, &mut error, ) }; @@ -794,7 +794,7 @@ mod tests { FFINetwork::Testnet, 0, 0, - FFIDerivationPathType::BlockchainIdentities, + FFIDerivationPathType::PathBlockchainIdentities, &mut error, ) }; diff --git a/key-wallet-ffi/src/lib.rs b/key-wallet-ffi/src/lib.rs index 1090e2021..4eaebb12f 100644 --- a/key-wallet-ffi/src/lib.rs +++ b/key-wallet-ffi/src/lib.rs @@ -28,8 +28,10 @@ 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 utxo::FFIUTXO; // ============================================================================ // Initialization and Version diff --git a/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h b/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h index 32fa586a3..15c3a4442 100644 --- a/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h +++ b/swift-dash-core-sdk/Sources/DashSPVFFI/include/dash_spv_ffi.h @@ -32,12 +32,6 @@ typedef enum FFIValidationMode { Full = 2, } FFIValidationMode; -typedef enum FFIWatchItemType { - Address = 0, - Script = 1, - Outpoint = 2, -} FFIWatchItemType; - typedef struct FFIClientConfig FFIClientConfig; /** @@ -91,20 +85,6 @@ typedef struct FFISpvStats { uint64_t uptime; } FFISpvStats; -typedef struct FFIWatchItem { - enum FFIWatchItemType item_type; - struct FFIString data; -} FFIWatchItem; - -typedef struct FFIBalance { - uint64_t confirmed; - uint64_t pending; - uint64_t instantlocked; - uint64_t mempool; - uint64_t mempool_instant; - uint64_t total; -} FFIBalance; - /** * FFI-safe array that transfers ownership of memory to the C caller. * @@ -146,6 +126,21 @@ typedef void (*MempoolConfirmedCallback)(const uint8_t (*txid)[32], typedef void (*MempoolRemovedCallback)(const uint8_t (*txid)[32], uint8_t reason, void *user_data); +typedef void (*CompactFilterMatchedCallback)(const uint8_t (*block_hash)[32], + const char *matched_scripts, + const char *wallet_id, + void *user_data); + +typedef void (*WalletTransactionCallback)(const char *wallet_id, + uint32_t account_index, + const uint8_t (*txid)[32], + bool confirmed, + int64_t amount, + const char *addresses, + uint32_t block_height, + bool is_ours, + void *user_data); + typedef struct FFIEventCallbacks { BlockCallback on_block; TransactionCallback on_transaction; @@ -153,6 +148,8 @@ typedef struct FFIEventCallbacks { MempoolTransactionCallback on_mempool_transaction_added; MempoolConfirmedCallback on_mempool_transaction_confirmed; MempoolRemovedCallback on_mempool_transaction_removed; + CompactFilterMatchedCallback on_compact_filter_matched; + WalletTransactionCallback on_wallet_transaction; void *user_data; } FFIEventCallbacks; @@ -212,52 +209,6 @@ typedef struct FFIUnconfirmedTransaction { uintptr_t addresses_len; } FFIUnconfirmedTransaction; -typedef struct FFIUtxo { - struct FFIString txid; - uint32_t vout; - uint64_t amount; - struct FFIString script_pubkey; - struct FFIString address; - uint32_t height; - bool is_coinbase; - bool is_confirmed; - bool is_instantlocked; -} FFIUtxo; - -typedef struct FFITransactionResult { - struct FFIString txid; - int32_t version; - uint32_t locktime; - uint32_t size; - uint32_t weight; - uint64_t fee; - uint64_t confirmation_time; - uint32_t confirmation_height; -} FFITransactionResult; - -typedef struct FFIBlockResult { - struct FFIString hash; - uint32_t height; - uint32_t time; - uint32_t tx_count; -} FFIBlockResult; - -typedef struct FFIFilterMatch { - struct FFIString block_hash; - uint32_t height; - bool block_requested; -} FFIFilterMatch; - -typedef struct FFIAddressStats { - struct FFIString address; - uint32_t utxo_count; - uint64_t total_value; - uint64_t confirmed_value; - uint64_t pending_value; - uint32_t spendable_count; - uint32_t coinbase_count; -} FFIAddressStats; - struct FFIDashSpvClient *dash_spv_ffi_client_new(const struct FFIClientConfig *config); int32_t dash_spv_ffi_client_start(struct FFIDashSpvClient *client); @@ -361,14 +312,8 @@ struct FFISpvStats *dash_spv_ffi_client_get_stats(struct FFIDashSpvClient *clien bool dash_spv_ffi_client_is_filter_sync_available(struct FFIDashSpvClient *client); -int32_t dash_spv_ffi_client_add_watch_item(struct FFIDashSpvClient *client, - const struct FFIWatchItem *item); - -int32_t dash_spv_ffi_client_remove_watch_item(struct FFIDashSpvClient *client, - const struct FFIWatchItem *item); - -struct FFIBalance *dash_spv_ffi_client_get_address_balance(struct FFIDashSpvClient *client, - const char *address); +FFIBalance *dash_spv_ffi_client_get_address_balance(struct FFIDashSpvClient *client, + const char *address); struct FFIArray dash_spv_ffi_client_get_utxos(struct FFIDashSpvClient *client); @@ -405,7 +350,7 @@ struct FFIArray dash_spv_ffi_client_get_watched_addresses(struct FFIDashSpvClien struct FFIArray dash_spv_ffi_client_get_watched_scripts(struct FFIDashSpvClient *client); -struct FFIBalance *dash_spv_ffi_client_get_total_balance(struct FFIDashSpvClient *client); +FFIBalance *dash_spv_ffi_client_get_total_balance(struct FFIDashSpvClient *client); int32_t dash_spv_ffi_client_rescan_blockchain(struct FFIDashSpvClient *client, uint32_t _from_height); @@ -424,14 +369,14 @@ struct FFIArray dash_spv_ffi_client_get_address_utxos(struct FFIDashSpvClient *c int32_t dash_spv_ffi_client_enable_mempool_tracking(struct FFIDashSpvClient *client, enum FFIMempoolStrategy strategy); -struct FFIBalance *dash_spv_ffi_client_get_balance_with_mempool(struct FFIDashSpvClient *client); +FFIBalance *dash_spv_ffi_client_get_balance_with_mempool(struct FFIDashSpvClient *client); int32_t dash_spv_ffi_client_get_mempool_transaction_count(struct FFIDashSpvClient *client); int32_t dash_spv_ffi_client_record_send(struct FFIDashSpvClient *client, const char *txid); -struct FFIBalance *dash_spv_ffi_client_get_mempool_balance(struct FFIDashSpvClient *client, - const char *address); +FFIBalance *dash_spv_ffi_client_get_mempool_balance(struct FFIDashSpvClient *client, + const char *address); struct FFIClientConfig *dash_spv_ffi_config_new(enum FFINetwork network); @@ -590,25 +535,3 @@ const char *dash_spv_ffi_version(void); const char *dash_spv_ffi_get_network_name(enum FFINetwork network); void dash_spv_ffi_enable_test_mode(void); - -struct FFIWatchItem *dash_spv_ffi_watch_item_address(const char *address); - -struct FFIWatchItem *dash_spv_ffi_watch_item_script(const char *script_hex); - -struct FFIWatchItem *dash_spv_ffi_watch_item_outpoint(const char *txid, uint32_t vout); - -void dash_spv_ffi_watch_item_destroy(struct FFIWatchItem *item); - -void dash_spv_ffi_balance_destroy(struct FFIBalance *balance); - -void dash_spv_ffi_utxo_destroy(struct FFIUtxo *utxo); - -void dash_spv_ffi_transaction_result_destroy(struct FFITransactionResult *tx); - -void dash_spv_ffi_block_result_destroy(struct FFIBlockResult *block); - -void dash_spv_ffi_filter_match_destroy(struct FFIFilterMatch *filter_match); - -void dash_spv_ffi_address_stats_destroy(struct FFIAddressStats *stats); - -int32_t dash_spv_ffi_validate_address(const char *address, enum FFINetwork network); diff --git a/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Core/FFIBridge.swift b/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Core/FFIBridge.swift index 3abadf4dc..722060862 100644 --- a/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Core/FFIBridge.swift +++ b/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Core/FFIBridge.swift @@ -148,35 +148,5 @@ internal enum FFIBridge { return try body(ptr, data.count) } } - - // MARK: - Type Conversions - - static func convertWatchItemType(_ type: WatchItemType) -> FFIWatchItemType { - switch type { - case .address: - return FFIWatchItemType(rawValue: 0) - case .script: - return FFIWatchItemType(rawValue: 1) - case .outpoint: - return FFIWatchItemType(rawValue: 2) - } - } - - static func createFFIWatchItem(type: WatchItemType, data: String) -> FFIWatchItem { - let cString = (data as NSString).utf8String! - let length = strlen(cString) - let ffiString = FFIString(ptr: UnsafeMutablePointer(mutating: cString), length: UInt(length)) - return FFIWatchItem( - item_type: convertWatchItemType(type), - data: ffiString - ) - } -} -// MARK: - Watch Item Type - -public enum WatchItemType { - case address - case script - case outpoint -} \ No newline at end of file +} diff --git a/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Core/SPVClient.swift b/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Core/SPVClient.swift index 871e11f60..187db798f 100644 --- a/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Core/SPVClient.swift +++ b/swift-dash-core-sdk/Sources/SwiftDashCoreSDK/Core/SPVClient.swift @@ -612,78 +612,6 @@ public final class SPVClient { return dash_spv_ffi_client_is_filter_sync_available(client) } - // MARK: - Watch Items - - public func addWatchItem(type: WatchItemType, data: String) async throws { - guard isConnected, let client = client else { - throw DashSDKError.notConnected - } - - // Create FFI watch item based on type - let watchItem: UnsafeMutablePointer? - - switch type { - case .address: - watchItem = dash_spv_ffi_watch_item_address(data) - case .script: - watchItem = dash_spv_ffi_watch_item_script(data) - case .outpoint: - // For outpoint, we need to parse txid and vout from data - // Expected format: "txid:vout" - let components = data.split(separator: ":") - guard components.count == 2, - let vout = UInt32(components[1]) else { - throw DashSDKError.invalidArgument("Invalid outpoint format. Expected: txid:vout") - } - let txid = String(components[0]) - watchItem = dash_spv_ffi_watch_item_outpoint(txid, vout) - } - - guard let item = watchItem else { - throw DashSDKError.invalidArgument("Failed to create watch item") - } - defer { - dash_spv_ffi_watch_item_destroy(item) - } - - let result = dash_spv_ffi_client_add_watch_item(client, item) - try FFIBridge.checkError(result) - } - - public func removeWatchItem(type: WatchItemType, data: String) async throws { - guard isConnected, let client = client else { - throw DashSDKError.notConnected - } - - // Create FFI watch item based on type - let watchItem: UnsafeMutablePointer? - - switch type { - case .address: - watchItem = dash_spv_ffi_watch_item_address(data) - case .script: - watchItem = dash_spv_ffi_watch_item_script(data) - case .outpoint: - // For outpoint, we need to parse txid and vout from data - let components = data.split(separator: ":") - guard components.count == 2, - let vout = UInt32(components[1]) else { - throw DashSDKError.invalidArgument("Invalid outpoint format. Expected: txid:vout") - } - let txid = String(components[0]) - watchItem = dash_spv_ffi_watch_item_outpoint(txid, vout) - } - - guard let item = watchItem else { - throw DashSDKError.invalidArgument("Failed to create watch item") - } - defer { - dash_spv_ffi_watch_item_destroy(item) - } - - let result = dash_spv_ffi_client_remove_watch_item(client, item) - try FFIBridge.checkError(result) - } // MARK: - Lifecycle