Skip to content

Commit 7dcfb2a

Browse files
committed
wip: implement create_psbt for Wallet
psbt_params: Add optional plan Assets to Params wallet: Update `create_psbt` to get assets from the params - Add variant `CreatePsbtError::Plan` which happens when a manually selected output can not be planned. example: Change `psbt` example to sign using `bip32::Xpriv` fix: map the change keychain in `create_psbt` Move psbt Params to src/psbt/params.rs Make `SelectionStrategy` non-exhaustive wallet: Enhance `plan_input` by including after + older The idea is that if the local chain has surpassed the lock height needed to spend a utxo, then the wallet can respond accordingly with no interaction on the part of the user. params: Fix `add_assets` function signature Rename PsbtParams test: Add `test_create_psbt` psbt: Add method `PsbtParams::add_utxos` - Changed type of `PsbtParams::utxos` field to `HashSet<OutPoint>` - Fix logic of plan error in `create_psbt` - Add test `test_create_psbt_cltv` psbt,params: Add `remove_utxo` wip: Introduce `ReplaceParams` fix clippy nit test: improve `test_sanitize_rbf_set` test: Add `test_replace_by_fee`
1 parent 3af563e commit 7dcfb2a

File tree

9 files changed

+1451
-10
lines changed

9 files changed

+1451
-10
lines changed

