Skip to content

Commit 0c55a8d

Browse files
committed
feat(example_cli): use bdk_tx::Builder to create PSBTs
1 parent 71bf53d commit 0c55a8d

File tree

2 files changed

+109
-104
lines changed

2 files changed

+109
-104
lines changed

example-crates/example_cli/Cargo.toml

+4
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,7 @@ clap = { version = "4.5.17", features = ["derive", "env"] }
1616
rand = "0.8"
1717
serde = { version = "1", features = ["derive"] }
1818
serde_json = "1.0"
19+
20+
[dependencies.bdk_tx]
21+
git = "https://github.com/bitcoindevkit/bdk-tx"
22+
branch = "master"

example-crates/example_cli/src/lib.rs

+105-104
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use serde_json::json;
21
use std::cmp;
3-
use std::collections::HashMap;
42
use std::env;
53
use std::fmt;
64
use std::str::FromStr;
@@ -10,11 +8,10 @@ use anyhow::bail;
108
use anyhow::Context;
119
use bdk_chain::bitcoin::{
1210
absolute, address::NetworkUnchecked, bip32, consensus, constants, hex::DisplayHex, relative,
13-
secp256k1::Secp256k1, transaction, Address, Amount, Network, NetworkKind, PrivateKey, Psbt,
14-
PublicKey, Sequence, Transaction, TxIn, TxOut,
11+
secp256k1::Secp256k1, Address, Amount, Network, NetworkKind, Psbt, Transaction, TxOut, Txid,
1512
};
1613
use bdk_chain::miniscript::{
17-
descriptor::{DescriptorSecretKey, SinglePubKey},
14+
descriptor::DefiniteDescriptorKey,
1815
plan::{Assets, Plan},
1916
psbt::PsbtExt,
2017
Descriptor, DescriptorPublicKey, ForEachKey,
@@ -27,12 +24,14 @@ use bdk_chain::{
2724
tx_graph, ChainOracle, DescriptorExt, FullTxOut, IndexedTxGraph, Merge,
2825
};
2926
use bdk_coin_select::{
30-
metrics::LowestFee, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target,
31-
TargetFee, TargetOutputs,
27+
metrics, Candidate, ChangePolicy, CoinSelector, DrainWeights, FeeRate, Target, TargetFee,
28+
TargetOutputs,
3229
};
3330
use bdk_file_store::Store;
31+
use bdk_tx::{self, DataProvider, Signer};
3432
use clap::{Parser, Subcommand};
3533
use rand::prelude::*;
34+
use serde_json::json;
3635

3736
pub use anyhow;
3837
pub use clap;
@@ -263,21 +262,19 @@ pub fn create_tx<O: ChainOracle>(
263262
graph: &mut KeychainTxGraph,
264263
chain: &O,
265264
assets: &Assets,
266-
cs_algorithm: CoinSelectionAlgo,
265+
coin_selection: CoinSelectionAlgo,
267266
address: Address,
268267
value: u64,
269268
feerate: f32,
270269
) -> anyhow::Result<(Psbt, Option<ChangeInfo>)>
271270
where
272271
O::Error: std::error::Error + Send + Sync + 'static,
273272
{
274-
let mut changeset = keychain_txout::ChangeSet::default();
275-
276273
// get planned utxos
277274
let mut plan_utxos = planned_utxos(graph, chain, assets)?;
278275

279-
// sort utxos if cs-algo requires it
280-
match cs_algorithm {
276+
// sort utxos if coin selection requires it
277+
match coin_selection {
281278
CoinSelectionAlgo::LargestFirst => {
282279
plan_utxos.sort_by_key(|(_, utxo)| cmp::Reverse(utxo.txout.value))
283280
}
@@ -301,120 +298,109 @@ where
301298
})
302299
.collect();
303300

304-
// create recipient output(s)
305-
let mut outputs = vec![TxOut {
306-
value: Amount::from_sat(value),
307-
script_pubkey: address.script_pubkey(),
308-
}];
301+
// if this is a segwit spend, don't bother setting the input `non_witness_utxo`
302+
let only_witness_utxo = candidates.iter().all(|c| c.is_segwit);
309303

310304
let (change_keychain, _) = graph
311305
.index
312306
.keychains()
313307
.last()
314308
.expect("must have a keychain");
315309

316-
let ((change_index, change_script), index_changeset) = graph
310+
// get drain script
311+
let ((drain_index, drain_spk), index_changeset) = graph
317312
.index
318313
.next_unused_spk(change_keychain)
319314
.expect("Must exist");
320-
changeset.merge(index_changeset);
321-
322-
let mut change_output = TxOut {
323-
value: Amount::ZERO,
324-
script_pubkey: change_script,
325-
};
326315

327316
let change_desc = graph
328317
.index
329318
.keychains()
330319
.find(|(k, _)| k == &change_keychain)
331320
.expect("must exist")
332321
.1;
322+
let min_value = 3 * change_desc.dust_value().to_sat();
323+
324+
let change_policy = ChangePolicy {
325+
min_value,
326+
drain_weights: DrainWeights::TR_KEYSPEND,
327+
};
328+
329+
let mut builder = bdk_tx::Builder::new();
333330

334-
let min_drain_value = change_desc.dust_value().to_sat();
331+
let locktime = assets
332+
.absolute_timelock
333+
.unwrap_or(absolute::LockTime::from_consensus(
334+
chain.get_chain_tip()?.height,
335+
));
336+
builder.locktime(locktime);
335337

338+
// fund outputs
339+
builder.add_outputs([(address.script_pubkey(), Amount::from_sat(value))]);
336340
let target = Target {
337341
outputs: TargetOutputs::fund_outputs(
338-
outputs
339-
.iter()
340-
.map(|output| (output.weight().to_wu(), output.value.to_sat())),
342+
builder
343+
.target_outputs()
344+
.map(|(w, v)| (w.to_wu(), v.to_sat())),
341345
),
342346
fee: TargetFee {
343347
rate: FeeRate::from_sat_per_vb(feerate),
344348
..Default::default()
345349
},
346350
};
347351

348-
let change_policy = ChangePolicy {
349-
min_value: min_drain_value,
350-
drain_weights: DrainWeights::TR_KEYSPEND,
351-
};
352-
353-
// run coin selection
352+
// select coins
354353
let mut selector = CoinSelector::new(&candidates);
355-
match cs_algorithm {
356-
CoinSelectionAlgo::BranchAndBound => {
357-
let metric = LowestFee {
358-
target,
359-
long_term_feerate: FeeRate::from_sat_per_vb(10.0),
360-
change_policy,
361-
};
362-
match selector.run_bnb(metric, 10_000) {
363-
Ok(_) => {}
364-
Err(_) => selector
365-
.select_until_target_met(target)
366-
.context("selecting coins")?,
367-
}
354+
if let CoinSelectionAlgo::BranchAndBound = coin_selection {
355+
let metric = metrics::Changeless {
356+
target,
357+
change_policy,
358+
};
359+
if selector.run_bnb(metric, 10_000).is_err() {
360+
selector.select_until_target_met(target)?;
368361
}
369-
_ => selector
370-
.select_until_target_met(target)
371-
.context("selecting coins")?,
362+
} else {
363+
selector.select_until_target_met(target)?;
372364
}
373365

374-
// get the selected plan utxos
375-
let selected: Vec<_> = selector.apply_selection(&plan_utxos).collect();
366+
// apply selection
367+
let selection = selector
368+
.apply_selection(&plan_utxos)
369+
.cloned()
370+
.map(|(plan, txo)| bdk_tx::PlanUtxo {
371+
plan,
372+
outpoint: txo.outpoint,
373+
txout: txo.txout,
374+
});
375+
builder.add_inputs(selection);
376376

377-
// if the selection tells us to use change and the change value is sufficient, we add it as an output
378-
let mut change_info = Option::<ChangeInfo>::None;
377+
// if the selection tells us to use change, we add it as an output
378+
// and fill in the change_info.
379+
let mut change_info = None;
379380
let drain = selector.drain(target, change_policy);
380-
if drain.value > min_drain_value {
381-
change_output.value = Amount::from_sat(drain.value);
382-
outputs.push(change_output);
381+
if drain.is_some() {
382+
builder.add_change_output(drain_spk, Amount::from_sat(drain.value));
383383
change_info = Some(ChangeInfo {
384384
change_keychain,
385-
indexer: changeset,
386-
index: change_index,
385+
indexer: index_changeset,
386+
index: drain_index,
387387
});
388-
outputs.shuffle(&mut thread_rng());
389388
}
390389

391-
let unsigned_tx = Transaction {
392-
version: transaction::Version::TWO,
393-
lock_time: assets
394-
.absolute_timelock
395-
.unwrap_or(absolute::LockTime::from_height(
396-
chain.get_chain_tip()?.height,
397-
)?),
398-
input: selected
399-
.iter()
400-
.map(|(plan, utxo)| TxIn {
401-
previous_output: utxo.outpoint,
402-
sequence: plan
403-
.relative_timelock
404-
.map_or(Sequence::ENABLE_RBF_NO_LOCKTIME, Sequence::from),
405-
..Default::default()
406-
})
407-
.collect(),
408-
output: outputs,
390+
// build psbt
391+
let mut provider = Provider {
392+
graph,
393+
rng: &mut thread_rng(),
409394
};
410395

411-
// update psbt with plan
412-
let mut psbt = Psbt::from_unsigned_tx(unsigned_tx)?;
413-
for (i, (plan, utxo)) in selected.iter().enumerate() {
414-
let psbt_input = &mut psbt.inputs[i];
415-
plan.update_psbt_input(psbt_input);
416-
psbt_input.witness_utxo = Some(utxo.txout.clone());
417-
}
396+
let mut updater = builder.build_psbt(&mut provider)?;
397+
let opt = bdk_tx::UpdateOptions {
398+
only_witness_utxo,
399+
update_with_descriptor: true,
400+
..Default::default()
401+
};
402+
updater.update_psbt(&provider, opt)?;
403+
let (psbt, _) = updater.into_finalizer();
418404

419405
Ok((psbt, change_info))
420406
}
@@ -684,26 +670,11 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
684670
let secp = Secp256k1::new();
685671
let (_, keymap) = Descriptor::parse_descriptor(&secp, &desc_str)?;
686672
if keymap.is_empty() {
687-
bail!("unable to sign")
673+
bail!("unable to sign");
688674
}
689-
690-
// note: we're only looking at the first entry in the keymap
691-
// the idea is to find something that impls `GetKey`
692-
let sign_res = match keymap.iter().next().expect("not empty") {
693-
(DescriptorPublicKey::Single(single_pub), DescriptorSecretKey::Single(prv)) => {
694-
let pk = match single_pub.key {
695-
SinglePubKey::FullKey(pk) => pk,
696-
SinglePubKey::XOnly(_) => unimplemented!("single xonly pubkey"),
697-
};
698-
let keys: HashMap<PublicKey, PrivateKey> = [(pk, prv.key)].into();
699-
psbt.sign(&keys, &secp)
700-
}
701-
(_, DescriptorSecretKey::XPrv(k)) => psbt.sign(&k.xkey, &secp),
702-
_ => unimplemented!("multi xkey signer"),
703-
};
704-
705-
let _ = sign_res
706-
.map_err(|errors| anyhow::anyhow!("failed to sign PSBT {:?}", errors))?;
675+
let _ = psbt
676+
.sign(&Signer(keymap), &secp)
677+
.map_err(|(_, errors)| anyhow::anyhow!("failed to sign PSBT {:?}", errors))?;
707678

708679
let mut obj = serde_json::Map::new();
709680
obj.insert("psbt".to_string(), json!(psbt.to_string()));
@@ -776,6 +747,36 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args>(
776747
}
777748
}
778749

750+
/// Helper struct for providing transaction data to tx builder
751+
struct Provider<'a> {
752+
graph: &'a KeychainTxGraph,
753+
rng: &'a mut dyn RngCore,
754+
}
755+
756+
impl DataProvider for Provider<'_> {
757+
fn get_tx(&self, txid: Txid) -> Option<Transaction> {
758+
self.graph
759+
.graph()
760+
.get_tx(txid)
761+
.map(|tx| tx.as_ref().clone())
762+
}
763+
764+
fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<Descriptor<DefiniteDescriptorKey>> {
765+
let &(keychain, index) = self.graph.index.index_of_spk(txout.script_pubkey.clone())?;
766+
let desc = self
767+
.graph
768+
.index
769+
.get_descriptor(keychain)
770+
.expect("must have descriptor");
771+
desc.at_derivation_index(index).ok()
772+
}
773+
774+
fn sort_transaction(&mut self, tx: &mut Transaction) {
775+
tx.input.shuffle(self.rng);
776+
tx.output.shuffle(self.rng);
777+
}
778+
}
779+
779780
/// The initial state returned by [`init_or_load`].
780781
pub struct Init<CS: clap::Subcommand, S: clap::Args> {
781782
/// CLI args

0 commit comments

Comments
 (0)