use crate::blockstream::{BTransaction, Blockstream, FeeEstimates, Input, InputType, Output, Utxo};
use crate::BitcoinAddress;
use crate::BitcoinAmount;
use crate::Error;
use async_trait::async_trait;
use bitcoin::blockdata::script;
use std::cmp::Reverse;
use walletd_bip39::Seed;
use walletd_coin_core::CryptoAddress;
use walletd_coin_core::CryptoWalletBuilder;
use walletd_coin_core::{CryptoAmount, CryptoWallet};
use walletd_hd_key::slip44;
use walletd_hd_key::{HDKey, HDNetworkType, HDPath, HDPathBuilder, HDPathIndex, HDPurpose};
use bitcoin::script::PushBytes;
use ::secp256k1::{Message, Secp256k1, SecretKey};
pub use bitcoin::{
sighash::EcdsaSighashType, Address, AddressType, Network, PrivateKey as BitcoinPrivateKey,
PublicKey as BitcoinPublicKey, Script,
};
const DEFAULT_GAP_LIMIT: usize = 20;
#[derive(Debug, Clone)]
pub struct BitcoinWallet {
address_format: AddressType,
associated: Vec<AssociatedAddress>,
blockchain_client: Option<Blockstream>,
master_hd_key: Option<HDKey>,
gap_limit: usize,
account_discovery: bool,
hd_path_builder: Option<HDPathBuilder>,
}
impl Default for BitcoinWallet {
fn default() -> Self {
Self {
associated: Vec::new(),
blockchain_client: None,
address_format: AddressType::P2wpkh,
master_hd_key: None,
gap_limit: DEFAULT_GAP_LIMIT,
account_discovery: true,
hd_path_builder: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AssociatedAddress {
pub address: BitcoinAddress,
pub hd_key: HDKey,
}
impl AssociatedAddress {
pub fn new(address: BitcoinAddress, hd_key: HDKey) -> Self {
Self { address, hd_key }
}
pub fn address(&self) -> &BitcoinAddress {
&self.address
}
pub fn hd_key(&self) -> &HDKey {
&self.hd_key
}
}
#[async_trait]
impl CryptoWallet for BitcoinWallet {
type ErrorType = Error;
type BlockchainClient = Blockstream;
type CryptoAmount = BitcoinAmount;
type NetworkType = Network;
type WalletBuilder = BitcoinWalletBuilder;
type AddressFormat = AddressType;
async fn balance(&self) -> Result<BitcoinAmount, Error> {
let client = self.blockchain_client()?;
let mut total_balance = BitcoinAmount::new();
for addr in self.addresses() {
let balance = addr.balance(client).await?;
total_balance = (total_balance + balance)?;
}
Ok(total_balance)
}
fn builder() -> Self::WalletBuilder {
BitcoinWalletBuilder::new()
}
async fn transfer(
&self,
send_amount: &BitcoinAmount,
to_public_address: &str,
) -> Result<String, Error> {
let client = self.blockchain_client()?;
let receiver_view_wallet =
BitcoinAddress::from_public_address(to_public_address, self.network()?)?;
let fee_estimates: FeeEstimates = client.fee_estimates().await?;
let confirmation_target: u32 = 6;
let fee_map = &fee_estimates.0;
let fee_sat_per_byte = if !fee_map.is_empty() {
fee_map
.get(confirmation_target.to_string().as_str())
.expect("fee_map missing key")
.as_f64()
.expect("Unable to convert to f64")
} else {
return Err(Error::MissingFeeMap);
};
let mut available_utxos = Vec::new();
for addr in self.addresses() {
let utxos = client.utxo(&addr.public_address()).await?;
available_utxos.push(utxos);
}
let mut total_value_from_utxos = 0;
let mut inputs_available: Vec<Utxo> = Vec::new();
let mut inputs_available_tx_info: Vec<BTransaction> = Vec::new();
let change_addr = self.next_change_address()?.address_info().clone();
let mut keys_per_input: Vec<(BitcoinPrivateKey, BitcoinPublicKey)> = Vec::new();
let mut utxo_addr_index = Vec::new();
for (i, utxos_i) in available_utxos.iter().enumerate() {
for utxo in utxos_i.iter() {
if utxo.status.confirmed {
total_value_from_utxos += &utxo.value;
let tx_info = client.transaction(utxo.txid.as_str()).await?;
inputs_available.push(utxo.clone());
inputs_available_tx_info.push(tx_info);
utxo_addr_index.push(i);
}
}
}
let available_input_max = BitcoinAmount {
satoshi: total_value_from_utxos,
};
if available_input_max < *send_amount {
return Err(Error::InsufficientFunds("Insufficient funds".into()));
}
let prepared = Self::prepare_transaction(
fee_sat_per_byte,
&inputs_available,
&inputs_available_tx_info,
send_amount,
&receiver_view_wallet,
change_addr,
)?;
let transaction = prepared.0;
let chosen_indices = prepared.1;
for ind in chosen_indices {
let index = utxo_addr_index[ind];
let private_key = self.associated[index].address().private_key()?;
let public_key = self.associated[index].address().public_key()?;
let key_pair = (private_key, public_key);
keys_per_input.push(key_pair);
}
let signed_tx = Self::sign_tx(&transaction, keys_per_input)?;
let transaction_hex = BTransaction::serialize(&signed_tx)?;
let raw_transaction_hex: &'static str = Box::leak(transaction_hex.into_boxed_str());
let tx_id = client.post_a_transaction(raw_transaction_hex).await?;
Ok(tx_id)
}
fn set_blockchain_client(&mut self, client: Self::BlockchainClient) {
self.blockchain_client = Some(client);
}
async fn sync(&mut self) -> Result<(), Error> {
self.add_previously_used_addresses().await?;
Ok(())
}
fn receive_address(&self) -> Result<String, Error> {
let next_receive_address = self.next_address()?;
Ok(next_receive_address.public_address())
}
fn blockchain_client(&self) -> Result<&Blockstream, Error> {
match &self.blockchain_client {
Some(client) => Ok(client),
None => Err(Error::MissingBlockchainClient),
}
}
}
impl BitcoinWallet {
pub fn add(&mut self, associated: &AssociatedAddress) {
if self.addresses().contains(&associated.address) {
return;
}
self.associated.push(associated.clone());
}
pub fn associated_info(&self) -> &[AssociatedAddress] {
&self.associated
}
pub fn addresses(&self) -> Vec<BitcoinAddress> {
self.associated.iter().map(|x| x.address.clone()).collect()
}
pub fn coin_type_id(&self) -> Result<u32, Error> {
match self.network()? {
Network::Bitcoin => Ok(slip44::Coin::Bitcoin.id()),
Network::Testnet | Network::Regtest => Ok(slip44::Coin::Testnet.id()),
other => Err(Error::CurrentlyNotSupported(format!(
"Network {} currently not supported",
other
))),
}
}
pub fn default_hd_purpose(&self) -> Result<HDPurpose, Error> {
match self.address_format() {
AddressType::P2pkh => Ok(HDPurpose::BIP44),
AddressType::P2sh => Ok(HDPurpose::BIP49),
AddressType::P2wpkh => Ok(HDPurpose::BIP84),
other => Err(Error::CurrentlyNotSupported(format!(
"Address format {} currently not supported",
other
))),
}
}
pub async fn add_previously_used_addresses(&mut self) -> Result<(), Error> {
let master_hd_key = self.master_hd_key()?;
let address_format = self.address_format();
let blockchain_client = self.blockchain_client()?.clone();
let gap_limit = self.gap_limit;
let mut path_builder = match self.hd_path_builder.clone() {
Some(deriv_type) => deriv_type,
None => {
let mut builder = HDPath::builder();
builder
.purpose_index(self.default_hd_purpose()?.to_shortform_num())
.coin_type_index(self.coin_type_id()?)
.account_index(0)
.address_index(0);
builder
}
};
let mut current_gap = 0;
let mut search_next_account = true;
let mut account_index = 0;
let mut address_index = 0;
while search_next_account {
search_next_account = false;
while current_gap < gap_limit {
for change_index in 0..2 {
let specify_deriv_path = &path_builder
.clone()
.change_index(change_index)
.build()
.to_string();
let derived = master_hd_key.derive(specify_deriv_path)?;
let address = BitcoinAddress::from_hd_key(&derived, address_format)?;
let exists = blockchain_client
.check_if_past_transactions_exist(&address.public_address())
.await?;
log::info!(
"For deriv path: {}, address: {}, previous transaction history: {}",
&specify_deriv_path,
address.public_address(),
exists
);
if exists {
search_next_account = true;
let associated = AssociatedAddress::new(address, derived);
self.add(&associated);
} else if change_index == 0 {
current_gap += 1;
}
}
address_index += 1;
path_builder.address_index(address_index);
}
if !self.account_discovery {
break;
}
account_index += 1;
path_builder.account_index(account_index);
address_index = 0;
current_gap = 0;
}
Ok(())
}
pub fn address_format(&self) -> AddressType {
self.address_format
}
pub fn master_hd_key(&self) -> Result<HDKey, Error> {
match &self.master_hd_key {
Some(key) => Ok(key.clone()),
None => Err(Error::MissingMasterHDKey),
}
}
pub fn network(&self) -> Result<Network, Error> {
match self.master_hd_key()?.network() {
HDNetworkType::MainNet => Ok(Network::Bitcoin),
HDNetworkType::TestNet => Ok(Network::Testnet),
}
}
pub fn add_address_index(&mut self, address_index: u32) -> Result<(), Error> {
let purpose = self.default_hd_purpose()?.to_shortform_num();
let coin_type = self.coin_type_id()?;
let account = HDPathIndex::IndexHardened(0);
let add_deriv_path = match self.hd_path_builder() {
Ok(mut hd_path_builder) => hd_path_builder
.account_index(0)
.address_index(address_index)
.build(),
Err(_) => HDPath::builder()
.purpose_index(purpose)
.coin_type_index(coin_type)
.account_index(account.to_shortform_num())
.hardened_account()
.address_index(address_index)
.build(),
};
let _check_purpose = add_deriv_path.purpose()?;
let _check_coin_type = add_deriv_path.coin_type()?;
let add_hd_key = self.master_hd_key()?.derive(&add_deriv_path.to_string())?;
let btc_address = BitcoinAddress::from_hd_key(&add_hd_key, self.address_format)?;
self.add(&AssociatedAddress::new(btc_address, add_hd_key));
Ok(())
}
pub fn next_address(&self) -> Result<BitcoinAddress, Error> {
let purpose = self.default_hd_purpose()?.to_shortform_num();
let coin_type = self.coin_type_id()?;
let account = HDPathIndex::IndexHardened(0);
let mut max_address = 0;
let mut path_builder = HDPath::builder();
match self.hd_path_builder() {
Ok(mut hd_path_builder) => {
hd_path_builder.account_index(0);
path_builder = hd_path_builder;
}
Err(_) => {
path_builder
.purpose_index(purpose)
.coin_type_index(coin_type)
.account_index(account.to_shortform_num())
.hardened_account();
}
};
for info in self.associated.iter() {
let deriv_path = &info.hd_key().derivation_path();
let account = deriv_path.account()?.to_shortform_num();
let address_index = deriv_path.address()?.to_shortform_num();
if account == 0 && address_index > max_address {
max_address = address_index;
}
}
let next_deriv_path = path_builder
.address_index(max_address + 1)
.build()
.to_string();
let next_hd_key = self.master_hd_key()?.derive(&next_deriv_path)?;
BitcoinAddress::from_hd_key(&next_hd_key, self.address_format)
}
pub fn next_change_address(&self) -> Result<BitcoinAddress, Error> {
let purpose = match &self.hd_path_builder {
Some(builder) => match builder.purpose {
Some(purpose) => purpose,
None => self.default_hd_purpose()?.to_shortform_num(),
},
None => self.default_hd_purpose()?.to_shortform_num(),
};
let coin_type = self.coin_type_id()?;
let account = HDPathIndex::IndexHardened(0);
let mut max_address = 0;
let mut path_builder = match self.hd_path_builder.clone() {
Some(builder) => builder,
None => {
let mut builder = HDPath::builder();
builder
.purpose_index(purpose)
.coin_type_index(coin_type)
.account_index(account.to_shortform_num())
.hardened_account();
builder
}
};
path_builder.change_index(1);
for info in self.associated.iter() {
let deriv_path = &info.hd_key().derivation_path();
let change_index_derived = deriv_path.change()?.to_shortform_num();
let address_index_derived = deriv_path.address()?.to_shortform_num();
if (change_index_derived == 1) & (address_index_derived > max_address) {
max_address = address_index_derived;
}
}
let next_deriv_path = path_builder
.address_index(max_address + 1)
.build()
.to_string();
let next_hd_key = self.master_hd_key()?.derive(&next_deriv_path)?;
BitcoinAddress::from_hd_key(&next_hd_key, self.address_format)
}
pub fn set_master_hd_key(&mut self, master_hd_key: HDKey) {
self.master_hd_key = Some(master_hd_key);
}
pub fn set_gap_limit(&mut self, gap_limit: usize) {
self.gap_limit = gap_limit;
}
pub fn set_account_discovery(&mut self, account_discovery: bool) {
self.account_discovery = account_discovery;
}
pub fn set_hd_path_builder(&mut self, hd_path_builder: HDPathBuilder) {
self.hd_path_builder = Some(hd_path_builder);
}
pub fn gap_limit(&self) -> usize {
self.gap_limit
}
pub fn account_discovery(&self) -> bool {
self.account_discovery
}
pub fn hd_path_builder(&self) -> Result<HDPathBuilder, Error> {
match &self.hd_path_builder {
Some(builder) => Ok(builder.clone()),
None => {
let mut builder = HDPath::builder();
builder
.purpose_index(self.default_hd_purpose()?.to_shortform_num())
.coin_type_index(self.coin_type_id()?);
Ok(builder)
}
}
}
pub fn signature_sighashall_for_transaction_hash(
transaction_hash: &str,
private_key: &BitcoinPrivateKey,
) -> Result<String, Error> {
let sighash_type = EcdsaSighashType::All;
let secp = Secp256k1::new();
let message = Message::from_slice(&hex::decode(transaction_hash)?).expect("32 bytes");
let mut sig = secp.sign_ecdsa(&message, &SecretKey::from_slice(&private_key.to_bytes())?);
sig.normalize_s();
let mut sig_with_hashtype = sig.serialize_der().to_vec();
sig_with_hashtype.push(sighash_type.to_u32().try_into()?);
let content_len_index = 1;
let mut len_content = sig_with_hashtype[content_len_index];
let r_len_index = 3;
let mut len_r = sig_with_hashtype[r_len_index];
let r_first_byte = sig_with_hashtype[r_len_index + 1];
if r_first_byte == 0 {
let r_second_byte = sig_with_hashtype[r_len_index + 2];
if r_second_byte < 0x80 {
len_r -= 1;
len_content -= 1;
sig_with_hashtype.remove(r_len_index + 1); sig_with_hashtype[content_len_index] = len_content;
sig_with_hashtype[r_len_index] = len_r;
}
}
let s_len_index: usize = (3 + len_r + 1 + 1).into();
let mut len_s = sig_with_hashtype[s_len_index];
let s_first_byte = sig_with_hashtype[s_len_index + 1];
if s_first_byte == 0 {
let s_second_byte = sig_with_hashtype[s_len_index + 2];
if s_second_byte < 0x80 {
len_s -= 1;
len_content -= 1;
sig_with_hashtype.remove(s_len_index + 1);
sig_with_hashtype[content_len_index] = len_content;
sig_with_hashtype[s_len_index] = len_s;
}
}
let signature = hex::encode(&sig_with_hashtype);
Ok(signature)
}
pub fn sign_tx(
tx: &BTransaction,
keys_per_input: Vec<(BitcoinPrivateKey, BitcoinPublicKey)>,
) -> Result<BTransaction, Error> {
let mut inputs = tx.vin.clone();
for (i, input) in inputs.iter_mut().enumerate() {
let sighash_type = EcdsaSighashType::All;
let transaction_hash_for_input_with_sighash =
tx.transaction_hash_for_signing_segwit_input_index(i, sighash_type.to_u32())?;
let private_key = &keys_per_input[i].0;
let public_key = &keys_per_input[i].1;
let sig_with_hashtype = BitcoinWallet::signature_sighashall_for_transaction_hash(
&transaction_hash_for_input_with_sighash,
private_key,
)?;
let sig_with_hashtype_vec = hex::decode(&sig_with_hashtype)?;
let sig_with_hashtype_bytes: &PushBytes =
sig_with_hashtype_vec.as_slice().try_into()?;
let prevout_lockingscript_type = &input.prevout.scriptpubkey_type;
match prevout_lockingscript_type.as_str() {
"p2pkh" => {
let script_sig = script::Builder::new()
.push_slice(sig_with_hashtype_bytes)
.push_key(public_key)
.into_script();
input.scriptsig_asm = script_sig.to_asm_string();
input.scriptsig = hex::encode(script_sig.as_bytes());
}
"p2sh" => {
return Err(Error::CurrentlyNotSupported(
"Not currently handling P2SH".into(),
));
}
"v0_p2wsh" => {
return Err(Error::CurrentlyNotSupported(
"Not currently handling v0_p2wsh".into(),
));
}
"v0_p2wpkh" => {
input.witness = vec![sig_with_hashtype, hex::encode(public_key.to_bytes())];
}
_ => {
return Err(Error::CurrentlyNotSupported(
"Unidentified locking script type from previous output".into(),
))
}
}
}
let mut signed_tx = tx.clone();
signed_tx.vin = inputs;
Ok(signed_tx)
}
pub fn choose_inputs_and_set_fee(
utxo_available: &Vec<Utxo>,
send_amount: &BitcoinAmount,
inputs_available_tx_info: &[BTransaction],
byte_fee: f64,
) -> Result<(Vec<Input>, BitcoinAmount, Vec<usize>), Error> {
let mut indices = (0..utxo_available.len()).collect::<Vec<_>>();
indices.sort_by_key(|&i| Reverse(&utxo_available[i].value));
let mut chosen_indices = Vec::new();
let mut inputs: Vec<Input> = Vec::new();
let min_goal_target = (*send_amount * 1.5)?;
let mut obtained_amount = BitcoinAmount { satoshi: 0 };
let mut met_goal = false;
let mut segwit_transaction = false;
for ind in &indices {
let utxo = &utxo_available[*ind];
let utxo_prevout = &inputs_available_tx_info[*ind].vout[utxo.vout as usize];
if !segwit_transaction && InputType::new(utxo_prevout)?.is_segwit() {
segwit_transaction = true;
}
let value = BitcoinAmount {
satoshi: utxo.value,
};
obtained_amount = (obtained_amount + value)?;
let mut input = Input {
..Default::default()
};
let input_tx_info = &inputs_available_tx_info[*ind];
let input_utxo = &utxo_available[*ind];
input.txid = input_tx_info.txid.to_owned();
input.vout = input_utxo.vout;
input.prevout = utxo_prevout.to_owned();
for command in input
.prevout
.scriptpubkey_asm
.split_whitespace()
.collect::<Vec<_>>()
.iter()
{
let mut chars = command.chars();
let first_char = chars.next();
let second_char = chars.next();
if let Some(first) = first_char {
if let Some(second) = second_char {
if first != 'O' && second != 'P' {
input.prevout.pubkeyhash = command.to_string();
}
}
}
}
inputs.push(input);
chosen_indices.push(*ind);
if obtained_amount > min_goal_target {
met_goal = true;
break;
}
}
if met_goal {
let change_and_fee_amount = (obtained_amount - *send_amount)?;
let num_inputs = inputs.len();
let num_outputs = 2; let set_fee = BitcoinAmount {
satoshi: Self::estimate_fee_with_default_sizes(
segwit_transaction,
num_inputs,
num_outputs,
byte_fee,
)?,
};
let change_amount = (change_and_fee_amount - set_fee)?;
let min_change_amount = BitcoinAmount {
satoshi: Self::estimate_fee_with_default_sizes(segwit_transaction, 1, 0, byte_fee)?,
};
if change_amount > min_change_amount {
Ok((inputs, set_fee, chosen_indices))
}
else {
if inputs.len() < utxo_available.len() {
let wanted_extra = (min_change_amount - change_amount)?;
let min_goal_target = (obtained_amount + wanted_extra)?;
let start = inputs.len();
for ind in &indices[start..] {
let utxo = &utxo_available[*ind];
let utxo_prevout = &inputs_available_tx_info[*ind].vout[utxo.vout as usize];
if !segwit_transaction && InputType::new(utxo_prevout)?.is_segwit() {
segwit_transaction = true;
}
let value = BitcoinAmount {
satoshi: utxo.value,
};
obtained_amount = (obtained_amount + value)?;
let mut input = Input {
..Default::default()
};
let input_tx_info = &inputs_available_tx_info[*ind];
let input_utxo = &utxo_available[*ind];
input.txid = input_tx_info.txid.clone();
input.vout = input_utxo.vout;
input.prevout = utxo_prevout.to_owned();
for command in input
.prevout
.scriptpubkey_asm
.split_whitespace()
.collect::<Vec<_>>()
.iter()
{
let mut chars = command.chars();
let first_char = chars.next();
let second_char = chars.next();
if let Some(first) = first_char {
if let Some(second) = second_char {
if first != 'O' && second != 'P' {
input.prevout.pubkeyhash = command.to_string();
}
}
}
}
inputs.push(input);
chosen_indices.push(*ind);
if obtained_amount > min_goal_target {
return Ok((inputs, set_fee, chosen_indices));
}
}
return Ok((inputs, set_fee, chosen_indices));
}
Ok((inputs, set_fee, chosen_indices))
}
} else {
let num_inputs = inputs.len();
let num_outputs = 2; let set_fee = BitcoinAmount {
satoshi: Self::estimate_fee_with_default_sizes(
segwit_transaction,
num_inputs,
num_outputs,
byte_fee,
)?,
};
if obtained_amount > (*send_amount + set_fee)? {
Ok((inputs, set_fee, chosen_indices))
} else {
Err(Error::InsufficientFunds(
"Not enough funds to cover the send amount as well as the fee needed".into(),
))
}
}
}
pub fn estimate_fee_with_default_sizes(
is_segwit: bool,
num_inputs: usize,
num_outputs: usize,
byte_fee: f64,
) -> Result<u64, Error> {
const NONSEGWIT_DEFAULT_BYTES_PER_INPUT: usize = 148;
const NONSEGWIT_DEFAULT_BYTES_PER_OUTPUT: usize = 34;
const NONSEGWIT_DEFAULT_BYTES_BASE: usize = 10;
const SEGWIT_DEFAULT_BYTES_PER_INPUT: usize = 102;
const SEGWIT_DEFAULT_BYTES_PER_OUTPUT: usize = 31;
const SEGWIT_DEFAULT_BYTES_BASE: usize = 10;
if is_segwit {
let tx_size = (num_inputs * NONSEGWIT_DEFAULT_BYTES_PER_INPUT)
+ (num_outputs * NONSEGWIT_DEFAULT_BYTES_PER_OUTPUT)
+ NONSEGWIT_DEFAULT_BYTES_BASE;
let estimated_fee = f64::ceil(byte_fee * (tx_size as f64)) as u64;
Ok(estimated_fee)
} else {
let tx_size = (num_inputs * SEGWIT_DEFAULT_BYTES_PER_INPUT)
+ (num_outputs * SEGWIT_DEFAULT_BYTES_PER_OUTPUT)
+ SEGWIT_DEFAULT_BYTES_BASE;
let estimated_fee = f64::ceil(byte_fee * (tx_size as f64)) as u64;
Ok(estimated_fee)
}
}
pub fn prepare_transaction(
fee_sat_per_byte: f64,
utxo_available: &Vec<Utxo>,
inputs_available_tx_info: &[BTransaction],
send_amount: &BitcoinAmount,
receiver_view_wallet: &BitcoinAddress,
change_addr: Address,
) -> Result<(BTransaction, Vec<usize>), Error> {
let (inputs, fee_amount, chosen_indices) = Self::choose_inputs_and_set_fee(
utxo_available,
send_amount,
inputs_available_tx_info,
fee_sat_per_byte,
)?;
let inputs_amount = BitcoinAmount {
satoshi: inputs.iter().map(|x| x.prevout.value).sum(),
};
if inputs_amount < (*send_amount + fee_amount)? {
return Err(Error::InsufficientFunds(
"Insufficient funds to send amount and cover fees".into(),
));
}
let change_amount = ((inputs_amount - *send_amount)? - fee_amount)?;
let mut outputs: Vec<Output> = Vec::new();
let mut output_send = Output {
..Default::default()
};
output_send.value = send_amount.satoshi();
output_send.set_scriptpubkey_info(receiver_view_wallet.address_info().clone())?;
outputs.push(output_send);
let mut output_change = Output {
..Default::default()
};
output_change.value = change_amount.satoshi();
output_change.set_scriptpubkey_info(change_addr)?;
outputs.push(output_change);
let mut transaction = BTransaction {
..Default::default()
};
transaction.version = 1;
transaction.locktime = 0;
transaction.vin = inputs;
transaction.vout = outputs.clone();
transaction.fee = fee_amount.satoshi();
Ok((transaction, chosen_indices))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BitcoinWalletBuilder {
address_format: AddressType,
hd_purpose: Option<HDPurpose>,
master_hd_key: Option<HDKey>,
gap_limit_specified: Option<usize>,
account_discovery: bool,
mnemonic_seed: Option<Seed>,
network_type: Network,
hd_path_builder: HDPathBuilder,
}
impl Default for BitcoinWalletBuilder {
fn default() -> Self {
let default_hd_purpose = HDPurpose::BIP84;
let mut deriv_path_builder = HDPath::builder();
deriv_path_builder
.purpose_index(default_hd_purpose.to_shortform_num())
.hardened_purpose()
.coin_type_index(slip44::Coin::Bitcoin.id())
.hardened_coin_type()
.hardened_account()
.non_hardened_change()
.non_hardened_address();
Self {
address_format: AddressType::P2wpkh,
hd_purpose: Some(HDPurpose::BIP84),
master_hd_key: None,
gap_limit_specified: Some(DEFAULT_GAP_LIMIT),
account_discovery: true,
mnemonic_seed: None,
network_type: Network::Bitcoin,
hd_path_builder: deriv_path_builder,
}
}
}
impl CryptoWalletBuilder<BitcoinWallet> for BitcoinWalletBuilder {
fn new() -> Self {
Self::default()
}
fn master_hd_key(&mut self, master_hd_key: HDKey) -> &mut Self {
self.master_hd_key = Some(master_hd_key);
self
}
fn mnemonic_seed(&mut self, mnemonic_seed: Seed) -> &mut Self {
self.mnemonic_seed = Some(mnemonic_seed);
self
}
fn address_format(
&mut self,
address_format: <BitcoinWallet as CryptoWallet>::AddressFormat,
) -> &mut Self {
self.address_format = address_format;
self
}
fn network_type(&mut self, network_type: Network) -> &mut Self {
self.network_type = network_type;
self
}
fn hd_path_builder(&mut self, hd_path_builder: HDPathBuilder) -> &mut Self {
self.hd_path_builder = hd_path_builder;
self
}
fn build(&self) -> Result<BitcoinWallet, Error> {
let master_hd_key = match (&self.master_hd_key, &self.mnemonic_seed) {
(None, None) => {
return Err(Error::UnableToImportWallet(
"Neither the master HD key nor the mnemonic seed was provided".to_string(),
))
}
(Some(key), _) => key.clone(),
(None, Some(seed)) => {
let hd_network_type = match self.network_type {
Network::Bitcoin => HDNetworkType::MainNet,
_ => HDNetworkType::TestNet,
};
HDKey::new_master(seed.clone(), hd_network_type)?
}
};
let hd_purpose = match self.hd_purpose {
None => self.default_hd_purpose()?,
Some(purpose) => purpose,
};
let coin_type_id = self.coin_type_id()?;
let mut hd_path_builder = HDPath::builder();
hd_path_builder
.purpose_index(hd_purpose.to_shortform_num())
.hardened_purpose()
.coin_type_index(coin_type_id)
.hardened_coin_type();
let wallet = BitcoinWallet {
address_format: self.address_format,
associated: Vec::new(),
blockchain_client: None,
master_hd_key: Some(master_hd_key),
account_discovery: self.account_discovery,
gap_limit: self.gap_limit_specified.unwrap_or(DEFAULT_GAP_LIMIT),
hd_path_builder: Some(hd_path_builder),
};
Ok(wallet)
}
}
impl BitcoinWalletBuilder {
pub fn gap_limit(&mut self, gap_limit: usize) -> &mut Self {
self.gap_limit_specified = Some(gap_limit);
self
}
pub fn account_discovery(&mut self) -> &mut Self {
self.account_discovery = true;
self
}
pub fn no_account_discovery(&mut self) -> &mut Self {
self.account_discovery = false;
self
}
pub fn default_hd_purpose(&self) -> Result<HDPurpose, Error> {
match self.address_format {
AddressType::P2pkh => Ok(HDPurpose::BIP44),
AddressType::P2sh => Ok(HDPurpose::BIP49),
AddressType::P2wpkh => Ok(HDPurpose::BIP84),
other => Err(Error::CurrentlyNotSupported(format!(
"Address format {} currently not supported",
other
))),
}
}
pub fn coin_type_id(&self) -> Result<u32, Error> {
match &self.master_hd_key {
Some(key) => match key.network() {
HDNetworkType::MainNet => Ok(slip44::Coin::Bitcoin.id()),
HDNetworkType::TestNet => Ok(slip44::Coin::Testnet.id()),
},
None => Err(Error::MissingMasterHDKey),
}
}
}
#[cfg(test)]
mod test_bitcoin_wallet;
#[cfg(test)]
mod test_bitcoin_wallet_builder;