Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
27ee620
feat: Add `create_psbt{_with_aux_rand}` for Wallet
ValuedMammal Aug 12, 2025
c162237
docs: Improve documentation
ValuedMammal Sep 11, 2025
735a5fb
feat(psbt_params): Add `filter_utxos`
ValuedMammal Oct 1, 2025
a6153b3
refactor(tx_builder): Generalize `TxOrdering` inputs and outputs
ValuedMammal Oct 1, 2025
c9e5597
feat(psbt_params): Add method `ordering` for PsbtParams
ValuedMammal Oct 1, 2025
3143640
wallet: Update `create_psbt_from_selector` to call the TxOrdering
ValuedMammal Oct 1, 2025
88582b1
Fix imports
ValuedMammal Oct 7, 2025
353a49c
fix(wallet): Use `Output::with_descriptor` for self sending outputs
ValuedMammal Oct 9, 2025
8471247
wallet: Add missing `replace_by_fee` method which calls the thread-rng
ValuedMammal Oct 9, 2025
7925bc9
psbt: Add method `PsbtParams::add_planned_input`
ValuedMammal Oct 1, 2025
187d5b2
test(psbt): Add `test_selected_outpoints_are_unique`
ValuedMammal Oct 1, 2025
003e2f5
test: Add `test_add_planned_psbt_input`
ValuedMammal Oct 9, 2025
8dc6229
`UtxoFilter` has a Default implementation that applies no filtering
ValuedMammal Oct 9, 2025
9f0e256
docs: Add docsrs cfg attribute for methods gated by "std"
ValuedMammal Oct 21, 2025
5d99f60
psbt: Add `PsbtParams::maturity_height`
ValuedMammal Oct 22, 2025
f257aba
psbt: Add `PsbtParams::locktime`
ValuedMammal Oct 22, 2025
5ef1274
feat(psbt): Add `PsbtParams::manually_selected_only`
ValuedMammal Oct 22, 2025
c73001c
psbt: Return InsufficientFunds if InputCandidates is empty
ValuedMammal Oct 22, 2025
a1a8c5b
feat(psbt): Add `PsbtParams::only_witness_utxo`
ValuedMammal Oct 22, 2025
bf51f18
test: Add `test_create_psbt_csv`
ValuedMammal Oct 22, 2025
5285937
psbt: Add `PsbtParams::canonicalization_params`
ValuedMammal Oct 22, 2025
25e4b4e
feat(psbt): Add `PsbtParams::add_global_xpubs`
ValuedMammal Oct 24, 2025
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
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))
}
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