Skip to content
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
bdk_chain = { version = "0.23.1", features = ["miniscript", "serde"], default-features = false }
bdk_coin_select = { version = "0.4.1" }
bitcoin = { version = "0.32.7", features = ["serde", "base64"], default-features = false }
miniscript = { version = "12.3.1", features = ["serde"], default-features = false }
rand_core = { version = "0.6.0" }
Expand All @@ -30,6 +31,11 @@ bdk_file_store = { version = "0.21.1", optional = true }
bip39 = { version = "2.0", optional = true }
tempfile = { version = "3.20.0", optional = true }

[dependencies.bdk_tx]
version = "0.2.0"
git = "https://github.com/valuedmammal/bdk-tx"
branch = "release/0_2_0"

[features]
default = ["std"]
std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
Expand Down
112 changes: 112 additions & 0 deletions examples/cpfp.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use bdk_tx::Signer;
use bdk_wallet::{KeychainKind, Wallet};
use bitcoin::{
consensus::encode::deserialize_hex, secp256k1::Secp256k1, Amount, Network, OutPoint, ScriptBuf,
Transaction, TxOut,
};
use miniscript::Descriptor;
use std::sync::Arc;

const EXTERNAL: &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)";
const INTERNAL: &str = "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)";

const FEERATE: f32 = 10.0;

fn main() -> anyhow::Result<()> {
let secp = Secp256k1::new();
let (external_desc, mut keymap) = Descriptor::parse_descriptor(&secp, EXTERNAL)?;
let (internal_desc, internal_keymap) = Descriptor::parse_descriptor(&secp, INTERNAL)?;
keymap.extend(internal_keymap);

let mut wallet = Wallet::create(external_desc, internal_desc)
.network(Network::Regtest)
.create_wallet_no_persist()?;

// Track balances for sanity checking
let initial_balance = wallet.balance().total();
println!("Initial balance: {}", initial_balance);

let tx0: Transaction = deserialize_hex(
"02000000000101401087cb611c1173462be69d8abb501edaf0e89cf086d0c88e377043fc7f6bde0000000000fdffffff02db1285270100000022512049c3c5eac192a9ee551f1a3a45bbb47c68c7c01e8d007847a44cdca20080a55f80de80020000000022512005472086085253543288c12a67aa2975f1e8e698b1f026d625238ef84abbfe2b024730440220787949255eb0af8e9f69b6e4f112a3a157c02a4498b87f5dede45eafd46405390220435c5562e86d1a2ad3f752d90d1fb877d8a207b09b5688c5d3371c201c534f9e012102cb066247461fb43246467b94f72497be4f5fa863baeca191c431648559e7efd365000000",
)?;
let tx0 = Arc::new(tx0);
let outpoint = fund_wallet(&mut wallet, tx0.clone())?;

let funded_balance = wallet.balance().total();
println!("Balance after funding: {} sat", funded_balance);

let next_index = wallet.next_derivation_index(KeychainKind::External);
let definite_descriptor: Descriptor<miniscript::DefiniteDescriptorKey> = wallet
.public_descriptor(KeychainKind::External)
.at_derivation_index(next_index)?;

let target_feerate = bdk_coin_select::FeeRate::from_sat_per_vb(FEERATE);

let (mut psbt, finalizer) =
wallet.create_sweep(outpoint, definite_descriptor, target_feerate)?;

let _ = psbt.sign(&Signer(keymap), &secp).unwrap();
let res = finalizer.finalize(&mut psbt);
assert!(res.is_finalized());

let tx1 = psbt.extract_tx().expect("Must be finalized!");
assert_eq!(tx1.input.len(), 1, "Child should have 1 input");
assert_eq!(
tx1.input[0].previous_output, outpoint,
"Should spend from parent"
);

wallet.apply_unconfirmed_txs([(Arc::new(tx1.clone()), 110)]);
let tx1 = Arc::new(tx1);

compute_feerate(&wallet, &[tx0, tx1]);

let final_balance = wallet.balance().total();
println!("Final balance: {} sat", final_balance);

Ok(())
}

/// Compute the package feerate and print it to stdout.
fn compute_feerate(wallet: &Wallet, txs: &[Arc<Transaction>]) {
let mut acc_fee = 0;
let mut acc_vsize = 0;

for tx in txs {
let fee = wallet.calculate_fee(tx).unwrap().to_sat();
let vsize = tx.vsize() as u64;
let feerate = fee as f32 / vsize as f32;
println!("Fee {fee} Vsize {vsize} FeeRate {}", feerate);
acc_fee += fee;
acc_vsize += vsize;
}

println!("Target feerate {}", FEERATE);
println!("Package feerate {}", acc_fee as f32 / acc_vsize as f32);
}

