diff --git a/dash-spv-ffi/FFI_API.md b/dash-spv-ffi/FFI_API.md index fd2fee041..1173a53d9 100644 --- a/dash-spv-ffi/FFI_API.md +++ b/dash-spv-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 48 +**Total Functions**: 49 ## Table of Contents @@ -12,6 +12,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I - [Configuration](#configuration) - [Synchronization](#synchronization) - [Wallet Operations](#wallet-operations) +- [Transaction Management](#transaction-management) - [Platform Integration](#platform-integration) - [Event Callbacks](#event-callbacks) - [Error Handling](#error-handling) @@ -75,6 +76,14 @@ Functions: 2 | `dash_spv_ffi_client_clear_wallet_event_callbacks` | Clear wallet event callbacks | client | | `dash_spv_ffi_client_set_wallet_event_callbacks` | Set wallet event callbacks for push-based event notifications | client | +### Transaction Management + +Functions: 1 + +| Function | Description | Module | +|----------|-------------|--------| +| `dash_spv_ffi_client_broadcast_transaction` | Broadcasts a transaction to the Dash network via connected peers | client | + ### Platform Integration Functions: 2 @@ -563,6 +572,24 @@ Set wallet event callbacks for push-based event notifications. The monitoring t --- +### Transaction Management - Detailed + +#### `dash_spv_ffi_client_broadcast_transaction` + +```c +dash_spv_ffi_client_broadcast_transaction(client: *mut FFIDashSpvClient, tx_bytes: *const u8, length: usize,) -> i32 +``` + +**Description:** +Broadcasts a transaction to the Dash network via connected peers. # Safety - `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient - `tx_bytes` must be a valid, non-null pointer to the transaction data - `length` must be the length of the transaction data in bytes + +**Safety:** +- `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient - `tx_bytes` must be a valid, non-null pointer to the transaction data - `length` must be the length of the transaction data in bytes + +**Module:** `client` + +--- + ### Platform Integration - Detailed #### `ffi_dash_spv_get_platform_activation_height` diff --git a/dash-spv-ffi/include/dash_spv_ffi.h b/dash-spv-ffi/include/dash_spv_ffi.h index 78bf81973..cb83e1d84 100644 --- a/dash-spv-ffi/include/dash_spv_ffi.h +++ b/dash-spv-ffi/include/dash_spv_ffi.h @@ -535,6 +535,21 @@ struct FFISyncProgress *dash_spv_ffi_client_get_manager_sync_progress(struct FFI */ int32_t dash_spv_ffi_client_clear_storage(struct FFIDashSpvClient *client) ; +/** + * Broadcasts a transaction to the Dash network via connected peers. + * + * # Safety + * + * - `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient + * - `tx_bytes` must be a valid, non-null pointer to the transaction data + * - `length` must be the length of the transaction data in bytes + */ + +int32_t dash_spv_ffi_client_broadcast_transaction(struct FFIDashSpvClient *client, + const uint8_t *tx_bytes, + uintptr_t length) +; + /** * Destroy the client and free associated resources. * diff --git a/dash-spv-ffi/src/client.rs b/dash-spv-ffi/src/client.rs index 00f7d27db..135dfadad 100644 --- a/dash-spv-ffi/src/client.rs +++ b/dash-spv-ffi/src/client.rs @@ -481,6 +481,47 @@ pub unsafe extern "C" fn dash_spv_ffi_client_clear_storage(client: *mut FFIDashS } } +/// Broadcasts a transaction to the Dash network via connected peers. +/// +/// # Safety +/// +/// - `client` must be a valid, non-null pointer to an initialized FFIDashSpvClient +/// - `tx_bytes` must be a valid, non-null pointer to the transaction data +/// - `length` must be the length of the transaction data in bytes +#[no_mangle] +pub unsafe extern "C" fn dash_spv_ffi_client_broadcast_transaction( + client: *mut FFIDashSpvClient, + tx_bytes: *const u8, + length: usize, +) -> i32 { + null_check!(client); + null_check!(tx_bytes); + + let tx_bytes = std::slice::from_raw_parts(tx_bytes, length); + + let tx = match dashcore::consensus::deserialize::(tx_bytes) { + Ok(t) => t, + Err(e) => { + set_last_error(&format!("Invalid transaction: {}", e)); + return FFIErrorCode::InvalidArgument as i32; + } + }; + + let client = &(*client); + + let spv_client = client.inner.clone(); + + let result = client.runtime.block_on(async { spv_client.broadcast_transaction(&tx).await }); + + match result { + Ok(_) => FFIErrorCode::Success as i32, + Err(e) => { + set_last_error(&format!("Failed to broadcast transaction: {}", e)); + FFIErrorCode::from(e) as i32 + } + } +} + /// Destroy the client and free associated resources. /// /// # Safety diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index e1f514166..485729ed9 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -4,7 +4,7 @@ This document provides a comprehensive reference for all FFI (Foreign Function I **Auto-generated**: This documentation is automatically generated from the source code. Do not edit manually. -**Total Functions**: 261 +**Total Functions**: 259 ## Table of Contents @@ -69,7 +69,7 @@ Functions: 19 ### Wallet Operations -Functions: 65 +Functions: 63 | Function | Description | Module | |----------|-------------|--------| @@ -109,7 +109,6 @@ Functions: 65 | `wallet_add_dashpay_receiving_account` | Add a DashPay receiving funds account # Safety - `wallet` must be a valid... | wallet | | `wallet_add_platform_payment_account` | Add a Platform Payment account (DIP-17) to the wallet Platform Payment... | wallet | | `wallet_build_and_sign_transaction` | Build and sign a transaction using the wallet's managed info This is the... | transaction | -| `wallet_build_transaction` | Build a transaction (unsigned) This creates an unsigned transaction | transaction | | `wallet_check_transaction` | Check if a transaction belongs to the wallet using ManagedWalletInfo #... | transaction | | `wallet_create_from_mnemonic` | Create a new wallet from mnemonic (backward compatibility - single network) ... | wallet | | `wallet_create_from_mnemonic_with_options` | Create a new wallet from mnemonic with options # Safety - `mnemonic` must... | wallet | @@ -137,7 +136,6 @@ Functions: 65 | `wallet_get_xpub` | Get extended public key for account # Safety - `wallet` must be a valid... | wallet | | `wallet_has_mnemonic` | Check if wallet has mnemonic # Safety - `wallet` must be a valid pointer... | wallet | | `wallet_is_watch_only` | Check if wallet is watch-only # Safety - `wallet` must be a valid pointer... | wallet | -| `wallet_sign_transaction` | Sign a transaction # Safety - `wallet` must be a valid pointer to an... | transaction | ### Account Management @@ -1286,30 +1284,14 @@ This function dereferences a raw pointer to FFIWallet. The caller must ensure th #### `wallet_build_and_sign_transaction` ```c -wallet_build_and_sign_transaction(managed_wallet: *mut FFIManagedWalletInfo, wallet: *const FFIWallet, account_index: c_uint, outputs: *const FFITxOutput, outputs_count: usize, fee_per_kb: u64, current_height: u32, tx_bytes_out: *mut *mut u8, tx_len_out: *mut usize, error: *mut FFIError,) -> bool +wallet_build_and_sign_transaction(manager: *const FFIWalletManager, wallet: *const FFIWallet, account_index: u32, outputs: *const FFITxOutput, outputs_count: usize, fee_rate: FFIFeeRate, fee_out: *mut u64, tx_bytes_out: *mut *mut u8, tx_len_out: *mut usize, error: *mut FFIError,) -> bool ``` **Description:** -Build and sign a transaction using the wallet's managed info This is the recommended way to build transactions. It handles: - UTXO selection using coin selection algorithms - Fee calculation - Change address generation - Transaction signing # Safety - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` +Build and sign a transaction using the wallet's managed info This is the recommended way to build transactions. It handles: - UTXO selection using coin selection algorithms - Fee calculation - Change address generation - Transaction signing # Safety - `manager` must be a valid pointer to an FFIWalletManager - `wallet` must be a valid pointer to an FFIWallet - `account_index` must be a valid BIP44 account index present in the wallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - `fee_rate` must be a valid variant of FFIFeeRate - `fee_out` must be a valid, non-null pointer to a `u64`; on success it receives the calculated transaction fee in duffs - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` **Safety:** -- `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo - `wallet` must be a valid pointer to an FFIWallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` - -**Module:** `transaction` - ---- - -#### `wallet_build_transaction` - -```c -wallet_build_transaction(wallet: *mut FFIWallet, account_index: c_uint, outputs: *const FFITxOutput, outputs_count: usize, fee_per_kb: u64, tx_bytes_out: *mut *mut u8, tx_len_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Build a transaction (unsigned) This creates an unsigned transaction. Use wallet_sign_transaction to sign it afterward. For a combined build+sign operation, use wallet_build_and_sign_transaction. # Safety - `wallet` must be a valid pointer to an FFIWallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` +- `manager` must be a valid pointer to an FFIWalletManager - `wallet` must be a valid pointer to an FFIWallet - `account_index` must be a valid BIP44 account index present in the wallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - `fee_rate` must be a valid variant of FFIFeeRate - `fee_out` must be a valid, non-null pointer to a `u64`; on success it receives the calculated transaction fee in duffs - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` **Module:** `transaction` @@ -1747,22 +1729,6 @@ Check if wallet is watch-only # Safety - `wallet` must be a valid pointer to a --- -#### `wallet_sign_transaction` - -```c -wallet_sign_transaction(wallet: *const FFIWallet, tx_bytes: *const u8, tx_len: usize, signed_tx_out: *mut *mut u8, signed_len_out: *mut usize, error: *mut FFIError,) -> bool -``` - -**Description:** -Sign a transaction # Safety - `wallet` must be a valid pointer to an FFIWallet - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `signed_tx_out` must be a valid pointer to store the signed transaction bytes pointer - `signed_len_out` must be a valid pointer to store the signed transaction length - `error` must be a valid pointer to an FFIError - The returned signed transaction bytes must be freed with `transaction_bytes_free` - -**Safety:** -- `wallet` must be a valid pointer to an FFIWallet - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - `signed_tx_out` must be a valid pointer to store the signed transaction bytes pointer - `signed_len_out` must be a valid pointer to store the signed transaction length - `error` must be a valid pointer to an FFIError - The returned signed transaction bytes must be freed with `transaction_bytes_free` - -**Module:** `transaction` - ---- - ### Account Management - Detailed #### `account_collection_count` diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 4353e5117..5123196ea 100644 --- a/key-wallet-ffi/include/key_wallet_ffi.h +++ b/key-wallet-ffi/include/key_wallet_ffi.h @@ -169,6 +169,12 @@ typedef enum { SPANISH = 9, } FFILanguage; +typedef enum { + ECONOMY = 0, + NORMAL = 1, + PRIORITY = 2, +} FFIFeeRate; + /* FFI-compatible transaction context */ @@ -3500,53 +3506,6 @@ bool mnemonic_to_seed(const char *mnemonic, */ void mnemonic_free(char *mnemonic) ; -/* - Build a transaction (unsigned) - - This creates an unsigned transaction. Use wallet_sign_transaction to sign it afterward. - For a combined build+sign operation, use wallet_build_and_sign_transaction. - - # Safety - - - `wallet` must be a valid pointer to an FFIWallet - - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements - - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - - `tx_len_out` must be a valid pointer to store the transaction length - - `error` must be a valid pointer to an FFIError - - The returned transaction bytes must be freed with `transaction_bytes_free` - */ - -bool wallet_build_transaction(FFIWallet *wallet, - unsigned int account_index, - const FFITxOutput *outputs, - size_t outputs_count, - uint64_t fee_per_kb, - uint8_t **tx_bytes_out, - size_t *tx_len_out, - FFIError *error) -; - -/* - Sign a transaction - - # Safety - - - `wallet` must be a valid pointer to an FFIWallet - - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes - - `signed_tx_out` must be a valid pointer to store the signed transaction bytes pointer - - `signed_len_out` must be a valid pointer to store the signed transaction length - - `error` must be a valid pointer to an FFIError - - The returned signed transaction bytes must be freed with `transaction_bytes_free` - */ - -bool wallet_sign_transaction(const FFIWallet *wallet, - const uint8_t *tx_bytes, - size_t tx_len, - uint8_t **signed_tx_out, - size_t *signed_len_out, - FFIError *error) -; - /* Build and sign a transaction using the wallet's managed info @@ -3558,22 +3517,26 @@ bool wallet_sign_transaction(const FFIWallet *wallet, # Safety - - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo + - `manager` must be a valid pointer to an FFIWalletManager - `wallet` must be a valid pointer to an FFIWallet + - `account_index` must be a valid BIP44 account index present in the wallet - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements + - `fee_rate` must be a valid variant of FFIFeeRate + - `fee_out` must be a valid, non-null pointer to a `u64`; on success it receives the + calculated transaction fee in duffs - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer - `tx_len_out` must be a valid pointer to store the transaction length - `error` must be a valid pointer to an FFIError - The returned transaction bytes must be freed with `transaction_bytes_free` */ -bool wallet_build_and_sign_transaction(FFIManagedWalletInfo *managed_wallet, +bool wallet_build_and_sign_transaction(const FFIWalletManager *manager, const FFIWallet *wallet, - unsigned int account_index, + uint32_t account_index, const FFITxOutput *outputs, size_t outputs_count, - uint64_t fee_per_kb, - uint32_t current_height, + FFIFeeRate fee_rate, + uint64_t *fee_out, uint8_t **tx_bytes_out, size_t *tx_len_out, FFIError *error) diff --git a/key-wallet-ffi/src/transaction.rs b/key-wallet-ffi/src/transaction.rs index 10ace390e..7dcf638da 100644 --- a/key-wallet-ffi/src/transaction.rs +++ b/key-wallet-ffi/src/transaction.rs @@ -1,7 +1,7 @@ //! Transaction building and management use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_uint}; +use std::os::raw::c_char; use std::ptr; use std::slice; @@ -9,11 +9,13 @@ use dashcore::{ consensus, hashes::Hash, sighash::SighashCache, EcdsaSighashType, Network, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut, Txid, }; +use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface; +use key_wallet_manager::FeeRate; use secp256k1::{Message, Secp256k1, SecretKey}; use crate::error::{FFIError, FFIErrorCode}; -use crate::managed_wallet::FFIManagedWalletInfo; use crate::types::{FFINetwork, FFITransactionContext, FFIWallet}; +use crate::FFIWalletManager; // MARK: - Transaction Types @@ -55,89 +57,20 @@ pub struct FFITxOutput { pub amount: u64, } -/// Build a transaction (unsigned) -/// -/// This creates an unsigned transaction. Use wallet_sign_transaction to sign it afterward. -/// For a combined build+sign operation, use wallet_build_and_sign_transaction. -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements -/// - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer -/// - `tx_len_out` must be a valid pointer to store the transaction length -/// - `error` must be a valid pointer to an FFIError -/// - The returned transaction bytes must be freed with `transaction_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_build_transaction( - wallet: *mut FFIWallet, - account_index: c_uint, - outputs: *const FFITxOutput, - outputs_count: usize, - fee_per_kb: u64, - tx_bytes_out: *mut *mut u8, - tx_len_out: *mut usize, - error: *mut FFIError, -) -> bool { - if wallet.is_null() || outputs.is_null() || tx_bytes_out.is_null() || tx_len_out.is_null() { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); - return false; - } - - unsafe { - let _wallet = &mut *wallet; - let _outputs_slice = slice::from_raw_parts(outputs, outputs_count); - let _account_index = account_index; - let _fee_per_kb = fee_per_kb; - - // Note: This function creates unsigned transactions. - // A full implementation would require ManagedWalletInfo integration. - // For now, return an error directing users to the combined function. - FFIError::set_error( - error, - FFIErrorCode::WalletError, - "Use wallet_build_and_sign_transaction for transaction creation".to_string(), - ); - false - } +#[repr(C)] +pub enum FFIFeeRate { + Economy = 0, + Normal = 1, + Priority = 2, } -/// Sign a transaction -/// -/// # Safety -/// -/// - `wallet` must be a valid pointer to an FFIWallet -/// - `tx_bytes` must be a valid pointer to transaction bytes with at least `tx_len` bytes -/// - `signed_tx_out` must be a valid pointer to store the signed transaction bytes pointer -/// - `signed_len_out` must be a valid pointer to store the signed transaction length -/// - `error` must be a valid pointer to an FFIError -/// - The returned signed transaction bytes must be freed with `transaction_bytes_free` -#[no_mangle] -pub unsafe extern "C" fn wallet_sign_transaction( - wallet: *const FFIWallet, - tx_bytes: *const u8, - tx_len: usize, - signed_tx_out: *mut *mut u8, - signed_len_out: *mut usize, - error: *mut FFIError, -) -> bool { - if wallet.is_null() || tx_bytes.is_null() || signed_tx_out.is_null() || signed_len_out.is_null() - { - FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); - return false; - } - - unsafe { - let _wallet = &*wallet; - let _tx_slice = slice::from_raw_parts(tx_bytes, tx_len); - - // Note: Transaction signing would require implementing wallet signing logic - FFIError::set_error( - error, - FFIErrorCode::WalletError, - "Transaction signing not yet implemented".to_string(), - ); - false +impl From for FeeRate { + fn from(value: FFIFeeRate) -> Self { + match value { + FFIFeeRate::Economy => FeeRate::economy(), + FFIFeeRate::Normal => FeeRate::normal(), + FFIFeeRate::Priority => FeeRate::priority(), + } } } @@ -151,32 +84,37 @@ pub unsafe extern "C" fn wallet_sign_transaction( /// /// # Safety /// -/// - `managed_wallet` must be a valid pointer to an FFIManagedWalletInfo +/// - `manager` must be a valid pointer to an FFIWalletManager /// - `wallet` must be a valid pointer to an FFIWallet +/// - `account_index` must be a valid BIP44 account index present in the wallet /// - `outputs` must be a valid pointer to an array of FFITxOutput with at least `outputs_count` elements +/// - `fee_rate` must be a valid variant of FFIFeeRate +/// - `fee_out` must be a valid, non-null pointer to a `u64`; on success it receives the +/// calculated transaction fee in duffs /// - `tx_bytes_out` must be a valid pointer to store the transaction bytes pointer /// - `tx_len_out` must be a valid pointer to store the transaction length /// - `error` must be a valid pointer to an FFIError /// - The returned transaction bytes must be freed with `transaction_bytes_free` #[no_mangle] pub unsafe extern "C" fn wallet_build_and_sign_transaction( - managed_wallet: *mut FFIManagedWalletInfo, + manager: *const FFIWalletManager, wallet: *const FFIWallet, - account_index: c_uint, + account_index: u32, outputs: *const FFITxOutput, outputs_count: usize, - fee_per_kb: u64, - current_height: u32, + fee_rate: FFIFeeRate, + fee_out: *mut u64, tx_bytes_out: *mut *mut u8, tx_len_out: *mut usize, error: *mut FFIError, ) -> bool { // Validate inputs - if managed_wallet.is_null() + if manager.is_null() || wallet.is_null() || outputs.is_null() || tx_bytes_out.is_null() || tx_len_out.is_null() + || fee_out.is_null() { FFIError::set_error(error, FFIErrorCode::InvalidInput, "Null pointer provided".to_string()); return false; @@ -191,23 +129,33 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( return false; } - unsafe { - use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; - use key_wallet::wallet::managed_wallet_info::fee::{FeeLevel, FeeRate}; - use key_wallet::wallet::managed_wallet_info::transaction_builder::TransactionBuilder; - - let managed_wallet_ref = &mut *managed_wallet; - let wallet_ref = &*wallet; - let network_rust = managed_wallet_ref.inner().network; - let outputs_slice = slice::from_raw_parts(outputs, outputs_count); - - // Get the managed account - let managed_account = match managed_wallet_ref - .inner_mut() - .accounts - .standard_bip44_accounts - .get_mut(&account_index) - { + use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy; + use key_wallet::wallet::managed_wallet_info::fee::FeeLevel; + use key_wallet::wallet::managed_wallet_info::transaction_builder::TransactionBuilder; + + let manager_ref = &*manager; + let wallet_ref = &*wallet; + let network_rust = wallet_ref.inner().network; + let outputs_slice = slice::from_raw_parts(outputs, outputs_count); + + let managed_wallet = manager_ref.runtime.block_on(async { + let manager = manager_ref.manager.write().await; + + manager.get_wallet_info(&wallet_ref.inner().wallet_id).cloned() + }); + + let Some(mut managed_wallet) = managed_wallet else { + FFIError::set_error( + error, + FFIErrorCode::InvalidInput, + "Could not obtain ManagedWalletInfo for the provided wallet".to_string(), + ); + return false; + }; + + // Get the managed account + let managed_account = + match managed_wallet.accounts.standard_bip44_accounts.get_mut(&account_index) { Some(account) => account, None => { FFIError::set_error( @@ -219,204 +167,187 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( } }; - // Verify wallet and managed wallet have matching networks - if wallet_ref.inner().network != network_rust { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - "Wallet and managed wallet have different networks".to_string(), - ); - return false; - } - - let wallet_account = - match wallet_ref.inner().accounts.standard_bip44_accounts.get(&account_index) { - Some(account) => account, - None => { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - format!("Wallet account {} not found", account_index), - ); - return false; - } - }; - - // Convert FFI outputs to Rust outputs - let mut tx_builder = TransactionBuilder::new(); - - for output in outputs_slice { - // Convert address from C string - let address_str = match CStr::from_ptr(output.address).to_str() { - Ok(s) => s, - Err(_) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidInput, - "Invalid UTF-8 in output address".to_string(), - ); - return false; - } - }; - - // Parse address using dashcore - use std::str::FromStr; - let address = match dashcore::Address::from_str(address_str) { - Ok(addr) => { - // Verify network matches - let addr_network = addr.require_network(network_rust).map_err(|e| { - FFIError::set_error( - error, - FFIErrorCode::InvalidAddress, - format!("Address network mismatch: {}", e), - ); - }); - if addr_network.is_err() { - return false; - } - addr_network.unwrap() - } - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::InvalidAddress, - format!("Invalid address: {}", e), - ); - return false; - } - }; - - // Add output - tx_builder = match tx_builder.add_output(&address, output.amount) { - Ok(builder) => builder, - Err(e) => { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - format!("Failed to add output: {}", e), - ); - return false; - } - }; - } - - // Get change address (next internal address) - let xpub = wallet_account.extended_public_key(); - let change_address = match managed_account.next_change_address(Some(&xpub), true) { - Ok(addr) => addr, - Err(e) => { + let wallet_account = + match wallet_ref.inner().accounts.standard_bip44_accounts.get(&account_index) { + Some(account) => account, + None => { FFIError::set_error( error, FFIErrorCode::WalletError, - format!("Failed to get change address: {}", e), + format!("Wallet account {} not found", account_index), ); return false; } }; - // Set change address and fee level - // Convert fee_per_kb to fee_per_byte (1 KB = 1000 bytes) - let fee_per_byte = fee_per_kb / 1000; - let fee_rate = FeeRate::from_duffs_per_byte(fee_per_byte); - tx_builder = - tx_builder.set_change_address(change_address).set_fee_level(FeeLevel::Custom(fee_rate)); - - // Get available UTXOs (collect owned UTXOs, not references) - let utxos: Vec = managed_account.utxos.values().cloned().collect(); - - // Get the wallet's root extended private key for signing - use key_wallet::wallet::WalletType; - - let root_xpriv = match &wallet_ref.inner().wallet_type { - WalletType::Mnemonic { - root_extended_private_key, - .. - } => root_extended_private_key, - WalletType::Seed { - root_extended_private_key, - .. - } => root_extended_private_key, - WalletType::ExtendedPrivKey(root_extended_private_key) => root_extended_private_key, - _ => { + // Convert FFI outputs to Rust outputs + let mut tx_builder = TransactionBuilder::new(); + + for output in outputs_slice { + // Convert address from C string + let address_str = match CStr::from_ptr(output.address).to_str() { + Ok(s) => s, + Err(_) => { FFIError::set_error( error, - FFIErrorCode::WalletError, - "Cannot sign with watch-only wallet".to_string(), + FFIErrorCode::InvalidInput, + "Invalid UTF-8 in output address".to_string(), ); return false; } }; - // Build a map of address -> derivation path for all addresses in the account - use std::collections::HashMap; - let mut address_to_path: HashMap = - HashMap::new(); - - // Collect from all address pools (receive, change, etc.) - for pool in managed_account.account_type.address_pools() { - for addr_info in pool.addresses.values() { - address_to_path.insert(addr_info.address.clone(), addr_info.path.clone()); + // Parse address using dashcore + use std::str::FromStr; + let address = match dashcore::Address::from_str(address_str) { + Ok(addr) => { + // Verify network matches + let addr_network = addr.require_network(network_rust).map_err(|e| { + FFIError::set_error( + error, + FFIErrorCode::InvalidAddress, + format!("Address network mismatch: {}", e), + ); + }); + if addr_network.is_err() { + return false; + } + addr_network.unwrap() } - } - - // Select inputs and build transaction - let tx_builder_with_inputs = match tx_builder.select_inputs( - &utxos, - SelectionStrategy::BranchAndBound, - current_height, - |utxo| { - // Look up the derivation path for this UTXO's address - let path = address_to_path.get(&utxo.address)?; - - // Convert root key to ExtendedPrivKey and derive the child key - let root_ext_priv = root_xpriv.to_extended_priv_key(network_rust); - let secp = secp256k1::Secp256k1::new(); - let derived_xpriv = root_ext_priv.derive_priv(&secp, path).ok()?; - - Some(derived_xpriv.private_key) - }, - ) { - Ok(builder) => builder, Err(e) => { FFIError::set_error( error, - FFIErrorCode::WalletError, - format!("Coin selection failed: {}", e), + FFIErrorCode::InvalidAddress, + format!("Invalid address: {}", e), ); return false; } }; - // Build and sign the transaction - let transaction = match tx_builder_with_inputs.build() { - Ok(tx) => tx, + // Add output + tx_builder = match tx_builder.add_output(&address, output.amount) { + Ok(builder) => builder, Err(e) => { FFIError::set_error( error, FFIErrorCode::WalletError, - format!("Failed to build transaction: {}", e), + format!("Failed to add output: {}", e), ); return false; } }; + } - // Serialize the transaction - let serialized = consensus::serialize(&transaction); - let size = serialized.len(); - - // Allocate memory for the result - let bytes = Vec::::with_capacity(size).into_boxed_slice(); - let tx_bytes = Box::into_raw(bytes) as *mut u8; + // Get change address (next internal address) + let xpub = wallet_account.extended_public_key(); + let change_address = match managed_account.next_change_address(Some(&xpub), true) { + Ok(addr) => addr, + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to get change address: {}", e), + ); + return false; + } + }; - // Copy the serialized transaction - ptr::copy_nonoverlapping(serialized.as_ptr(), tx_bytes, size); + tx_builder = tx_builder + .set_change_address(change_address) + .set_fee_level(FeeLevel::Custom(fee_rate.into())); + + // Get available UTXOs (collect owned UTXOs, not references) + let utxos: Vec = managed_account.utxos.values().cloned().collect(); + + // Get the wallet's root extended private key for signing + use key_wallet::wallet::WalletType; + + let root_xpriv = match &wallet_ref.inner().wallet_type { + WalletType::Mnemonic { + root_extended_private_key, + .. + } => root_extended_private_key, + WalletType::Seed { + root_extended_private_key, + .. + } => root_extended_private_key, + WalletType::ExtendedPrivKey(root_extended_private_key) => root_extended_private_key, + _ => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + "Cannot sign with watch-only wallet".to_string(), + ); + return false; + } + }; - *tx_bytes_out = tx_bytes; - *tx_len_out = size; + // Build a map of address -> derivation path for all addresses in the account + use std::collections::HashMap; + let mut address_to_path: HashMap = + HashMap::new(); - FFIError::set_success(error); - true + // Collect from all address pools (receive, change, etc.) + for pool in managed_account.account_type.address_pools() { + for addr_info in pool.addresses.values() { + address_to_path.insert(addr_info.address.clone(), addr_info.path.clone()); + } } + + // Select inputs and build transaction + let tx_builder_with_inputs = match tx_builder.select_inputs( + &utxos, + SelectionStrategy::BranchAndBound, + managed_wallet.synced_height(), + |utxo| { + // Look up the derivation path for this UTXO's address + let path = address_to_path.get(&utxo.address)?; + + // Convert root key to ExtendedPrivKey and derive the child key + let root_ext_priv = root_xpriv.to_extended_priv_key(network_rust); + let secp = secp256k1::Secp256k1::new(); + let derived_xpriv = root_ext_priv.derive_priv(&secp, path).ok()?; + + Some(derived_xpriv.private_key) + }, + ) { + Ok(builder) => builder, + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Coin selection failed: {}", e), + ); + return false; + } + }; + + *fee_out = tx_builder_with_inputs.calculate_fee(); + + // Build and sign the transaction + let transaction = match tx_builder_with_inputs.build() { + Ok(tx) => tx, + Err(e) => { + FFIError::set_error( + error, + FFIErrorCode::WalletError, + format!("Failed to build transaction: {}", e), + ); + return false; + } + }; + + let serialized = consensus::serialize(&transaction); + let size = serialized.len(); + + let boxed = serialized.into_boxed_slice(); + let tx_bytes = Box::into_raw(boxed) as *mut u8; + + *tx_bytes_out = tx_bytes; + *tx_len_out = size; + + FFIError::set_success(error); + true } // Transaction context for checking @@ -1066,7 +997,3 @@ pub unsafe extern "C" fn address_to_pubkey_hash( Err(_) => -1, } } - -#[cfg(test)] -#[path = "transaction_tests.rs"] -mod transaction_tests; diff --git a/key-wallet-ffi/src/transaction_tests.rs b/key-wallet-ffi/src/transaction_tests.rs deleted file mode 100644 index f68628b51..000000000 --- a/key-wallet-ffi/src/transaction_tests.rs +++ /dev/null @@ -1,256 +0,0 @@ -#[cfg(test)] -#[allow(clippy::module_inception)] -mod transaction_tests { - use super::super::*; - use crate::error::{FFIError, FFIErrorCode}; - use crate::wallet; - use crate::FFINetwork; - use std::ffi::CString; - use std::os::raw::c_char; - use std::ptr; - - #[test] - fn test_build_transaction_with_null_wallet() { - let mut error = FFIError::success(); - - let output = FFITxOutput { - address: CString::new("yXdxAYfK7KGx7gNpVHUfRsQMNpMj5cAadG").unwrap().into_raw(), - amount: 100000, - }; - - let mut tx_bytes_out: *mut u8 = ptr::null_mut(); - let mut tx_len_out: usize = 0; - - let success = unsafe { - wallet_build_transaction( - ptr::null_mut(), - 0, - &output, - 1, - 1000, - &mut tx_bytes_out, - &mut tx_len_out, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - let _ = CString::from_raw(output.address as *mut c_char); - error.free_message(); - } - } - - #[test] - fn test_build_transaction_with_null_outputs() { - let mut error = FFIError::success(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let passphrase = CString::new("").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic( - mnemonic.as_ptr(), - passphrase.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - - let mut tx_bytes_out: *mut u8 = ptr::null_mut(); - let mut tx_len_out: usize = 0; - - let success = unsafe { - wallet_build_transaction( - wallet, - 0, - ptr::null(), - 0, - 1000, - &mut tx_bytes_out, - &mut tx_len_out, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - error.free_message(); - } - } - - #[test] - fn test_sign_transaction_with_null_wallet() { - let mut error = FFIError::success(); - - let tx_bytes = [0u8; 100]; - let mut signed_tx_out: *mut u8 = ptr::null_mut(); - let mut signed_len_out: usize = 0; - - let success = unsafe { - wallet_sign_transaction( - ptr::null(), - tx_bytes.as_ptr(), - tx_bytes.len(), - &mut signed_tx_out, - &mut signed_len_out, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - unsafe { error.free_message() }; - } - - #[test] - fn test_sign_transaction_with_null_tx_bytes() { - let mut error = FFIError::success(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let passphrase = CString::new("").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic( - mnemonic.as_ptr(), - passphrase.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - - let mut signed_tx_out: *mut u8 = ptr::null_mut(); - let mut signed_len_out: usize = 0; - - let success = unsafe { - wallet_sign_transaction( - wallet, - ptr::null(), - 0, - &mut signed_tx_out, - &mut signed_len_out, - &mut error, - ) - }; - - assert!(!success); - assert_eq!(error.code, FFIErrorCode::InvalidInput); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - error.free_message(); - } - } - - #[test] - fn test_transaction_context_enum() { - // Test that enum values are as expected - assert_eq!(FFITransactionContext::Mempool as u32, 0); - assert_eq!(FFITransactionContext::InBlock as u32, 1); - assert_eq!(FFITransactionContext::InChainLockedBlock as u32, 2); - } - - #[test] - fn test_build_transaction_not_implemented() { - let mut error = FFIError::success(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let passphrase = CString::new("").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic( - mnemonic.as_ptr(), - passphrase.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - - let output = FFITxOutput { - address: CString::new("yXdxAYfK7KGx7gNpVHUfRsQMNpMj5cAadG").unwrap().into_raw(), - amount: 100000, - }; - - let mut tx_bytes_out: *mut u8 = ptr::null_mut(); - let mut tx_len_out: usize = 0; - - let success = unsafe { - wallet_build_transaction( - wallet, - 0, - &output, - 1, - 1000, - &mut tx_bytes_out, - &mut tx_len_out, - &mut error, - ) - }; - - // Should fail because not implemented - assert!(!success); - assert_eq!(error.code, FFIErrorCode::WalletError); - - // Clean up - unsafe { - let _ = CString::from_raw(output.address as *mut c_char); - wallet::wallet_free(wallet); - error.free_message(); - } - } - - #[test] - fn test_sign_transaction_not_implemented() { - let mut error = FFIError::success(); - - // Create a wallet - let mnemonic = CString::new("abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about").unwrap(); - let passphrase = CString::new("").unwrap(); - - let wallet = unsafe { - wallet::wallet_create_from_mnemonic( - mnemonic.as_ptr(), - passphrase.as_ptr(), - FFINetwork::Testnet, - &mut error, - ) - }; - - let tx_bytes = [0u8; 100]; - let mut signed_tx_out: *mut u8 = ptr::null_mut(); - let mut signed_len_out: usize = 0; - - let success = unsafe { - wallet_sign_transaction( - wallet, - tx_bytes.as_ptr(), - tx_bytes.len(), - &mut signed_tx_out, - &mut signed_len_out, - &mut error, - ) - }; - - // Should fail because not implemented - assert!(!success); - assert_eq!(error.code, FFIErrorCode::WalletError); - - // Clean up - unsafe { - wallet::wallet_free(wallet); - error.free_message(); - } - } -} diff --git a/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs b/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs index 4d823cc77..6a3afc66c 100644 --- a/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs +++ b/key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs @@ -356,6 +356,13 @@ impl TransactionBuilder { self.build_internal() } + pub fn calculate_fee(&self) -> u64 { + let fee_rate = self.fee_level.fee_rate(); + let estimated_size = + self.estimate_transaction_size(self.inputs.len(), self.outputs.len() + 1); + fee_rate.calculate_fee(estimated_size) + } + /// Internal build method that uses the stored special_payload fn build_internal(mut self) -> Result { if self.inputs.is_empty() { @@ -412,10 +419,7 @@ impl TransactionBuilder { let mut tx_outputs = self.outputs.clone(); - // Calculate fee - let fee_rate = self.fee_level.fee_rate(); - let estimated_size = self.estimate_transaction_size(tx_inputs.len(), tx_outputs.len() + 1); - let fee = fee_rate.calculate_fee(estimated_size); + let fee = self.calculate_fee(); let change_amount = total_input.saturating_sub(total_output).saturating_sub(fee);