Skip to content

Exploring bdk-tx for Wallet type #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ members = [
"examples/example_wallet_esplora_blocking",
"examples/example_wallet_esplora_async",
"examples/example_wallet_rpc",
"examples/example_wallet_bdk_tx",
]

[workspace.package]
Expand Down
11 changes: 11 additions & 0 deletions examples/example_wallet_bdk_tx/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "example_wallet_bdk_tx"
version = "0.2.0"
edition = "2021"
publish = false

[dependencies]
bdk_wallet = { path = "../../wallet", features = ["file_store"] }
bdk_esplora = { version = "0.20", features = ["blocking"] }

anyhow = "1"
114 changes: 114 additions & 0 deletions examples/example_wallet_bdk_tx/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use std::{collections::BTreeSet, io::Write};

use bdk_esplora::{esplora_client, EsploraExt};
use bdk_wallet::bitcoin::FeeRate;
use bdk_wallet::{
bitcoin::{Amount, Network},
file_store::Store,
KeychainKind, SignOptions, TransactionParams, Wallet,
};

const DB_MAGIC: &str = "bdk_wallet_esplora_example";
const DB_PATH: &str = "bdk-example-bdk-tx.db";
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
const STOP_GAP: usize = 5;
const PARALLEL_REQUESTS: usize = 5;

const NETWORK: Network = Network::Signet;
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/1'/0/*)";
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/1'/1/*)";
const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";

fn main() -> Result<(), anyhow::Error> {
let mut db = Store::<bdk_wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), DB_PATH)?;

let wallet_opt = Wallet::load()
.descriptor(KeychainKind::External, Some(EXTERNAL_DESC))
.descriptor(KeychainKind::Internal, Some(INTERNAL_DESC))
.extract_keys()
.check_network(NETWORK)
.load_wallet(&mut db)?;
let mut wallet = match wallet_opt {
Some(wallet) => wallet,
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
.network(NETWORK)
.create_wallet(&mut db)?,
};

let address = wallet.next_unused_address(KeychainKind::External);
wallet.persist(&mut db)?;
println!(
"Next unused address: ({}) {}",
address.index, address.address
);

let balance = wallet.balance();
println!("Wallet balance before syncing: {}", balance.total());

print!("Syncing...");
let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking();

let request = wallet.start_full_scan().inspect({
let mut stdout = std::io::stdout();
let mut once = BTreeSet::<KeychainKind>::new();
move |keychain, spk_i, _| {
if once.insert(keychain) {
print!("\nScanning keychain [{:?}] ", keychain);
}
print!(" {:<3}", spk_i);
stdout.flush().expect("must flush")
}
});

let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?;

wallet.apply_update(update)?;
wallet.persist(&mut db)?;
println!();

let balance = wallet.balance();
println!("Wallet balance after syncing: {}", balance.total());

if balance.total() < SEND_AMOUNT {
println!(
"Please send at least {} to the receiving address",
SEND_AMOUNT
);
std::process::exit(0);
}

// ----------------------------
// TRANSACTION BUILDER WORKFLOW
// ----------------------------
// let mut tx_builder = wallet.build_tx();
// tx_builder
// .add_recipient(address.script_pubkey(), SEND_AMOUNT)
// .fee_rate(FeeRate::from_sat_per_vb(4).unwrap());
// let mut psbt = tx_builder.finish()?;

// ----------------------------
// USING BDK-TX THROUGH A NEW WALLET METHOD INSTEAD
// ----------------------------
// let transaction_params = TransactionParams {
// outputs: vec![(address.script_pubkey(), SEND_AMOUNT)],
// target_feerate: FeeRate::from_sat_per_vb(4).unwrap(),
// must_spend: Vec::new(),
// };
// let mut psbt = wallet.create_complex_transaction(transaction_params).unwrap();

let mut psbt = wallet
.create_transaction(
vec![(address.script_pubkey(), SEND_AMOUNT)],
FeeRate::from_sat_per_vb(4).unwrap(),
)
.unwrap();

let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized);

let tx = psbt.extract_tx()?;
client.broadcast(&tx)?;
println!("Tx broadcasted! Txid: {}", tx.compute_txid());

Ok(())
}
1 change: 1 addition & 0 deletions wallet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
bdk_chain = { version = "0.21.1", features = [ "miniscript", "serde" ], default-features = false }
bdk_file_store = { version = "0.18.1", optional = true }
bdk_tx = { git = "https://github.com/bitcoindevkit/bdk-tx.git", rev = "6e2414ed7e701c04d0af46ff477c2dbf9a9f75b2" }

# Optional dependencies
bip39 = { version = "2.0", optional = true }
Expand Down
135 changes: 133 additions & 2 deletions wallet/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ use alloc::{
sync::Arc,
vec::Vec,
};
use core::{cmp::Ordering, fmt, mem, ops::Deref};

use bdk_chain::{
indexed_tx_graph,
indexer::keychain_txout::KeychainTxOutIndex,
Expand All @@ -43,11 +41,13 @@ use bitcoin::{
transaction, Address, Amount, Block, BlockHash, FeeRate, Network, OutPoint, Psbt, ScriptBuf,
Sequence, Transaction, TxOut, Txid, Weight, Witness,
};
use core::{cmp::Ordering, fmt, mem, ops::Deref};
use miniscript::{
descriptor::KeyMap,
psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier},
};
use rand_core::RngCore;
use std::collections::BTreeSet;