fn fund_wallet(wallet: &mut Wallet, tx0: Arc<Transaction>) -> anyhow::Result<OutPoint> {
let txid0 = tx0.compute_txid();

// Previous output of tx0. This is needed for fee calculation.
let prevout = OutPoint::new(
"de6b7ffc4370378ec8d086f09ce8f0da1e50bb8a9de62b4673111c61cb871040".parse()?,
0,
);
let txout = TxOut {
script_pubkey: ScriptBuf::from_hex("0014ca5688311d4d0637f1c66bfd495eee02c5fe1755")?,
value: Amount::from_btc(50.0)?,
};
wallet.insert_txout(prevout, txout);
wallet.apply_unconfirmed_txs([(tx0.clone(), 100)]);

let outpoint = tx0
.output
.iter()
.enumerate()
.find(|(_vout, txo)| txo.value == Amount::from_btc(0.42).unwrap())
.map(|(vout, _)| OutPoint::new(txid0, vout as u32))
.unwrap();

Ok(outpoint)
}
117 changes: 117 additions & 0 deletions examples/psbt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#![allow(clippy::print_stdout)]

use std::collections::HashMap;
use std::str::FromStr;

use bdk_chain::BlockId;
use bdk_chain::ConfirmationBlockTime;
use bdk_wallet::psbt::{PsbtParams, SelectionStrategy::*};
use bdk_wallet::test_utils::*;
use bdk_wallet::{KeychainKind::External, Wallet};
use bitcoin::{
bip32, consensus,
secp256k1::{self, rand},
Address, Amount, TxIn, TxOut,
};
use rand::Rng;

// This example shows how to create a PSBT using BDK Wallet.

const NETWORK: bitcoin::Network = bitcoin::Network::Signet;
const SEND_TO: &str = "tb1pw3g5qvnkryghme7pyal228ekj6vq48zc5k983lqtlr2a96n4xw0q5ejknw";
const AMOUNT: Amount = Amount::from_sat(42_000);
const FEERATE: f64 = 2.0; // sat/vb

fn main() -> anyhow::Result<()> {
let (desc, change_desc) = get_test_wpkh_and_change_desc();
let secp = secp256k1::Secp256k1::new();

// Xpriv to be used for signing the PSBT
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L")?;

// Create wallet and fund it.
let mut wallet = Wallet::create(desc, change_desc)
.network(NETWORK)
.create_wallet_no_persist()?;

fund_wallet(&mut wallet)?;

let utxos = wallet
.list_unspent()
.map(|output| (output.outpoint, output))
.collect::<HashMap<_, _>>();

// Build params.
let mut params = PsbtParams::default();
let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?;
let feerate = feerate_unchecked(FEERATE);
params
.add_recipients([(addr, AMOUNT)])
.fee(bdk_tx::FeeStrategy::FeeRate(feerate))
.coin_selection(SingleRandomDraw);

// Create PSBT (which also returns the Finalizer).
let (mut psbt, finalizer) = wallet.create_psbt(params)?;

dbg!(&psbt);

let tx = &psbt.unsigned_tx;
for txin in &tx.input {
let op = txin.previous_output;
let output = utxos.get(&op).unwrap();
println!("TxIn: {}", output.txout.value);
}
for txout in &tx.output {
println!("TxOut: {}", txout.value);
}

let _ = psbt.sign(&xprv, &secp);
println!("Signed: {}", !psbt.inputs[0].partial_sigs.is_empty());
let finalize_res = finalizer.finalize(&mut psbt);
println!("Finalized: {}", finalize_res.is_finalized());

let tx = psbt.extract_tx()?;
let feerate = wallet.calculate_fee_rate(&tx)?;
println!("Fee rate: {} sat/vb", bdk_wallet::floating_rate!(feerate));

println!("{}", consensus::encode::serialize_hex(&tx));

Ok(())
}

fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> {
let anchor = ConfirmationBlockTime {
block_id: BlockId {
height: 260071,
hash: "000000099f67ae6469d1ad0525d756e24d4b02fbf27d65b3f413d5feb367ec48".parse()?,
},
confirmation_time: 1752184658,
};
insert_checkpoint(wallet, anchor.block_id);

let mut rng = rand::thread_rng();

// Fund wallet with several random utxos
for i in 0..21 {
let addr = wallet.reveal_next_address(External).address;
let value = 10_000 * (i + 1) + (100 * rng.gen_range(0..10));
let tx = bitcoin::Transaction {
lock_time: bitcoin::absolute::LockTime::ZERO,
version: bitcoin::transaction::Version::TWO,
input: vec![TxIn::default()],
output: vec![TxOut {
script_pubkey: addr.script_pubkey(),
value: Amount::from_sat(value),
}],
};
insert_tx_anchor(wallet, tx, anchor.block_id);
}

let tip = BlockId {
height: 260171,
hash: "0000000b9efb77450e753ae9fd7be9f69219511c27b6e95c28f4126f3e1591c3".parse()?,
};
insert_checkpoint(wallet, tip);

Ok(())
}
92 changes: 92 additions & 0 deletions examples/rbf.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#![allow(clippy::print_stdout)]

