Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions key-wallet-ffi/FFI_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
20 changes: 15 additions & 5 deletions key-wallet-ffi/include/key_wallet_ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ typedef enum {
SPANISH = 9,
} FFILanguage;

typedef enum {
ECONOMY = 0,
NORMAL = 1,
PRIORITY = 2,
} FFIFeeRate;

/*
FFI-compatible transaction context
*/
Expand Down Expand Up @@ -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)
Expand Down
105 changes: 65 additions & 40 deletions key-wallet-ffi/src/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
//! 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;

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

Expand Down Expand Up @@ -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<FFIFeeRate> 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:
Expand All @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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<key_wallet::Utxo> = managed_account.utxos.values().cloned().collect();
Expand Down Expand Up @@ -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)?;
Expand All @@ -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,
Expand Down
12 changes: 8 additions & 4 deletions key-wallet/src/wallet/managed_wallet_info/transaction_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Transaction, BuilderError> {
if self.inputs.is_empty() {
Expand Down Expand Up @@ -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);

Expand Down
Loading