Skip to content
Open
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
150 changes: 29 additions & 121 deletions key-wallet-ffi/src/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
//! Transaction building and management

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::slice;

use crate::error::{FFIError, FFIErrorCode};
use crate::types::{
transaction_context_from_ffi, FFIBlockInfo, FFITransactionContextType, FFIWallet,
Expand All @@ -20,10 +15,12 @@ use key_wallet::wallet::managed_wallet_info::asset_lock_builder::{
AssetLockFundingType, CreditOutputFunding,
};
use key_wallet::wallet::managed_wallet_info::fee::FeeRate;
use key_wallet::wallet::managed_wallet_info::transaction_building::AccountTypePreference;
use key_wallet::wallet::managed_wallet_info::wallet_info_interface::WalletInfoInterface;
use secp256k1::{Message, Secp256k1, SecretKey};

use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::slice;
use std::str::FromStr;
// MARK: - Transaction Types

/// Opaque handle for a transaction
Expand Down Expand Up @@ -110,128 +107,39 @@ 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::transaction_builder::TransactionBuilder;
let network_rust = wallet_ref.inner().network;
let outputs_slice = slice::from_raw_parts(outputs, outputs_count);
let ffi_outputs = slice::from_raw_parts(outputs, outputs_count);
let mut outputs = Vec::with_capacity(outputs_count);

for output in ffi_outputs {
if output.address.is_null() {
(*error).set(FFIErrorCode::InvalidInput, "Output address pointer is null");
return false;
}

// Convert address from C string
let address_str = unwrap_or_return!(CStr::from_ptr(output.address).to_str(), error);

// Parse address using dashcore
let address = unwrap_or_return!(dashcore::Address::from_str(address_str), error);

outputs.push((address, output.amount));
}

let wallet_id = wallet_ref.inner().wallet_id;

unsafe {
manager_ref.runtime.block_on(async {
let mut manager = manager_ref.manager.write().await;
let wallet_id = wallet_ref.inner().wallet_id;

// Get change address through the manager
let result = unwrap_or_return!(
manager.get_change_address(
let transaction = unwrap_or_return!(
manager.build_and_sign_transaction(
&wallet_id,
account_index,
AccountTypePreference::BIP44,
true,
outputs,
FeeRate::new(fee_per_kb)
),
error
);
let change_address = unwrap_or_return!(result.address, error);

// Get the managed account for UTXOs and signing data
let managed_wallet = unwrap_or_return!(manager.get_wallet_info_mut(&wallet_id), error);

let managed_account = unwrap_or_return!(
managed_wallet.accounts.standard_bip44_accounts.get_mut(&account_index),
error
);

// Convert FFI outputs to Rust outputs
let mut tx_builder = TransactionBuilder::new();

for output in outputs_slice {
if output.address.is_null() {
(*error).set(FFIErrorCode::InvalidInput, "Output address pointer is null");
return false;
}

// Convert address from C string
let address_str = unwrap_or_return!(CStr::from_ptr(output.address).to_str(), error);

// Parse address using dashcore
use std::str::FromStr;
let parsed = unwrap_or_return!(dashcore::Address::from_str(address_str), error);
let address = unwrap_or_return!(parsed.require_network(network_rust), error);

// Add output
tx_builder =
unwrap_or_return!(tx_builder.add_output(&address, output.amount), error);
}

tx_builder = tx_builder
.set_change_address(change_address)
.set_fee_rate(FeeRate::new(fee_per_kb));

// Get available UTXOs (collect owned UTXOs, not references)
let utxos: Vec<key_wallet::Utxo> = 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,
_ => {
(*error).set(FFIErrorCode::WalletError, "Cannot sign with watch-only wallet");
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<dashcore::Address, key_wallet::DerivationPath> =
HashMap::new();

// Collect from all address pools (receive, change, etc.)
for pool in managed_account.managed_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 select_inputs_result = tx_builder.select_inputs(
&utxos,
SelectionStrategy::BranchAndBound,
managed_wallet.last_processed_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)
},
);
let mut tx_builder_with_inputs = unwrap_or_return!(select_inputs_result, error);
let transaction = unwrap_or_return!(tx_builder_with_inputs.build(), error);

// This is tricky, the transaction creation + fee calculation need a little
// bit of love to avoid this kind of logic.
//
// First, we need to know that TransactionBuilder may add an extra output for change
// to the final transaction but not to itself, with that knowledge, we can compare the
// number of outputs in the transaction with the number of outputs in the TransactionBuilder
// to then call the appropriate fee calculation method
*fee_out = if transaction.output.len() > tx_builder_with_inputs.outputs().len() {
tx_builder_with_inputs.calculate_fee_with_extra_output()
} else {
tx_builder_with_inputs.calculate_fee()
};

// Serialize the transaction
let serialized = consensus::serialize(&transaction);
Expand Down
7 changes: 7 additions & 0 deletions key-wallet-manager/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Error types for the wallet manager.

use crate::WalletId;
use key_wallet::wallet::managed_wallet_info::transaction_builder::BuilderError;

/// Wallet manager errors
#[derive(Debug)]
Expand Down Expand Up @@ -97,3 +98,9 @@ impl From<key_wallet::Error> for WalletError {
}
}
}

impl From<BuilderError> for WalletError {
fn from(err: BuilderError) -> Self {
WalletError::TransactionBuild(format!("Transaction building error: {}", err))
}
}
115 changes: 115 additions & 0 deletions key-wallet-manager/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ use key_wallet::{ExtendedPubKey, WalletCoreBalance};
use std::collections::{BTreeMap, BTreeSet};
use std::str::FromStr;

use dashcore::address::NetworkUnchecked;
use dashcore::secp256k1;
use key_wallet::wallet::managed_wallet_info::coin_selection::SelectionStrategy;
use key_wallet::wallet::managed_wallet_info::fee::FeeRate;
use key_wallet::wallet::managed_wallet_info::transaction_builder::TransactionBuilder;
use key_wallet::wallet::WalletType;
use tokio::sync::broadcast;

/// Default capacity for the wallet event bus.
Expand Down Expand Up @@ -888,6 +894,115 @@ impl<T: WalletInfoInterface + Send + Sync + 'static> WalletManager<T> {
}
}

impl WalletManager<ManagedWalletInfo> {
pub fn build_and_sign_transaction(
&mut self,
wallet_id: &WalletId,
account_index: u32,
outputs: Vec<(Address<NetworkUnchecked>, u64)>,
fee_rate: FeeRate,
) -> Result<(Transaction, u64), WalletError> {
// Get change address through the manager
let change_address = self
.get_change_address(wallet_id, account_index, AccountTypePreference::BIP44, true)?
.address
.ok_or_else(|| {
WalletError::TransactionBuild("Failed to get change address".to_string())
})?;

// Get the managed account for UTXOs and signing data
let (wallet, managed_wallet) = self
.get_wallet_and_info_mut(wallet_id)
.ok_or(WalletError::WalletNotFound(*wallet_id))?;

let managed_account = managed_wallet
.accounts
.standard_bip44_accounts
.get_mut(&account_index)
.ok_or(WalletError::AccountNotFound(account_index))?;

// Convert FFI outputs to Rust outputs
let mut tx_builder = TransactionBuilder::new();

for output in outputs {
let checked_address = output.0.require_network(wallet.network).map_err(|e| {
WalletError::InvalidParameter(format!("Output address network mismatch: {}", e))
})?;
tx_builder = tx_builder.add_output(&checked_address, output.1)?;
}

tx_builder = tx_builder.set_change_address(change_address).set_fee_rate(fee_rate);

// Get available UTXOs (collect owned UTXOs, not references)
let utxos: Vec<key_wallet::Utxo> = managed_account.utxos.values().cloned().collect();

// Get the wallet's root extended private key for signing
let root_xpriv = match &wallet.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,
_ => {
return Err(WalletError::TransactionBuild(
"Cannot sign with watch-only wallet".to_string(),
));
}
};
Comment on lines +939 to +955
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ios wallet is external signable, so this is incorrect.


// Build a map of address -> derivation path for all addresses in the account
use std::collections::HashMap;
let mut address_to_path: HashMap<dashcore::Address, key_wallet::DerivationPath> =
HashMap::new();

// Collect from all address pools (receive, change, etc.)
for pool in managed_account.managed_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 mut tx_builder = tx_builder.select_inputs(
&utxos,
SelectionStrategy::BranchAndBound,
managed_wallet.last_processed_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(wallet.network);
let secp = secp256k1::Secp256k1::new();
let derived_xpriv = root_ext_priv.derive_priv(&secp, path).ok()?;

Some(derived_xpriv.private_key)
},
)?;
Comment thread
ZocoLini marked this conversation as resolved.

let transaction = tx_builder.build()?;

// This is tricky, the transaction creation + fee calculation need a little
// bit of love to avoid this kind of logic.
//
// First, we need to know that TransactionBuilder may add an extra output for change
// to the final transaction but not to itself, with that knowledge, we can compare the
// number of outputs in the transaction with the number of outputs in the TransactionBuilder
// to then call the appropriate fee calculation method
let fee = if transaction.output.len() > tx_builder.outputs().len() {
tx_builder.calculate_fee_with_extra_output()
} else {
tx_builder.calculate_fee()
};

Ok((transaction, fee))
}
}

/// Helper function for getting current timestamp
fn current_timestamp() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs()
Expand Down
Loading