diff --git a/key-wallet-ffi/FFI_API.md b/key-wallet-ffi/FFI_API.md index e1f514166..7a2e0b249 100644 --- a/key-wallet-ffi/FFI_API.md +++ b/key-wallet-ffi/FFI_API.md @@ -1286,14 +1286,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` +- `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` diff --git a/key-wallet-ffi/include/key_wallet_ffi.h b/key-wallet-ffi/include/key_wallet_ffi.h index 4353e5117..650391df9 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 */ @@ -3558,22 +3564,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..ce41f0958 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,14 @@ 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 libc::c_uint; 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 @@ -141,6 +144,23 @@ pub unsafe extern "C" fn wallet_sign_transaction( } } +#[repr(C)] +pub enum FFIFeeRate { + Economy = 0, + Normal = 1, + Priority = 2, +} + +impl From for FeeRate { + fn from(value: FFIFeeRate) -> Self { + match value { + FFIFeeRate::Economy => FeeRate::economy(), + FFIFeeRate::Normal => FeeRate::normal(), + FFIFeeRate::Priority => FeeRate::priority(), + } + } +} + /// Build and sign a transaction using the wallet's managed info /// /// This is the recommended way to build transactions. It handles: @@ -151,32 +171,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; @@ -193,41 +218,42 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( 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::fee::FeeLevel; use key_wallet::wallet::managed_wallet_info::transaction_builder::TransactionBuilder; - let managed_wallet_ref = &mut *managed_wallet; + let manager_ref = &*manager; let wallet_ref = &*wallet; - let network_rust = managed_wallet_ref.inner().network; + let network_rust = 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) - { - Some(account) => account, - None => { - FFIError::set_error( - error, - FFIErrorCode::WalletError, - format!("Account {} not found", account_index), - ); - return false; - } - }; + 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() + }); - // Verify wallet and managed wallet have matching networks - if wallet_ref.inner().network != network_rust { + let Some(mut managed_wallet) = managed_wallet else { FFIError::set_error( error, - FFIErrorCode::WalletError, - "Wallet and managed wallet have different networks".to_string(), + 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( + error, + FFIErrorCode::WalletError, + format!("Account {} not found", account_index), + ); + return false; + } + }; let wallet_account = match wallet_ref.inner().accounts.standard_bip44_accounts.get(&account_index) { @@ -314,12 +340,9 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( } }; - // 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)); + 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(); @@ -363,7 +386,7 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( let tx_builder_with_inputs = match tx_builder.select_inputs( &utxos, SelectionStrategy::BranchAndBound, - current_height, + managed_wallet.synced_height(), |utxo| { // Look up the derivation path for this UTXO's address let path = address_to_path.get(&utxo.address)?; @@ -387,6 +410,8 @@ pub unsafe extern "C" fn wallet_build_and_sign_transaction( } }; + *fee_out = tx_builder_with_inputs.calculate_fee(); + // Build and sign the transaction let transaction = match tx_builder_with_inputs.build() { Ok(tx) => tx, 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);