use std::str::FromStr;
use std::sync::Arc;

use bdk_chain::BlockId;
use bdk_wallet::test_utils::*;
use bdk_wallet::Wallet;
use bitcoin::{bip32, consensus, secp256k1, Address, FeeRate, Transaction};

// This example shows how to create a Replace-By-Fee (RBF) transaction using BDK Wallet.

const NETWORK: bitcoin::Network = bitcoin::Network::Regtest;
const SEND_TO: &str = "bcrt1q3yfqg2v9d605r45y5ddt5unz5n8v7jl5yk4a4f";

fn main() -> anyhow::Result<()> {
let desc = "wpkh(tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU/84h/1h/0h/0/*)";
let change_desc = "wpkh(tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU/84h/1h/0h/1/*)";
let secp = secp256k1::Secp256k1::new();

// Xpriv to be used for signing the PSBT
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU")?;

// Create wallet and "fund" it.
let mut wallet = Wallet::create(desc, change_desc)
.network(NETWORK)
.create_wallet_no_persist()?;

// `tx_1` is the unconfirmed wallet tx that we want to replace.
let tx_1 = fund_wallet(&mut wallet)?;
wallet.apply_unconfirmed_txs([(tx_1.clone(), 1234567000)]);

// We'll need to fill in the original recipient details.
let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?;
let txo = tx_1
.output
.iter()
.find(|txo| txo.script_pubkey == addr.script_pubkey())
.expect("failed to find orginal recipient")
.clone();

// Now build fee bump.
let (mut psbt, finalizer) = wallet.replace_by_fee_and_recipients(
&[Arc::clone(&tx_1)],
FeeRate::from_sat_per_vb_unchecked(5),
vec![(txo.script_pubkey, txo.value)],
)?;

let _ = psbt.sign(&xprv, &secp);
println!("Signed: {}", !psbt.inputs[0].partial_sigs.is_empty());
let finalize_res = finalizer.finalize(&mut psbt);
println!("Finalized: {}", finalize_res.is_finalized());

let tx = psbt.extract_tx()?;
let feerate = wallet.calculate_fee_rate(&tx)?;
println!("Fee rate: {} sat/vb", bdk_wallet::floating_rate!(feerate));

println!("{}", consensus::encode::serialize_hex(&tx));

wallet.apply_unconfirmed_txs([(tx.clone(), 1234567001)]);

let txid_2 = tx.compute_txid();

assert!(
wallet
.tx_graph()
.direct_conflicts(&tx_1)
.any(|(_, txid)| txid == txid_2),
"ERROR: RBF tx does not replace `tx_1`",
);

Ok(())
}

fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<Arc<Transaction>> {
// The parent of `tx`. This is needed to compute the original fee.
let tx0: Transaction = consensus::encode::deserialize_hex(
"020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0200f2052a010000001600144d34238b9c4c59b9e2781e2426a142a75b8901ab0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000",
)?;

let anchor_block = BlockId {
height: 101,
hash: "3bcc1c447c6b3886f43e416b5c21cf5c139dc4829a71dc78609bc8f6235611c5".parse()?,
};
insert_tx_anchor(wallet, tx0, anchor_block);

let tx: Transaction = consensus::encode::deserialize_hex(
"020000000001014cb96536e94ba3f840cb5c2c965c8f9a306209de63fcd02060219aaf14f1d7b30000000000fdffffff0280de80020000000016001489120429856e9f41d684a35aba7262a4cecf4bf4f312852701000000160014757a57b3009c0e9b2b9aa548434dc295e21aeb05024730440220400c0a767ce42e0ea02b72faabb7f3433e607b475111285e0975bba1e6fd2e13022059453d83cbacb6652ba075f59ca0437036f3f94cae1959c7c5c0f96a8954707a012102c0851c2d2bddc1dd0b05caeac307703ec0c4b96ecad5a85af47f6420e2ef6c661b000000",
)?;

Ok(Arc::new(tx))
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ pub use bdk_chain::rusqlite;
pub use bdk_chain::rusqlite_impl;
pub use descriptor::template;
pub use descriptor::HdKeyPaths;
pub use psbt::*;
pub use signer;
pub use signer::SignOptions;
pub use tx_builder::*;
Expand Down
4 changes: 4 additions & 0 deletions src/psbt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ use bitcoin::FeeRate;
use bitcoin::Psbt;
use bitcoin::TxOut;

mod params;

pub use params::*;

// TODO upstream the functions here to `rust-bitcoin`?

/// Trait to add functions to extract utxos and calculate fees.
Expand Down
Loading