mod changeset;
pub mod coin_selection;
Expand Down Expand Up @@ -77,7 +77,13 @@ use crate::wallet::{

// re-exports
pub use bdk_chain::Balance;
use bdk_tx::{
create_psbt, create_selection, CreatePsbtParams, CreateSelectionParams, InputCandidates,
InputGroup, Output,
};
use chain::KeychainIndexed;
pub use changeset::ChangeSet;
use miniscript::plan::Assets;
pub use params::*;
pub use persisted::*;
pub use utils::IsDust;
Expand Down Expand Up @@ -2425,6 +2431,131 @@ impl Wallet {
}
}

pub struct TransactionParams {
pub outputs: Vec<(ScriptBuf, Amount)>,
pub target_feerate: FeeRate,
pub must_spend: Vec<LocalOutput>,
// cannot_spend: Vec<LocalOutput>,
}

/// Methods that use the bdk_tx crate to build transactions
impl Wallet {
pub fn create_transaction(
&mut self,
outputs: Vec<(ScriptBuf, Amount)>,
target_feerate: FeeRate,
) -> Result<Psbt, CreateBdkTxError> {
let local_outputs: Vec<LocalOutput> = self.list_unspent().collect();
let outpoints: Vec<KeychainIndexed<KeychainKind, OutPoint>> = local_outputs
.into_iter()
.map(|o| ((o.keychain, o.derivation_index), o.outpoint.clone()))
.collect();
// let descriptors = self.keychains();
let descriptors: Vec<(KeychainKind, &ExtendedDescriptor)> = self.keychains().collect();

let mut descriptors_map = BTreeMap::new();
let _ = descriptors.into_iter().for_each(|(kind, desc)| {
descriptors_map.insert(kind, desc.clone());
});
dbg!(&descriptors_map);

let input_candidates: Vec<InputGroup> = InputCandidates::new(
&self.tx_graph(), // tx_graph
&self.local_chain(), // chain
self.local_chain().tip().block_id().clone(), // chain_tip
outpoints, // outpoints
descriptors_map, // descriptors
BTreeSet::default(), // allow_malleable
Assets::new(), // additional_assets
)
.unwrap()
.into_single_groups(|_| true);

let next_change_index: u32 = self.reveal_next_address(KeychainKind::Internal).index;
let public_change_descriptor = self.public_descriptor(KeychainKind::Internal);

let outputs_vector = outputs
.into_iter()
.map(|o| Output::with_script(o.0, o.1))
.collect::<Vec<_>>();

let (selection, metrics) = create_selection(CreateSelectionParams::new(
input_candidates,
public_change_descriptor
.at_derivation_index(next_change_index)
.map_err(|_| CreateBdkTxError::CannotCreateTx)?,
outputs_vector,
target_feerate,
))
.map_err(|_| CreateBdkTxError::CannotCreateTx)?;

let (psbt, _) = create_psbt(CreatePsbtParams::new(selection))
.map_err(|_| CreateBdkTxError::CannotCreateTx)?;

Ok(psbt)
}

pub fn create_complex_transaction(
&mut self,
transaction_params: TransactionParams,
) -> Result<Psbt, CreateBdkTxError> {
let local_outputs: Vec<LocalOutput> = self.list_unspent().collect();
let outpoints: Vec<KeychainIndexed<KeychainKind, OutPoint>> = local_outputs
.into_iter()
.map(|o| ((o.keychain, o.derivation_index), o.outpoint.clone()))
.collect();
// let descriptors = self.keychains();
let descriptors: Vec<(KeychainKind, &ExtendedDescriptor)> = self.keychains().collect();

let mut descriptors_map = BTreeMap::new();
let _ = descriptors.into_iter().for_each(|(kind, desc)| {
descriptors_map.insert(kind, desc.clone());
});
dbg!(&descriptors_map);

let input_candidates: Vec<InputGroup> = InputCandidates::new(
&self.tx_graph(), // tx_graph
&self.local_chain(), // chain
self.local_chain().tip().block_id().clone(), // chain_tip
outpoints, // outpoints
descriptors_map, // descriptors
BTreeSet::default(), // allow_malleable
Assets::new(), // additional_assets
)
.unwrap()
.into_single_groups(|_| true);

let next_change_index: u32 = self.reveal_next_address(KeychainKind::Internal).index;
let public_change_descriptor = self.public_descriptor(KeychainKind::Internal);

let outputs_vector = transaction_params
.outputs
.into_iter()
.map(|o| Output::with_script(o.0, o.1))
.collect::<Vec<_>>();

let (selection, metrics) = create_selection(CreateSelectionParams::new(
input_candidates,
public_change_descriptor
.at_derivation_index(next_change_index)
.map_err(|_| CreateBdkTxError::CannotCreateTx)?,
outputs_vector,
transaction_params.target_feerate,
))
.map_err(|_| CreateBdkTxError::CannotCreateTx)?;

let (psbt, _) = create_psbt(CreatePsbtParams::new(selection))
.map_err(|_| CreateBdkTxError::CannotCreateTx)?;

Ok(psbt)
}
}

#[derive(Debug)]
pub enum CreateBdkTxError {
CannotCreateTx,
}

impl AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationBlockTime>> for Wallet {
fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph<ConfirmationBlockTime> {
self.indexed_graph.graph()
Expand Down
Loading