wallet/Cargo.toml

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ bitcoin = { version = "0.32.6", features = [ "serde", "base64" ], default-featur
2222
serde = { version = "^1.0", features = ["derive"] }
2323
serde_json = { version = "^1.0" }
2424
bdk_chain = { version = "0.23.1", features = [ "miniscript", "serde" ], default-features = false }
25-
anyhow = { version = "1.0.98", optional = true }
26-
tempfile = { version = "3.20.0", optional = true }
25+
bdk_tx = { version = "0.1.0" }
26+
bdk_coin_select = { version = "0.4.0" }
2727

2828
# Optional dependencies
2929
bip39 = { version = "2.0", optional = true }
3030
bdk_file_store = { version = "0.21.1", optional = true }
31+
anyhow = { version = "1.0.98", optional = true }
32+
tempfile = { version = "3.20.0", optional = true }
3133

3234
[features]
3335
default = ["std"]
@@ -60,3 +62,11 @@ required-features = ["all-keys"]
6062
name = "miniscriptc"
6163
path = "examples/compiler.rs"
6264
required-features = ["compiler"]
65+
66+
[[example]]
67+
name = "psbt"
68+
required-features = ["test-utils"]
69+
70+
[[example]]
71+
name = "replace_by_fee"
72+
required-features = ["test-utils"]

wallet/examples/psbt.rs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
#![allow(clippy::print_stdout)]
2+
3+
use std::collections::HashMap;
4+
use std::str::FromStr;
5+
use std::sync::Arc;
6+
7+
use bdk_chain::BlockId;
8+
use bdk_chain::ConfirmationBlockTime;
9+
use bdk_chain::TxUpdate;
10+
use bdk_wallet::psbt::{PsbtParams, SelectionStrategy::*};
11+
use bdk_wallet::test_utils::*;
12+
use bdk_wallet::{KeychainKind::*, Update, Wallet};
13+
use bitcoin::{
14+
bip32, consensus,
15+
secp256k1::{self, rand},
16+
Address, Amount, OutPoint, TxIn, TxOut,
17+
};
18+
use rand::Rng;
19+
20+
// This example shows how to create a PSBT using BDK Wallet.
21+
22+
const NETWORK: bitcoin::Network = bitcoin::Network::Signet;
23+
const SEND_TO: &str = "tb1pw3g5qvnkryghme7pyal228ekj6vq48zc5k983lqtlr2a96n4xw0q5ejknw";
24+
const AMOUNT: Amount = Amount::from_sat(42_000);
25+
const FEERATE: f64 = 2.0; // sat/vb
26+
27+
fn main() -> anyhow::Result<()> {
28+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
29+
let secp = secp256k1::Secp256k1::new();
30+
31+
// Xpriv to be used for signing the PSBT
32+
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L")?;
33+
34+
// Create wallet and fund it.
35+
let mut wallet = Wallet::create(desc, change_desc)
36+
.network(NETWORK)
37+
.create_wallet_no_persist()?;
38+
39+
fund_wallet(&mut wallet)?;
40+
41+
let utxos = wallet
42+
.list_unspent()
43+
.map(|output| (output.outpoint, output))
44+
.collect::<HashMap<_, _>>();
45+
46+
// Build params.
47+
let mut params = PsbtParams::default();
48+
let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?;
49+
let feerate = feerate_unchecked(FEERATE);
50+
params
51+
.add_recipients([(addr, AMOUNT)])
52+
.feerate(feerate)
53+
.coin_selection(SingleRandomDraw);
54+
55+
// Create PSBT (which also returns the Finalizer).
56+
let (mut psbt, finalizer) = wallet.create_psbt(params)?;
57+
58+
dbg!(&psbt);
59+
60+
let tx = &psbt.unsigned_tx;
61+
for txin in &tx.input {
62+
let op = txin.previous_output;
63+
let output = utxos.get(&op).unwrap();
64+
println!("TxIn: {}", output.txout.value);
65+
}
66+
for txout in &tx.output {
67+
println!("TxOut: {}", txout.value);
68+
}
69+
70+
let _ = psbt.sign(&xprv, &secp);
71+
println!("Signed: {}", !psbt.inputs[0].partial_sigs.is_empty());
72+
let finalize_res = finalizer.finalize(&mut psbt);
73+
println!("Finalized: {}", finalize_res.is_finalized());
74+
75+
let tx = psbt.extract_tx()?;
76+
let feerate = wallet.calculate_fee_rate(&tx)?;
77+
println!("Fee rate: {} sat/vb", bdk_wallet::floating_rate!(feerate));
78+
79+
println!("{}", consensus::encode::serialize_hex(&tx));
80+
81+
Ok(())
82+
}
83+
84+
fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<()> {
85+
let anchor = ConfirmationBlockTime {
86+
block_id: BlockId {
87+
height: 260071,
88+
hash: "000000099f67ae6469d1ad0525d756e24d4b02fbf27d65b3f413d5feb367ec48".parse()?,
89+
},
90+
confirmation_time: 1752184658,
91+
};
92+
insert_checkpoint(wallet, anchor.block_id);
93+
94+
let mut rng = rand::thread_rng();
95+
96+
// Fund wallet with several random utxos
97+
for i in 0..21 {
98+
let value = 10_000 * (i + 1) + (100 * rng.gen_range(0..10));
99+
let addr = wallet.reveal_next_address(External).address;
100+
receive_output_to_addr(
101+
wallet,
102+
addr,
103+
Amount::from_sat(value),
104+
ReceiveTo::Block(anchor),
105+
);
106+
}
107+
108+
let tip = BlockId {
109+
height: 260171,
110+
hash: "0000000b9efb77450e753ae9fd7be9f69219511c27b6e95c28f4126f3e1591c3".parse()?,
111+
};
112+
insert_checkpoint(wallet, tip);
113+
114+
Ok(())
115+
}
116+
117+
// Note: this is borrowed from `test-utils`, but here the tx appears as a coinbase tx
118+
// and inserting it does not automatically include a timestamp.
119+
fn receive_output_to_addr(
120+
wallet: &mut Wallet,
121+
addr: Address,
122+
value: Amount,
123+
receive_to: impl Into<ReceiveTo>,
124+
) -> OutPoint {
125+
let tx = bitcoin::Transaction {
126+
lock_time: bitcoin::absolute::LockTime::ZERO,
127+
version: bitcoin::transaction::Version::TWO,
128+
input: vec![TxIn::default()],
129+
output: vec![TxOut {
130+
script_pubkey: addr.script_pubkey(),
131+
value,
132+
}],
133+
};
134+
135+
// Insert tx
136+
let txid = tx.compute_txid();
137+
let mut tx_update = TxUpdate::default();
138+
tx_update.txs = vec![Arc::new(tx)];
139+
wallet
140+
.apply_update(Update {
141+
tx_update,
142+
..Default::default()
143+
})
144+
.unwrap();
145+
146+
// Insert anchor or last-seen.
147+
match receive_to.into() {
148+
ReceiveTo::Block(anchor) => insert_anchor(wallet, txid, anchor),
149+
ReceiveTo::Mempool(last_seen) => insert_seen_at(wallet, txid, last_seen),
150+
}
151+
152+
OutPoint { txid, vout: 0 }
153+
}

wallet/examples/replace_by_fee.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#![allow(clippy::print_stdout)]
2+
3+
use std::str::FromStr;
4+
use std::sync::Arc;
5+
6+
use bdk_chain::BlockId;
7+
use bdk_chain::ConfirmationBlockTime;
8+
use bdk_chain::TxUpdate;
9+
use bdk_wallet::test_utils::*;
10+
use bdk_wallet::{Update, Wallet};
11+
use bitcoin::{bip32, consensus, secp256k1, Address, FeeRate, Transaction};
12+
13+
// This example shows how to create a Replace-By-Fee (RBF) transaction using BDK Wallet.
14+
15+
const NETWORK: bitcoin::Network = bitcoin::Network::Regtest;
16+
const SEND_TO: &str = "bcrt1q3yfqg2v9d605r45y5ddt5unz5n8v7jl5yk4a4f";
17+
18+
fn main() -> anyhow::Result<()> {
19+
let desc = "wpkh(tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU/84h/1h/0h/0/*)";
20+
let change_desc = "wpkh(tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU/84h/1h/0h/1/*)";
21+
let secp = secp256k1::Secp256k1::new();
22+
23+
// Xpriv to be used for signing the PSBT
24+
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPe5tkv8BYJRupCNULhJYDv6qrtVAK9fNVheU6TbscSedVi8KQk8vVZqXMnsGomtVkR4nprbgsxTS5mAQPV4dpPXNvsmYcgZU")?;
25+
26+
// Create wallet and "fund" it.
27+
let mut wallet = Wallet::create(desc, change_desc)
28+
.network(NETWORK)
29+
.create_wallet_no_persist()?;
30+
31+
// `tx_1` is the unconfirmed wallet tx that we want to replace.
32+
let tx_1 = fund_wallet(&mut wallet)?;
33+
wallet.apply_unconfirmed_txs([(tx_1.clone(), 1234567000)]);
34+
35+
// We'll need to fill in the original recipient details.
36+
let addr = Address::from_str(SEND_TO)?.require_network(NETWORK)?;
37+
let txo = tx_1
38+
.output
39+
.iter()
40+
.find(|txo| txo.script_pubkey == addr.script_pubkey())
41+
.expect("failed to find orginal recipient")
42+
.clone();
43+
44+
// Now build fee bump.
45+
let (mut psbt, finalizer) = wallet.replace_by_fee_and_recipients(
46+
&[Arc::clone(&tx_1)],
47+
FeeRate::from_sat_per_vb_unchecked(5),
48+
vec![(txo.script_pubkey, txo.value)],
49+
)?;
50+
51+
let _ = psbt.sign(&xprv, &secp);
52+
println!("Signed: {}", !psbt.inputs[0].partial_sigs.is_empty());
53+
let finalize_res = finalizer.finalize(&mut psbt);
54+
println!("Finalized: {}", finalize_res.is_finalized());
55+
56+
let tx = psbt.extract_tx()?;
57+
let feerate = wallet.calculate_fee_rate(&tx)?;
58+
println!("Fee rate: {} sat/vb", bdk_wallet::floating_rate!(feerate));
59+
60+
println!("{}", consensus::encode::serialize_hex(&tx));
61+
62+
wallet.apply_unconfirmed_txs([(tx.clone(), 1234567001)]);
63+
64+
let txid_2 = tx.compute_txid();
65+
66+
assert!(
67+
wallet
68+
.tx_graph()
69+
.direct_conflicts(&tx_1)
70+
.any(|(_, txid)| txid == txid_2),
71+
"ERROR: RBF tx does not conflict with `tx_1`",
72+
);
73+
74+
Ok(())
75+
}
76+
77+
fn fund_wallet(wallet: &mut Wallet) -> anyhow::Result<Arc<Transaction>> {
78+
// The parent of `tx`. This is needed to compute the original fee.
79+
let tx0: Arc<Transaction> = consensus::encode::deserialize_hex(
80+
"020000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025100ffffffff0200f2052a010000001600144d34238b9c4c59b9e2781e2426a142a75b8901ab0000000000000000266a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf90120000000000000000000000000000000000000000000000000000000000000000000000000",
81+
)
82+
.map(Arc::new)?;
83+
84+
let anchor_block = BlockId {
85+
height: 101,
86+
hash: "3bcc1c447c6b3886f43e416b5c21cf5c139dc4829a71dc78609bc8f6235611c5".parse()?,
87+
};
88+
insert_tx_anchor(wallet, tx0, anchor_block);
89+
90+
let tx: Arc<Transaction> = consensus::encode::deserialize_hex(
91+
"020000000001014cb96536e94ba3f840cb5c2c965c8f9a306209de63fcd02060219aaf14f1d7b30000000000fdffffff0280de80020000000016001489120429856e9f41d684a35aba7262a4cecf4bf4f312852701000000160014757a57b3009c0e9b2b9aa548434dc295e21aeb05024730440220400c0a767ce42e0ea02b72faabb7f3433e607b475111285e0975bba1e6fd2e13022059453d83cbacb6652ba075f59ca0437036f3f94cae1959c7c5c0f96a8954707a012102c0851c2d2bddc1dd0b05caeac307703ec0c4b96ecad5a85af47f6420e2ef6c661b000000",
92+
)
93+
.map(Arc::new)?;
94+
95+
Ok(tx)
96+
}
97+
98+
fn insert_tx_anchor(wallet: &mut Wallet, tx: Arc<Transaction>, block_id: BlockId) {
99+
insert_checkpoint(wallet, block_id);
100+
let anchor = ConfirmationBlockTime {
101+
block_id,
102+
confirmation_time: 1234567000,
103+
};
104+
let txid = tx.compute_txid();
105+
106+
let mut tx_update = TxUpdate::default();
107+
tx_update.txs = vec![tx];
108+
tx_update.anchors = [(anchor, txid)].into();
109+
110+
wallet
111+
.apply_update(Update {
112+
tx_update,
113+
..Default::default()
114+
})
115+
.expect("failed to apply update");
116+
}

wallet/src/psbt/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ use bitcoin::FeeRate;
1717
use bitcoin::Psbt;
1818
use bitcoin::TxOut;
1919

20+
mod params;
21+
22+
pub use params::*;
23+
2024
// TODO upstream the functions here to `rust-bitcoin`?
2125

2226
/// Trait to add functions to extract utxos and calculate fees.

0 commit comments

Comments
 (0)