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

[dependencies]
bdk_chain = { version = "0.23.1", features = ["miniscript", "serde"], default-features = false }
bdk_coin_select = { version = "0.4.1" }
bdk_tx = { version = "0.1.0" }
bitcoin = { version = "0.32.6", features = ["serde", "base64"], default-features = false }
miniscript = { version = "12.3.1", features = ["serde"], default-features = false }
rand_core = { version = "0.6.0" }
Expand Down
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)])
.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))
}
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