Skip to content

Commit 3e1290b

Browse files
committed
WIP
1 parent b900712 commit 3e1290b

File tree

6 files changed

+613
-184
lines changed

6 files changed

+613
-184
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ readme = "README.md"
1111
[dependencies]
1212
miniscript = { version = "12", default-features = false }
1313
bdk_coin_select = "0.4.0"
14+
bdk_chain = { version = "0.21" }
1415

1516
[dev-dependencies]
1617
anyhow = "1"
17-
bdk_chain = { version = "0.21" }
1818
bdk_tx = { path = "." }
1919
bitcoin = { version = "0.32", features = ["rand-std"] }
20+
bdk_testenv = "0.11.1"
21+
bdk_bitcoind_rpc = "0.18.0"
2022

2123
[features]
2224
default = ["std"]

src/create_psbt.rs

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
use std::vec::Vec;
2+
3+
use bitcoin::{absolute, transaction};
4+
use miniscript::{bitcoin, psbt::PsbtExt};
5+
6+
use crate::{Finalizer, Input, Output};
7+
8+
/// Parameters for creating a psbt.
9+
#[derive(Debug, Clone)]
10+
pub struct CreatePsbtParams {
11+
/// Inputs to fund the tx.
12+
///
13+
/// It is up to the caller to not duplicate inputs, spend from 2 conflicting txs, spend from
14+
/// invalid inputs, etc.
15+
pub inputs: Vec<Input>,
16+
/// Outputs.
17+
pub outputs: Vec<Output>,
18+
19+
/// Use a specific [`transaction::Version`].
20+
pub version: transaction::Version,
21+
22+
/// Fallback tx locktime.
23+
///
24+
/// The locktime to use if no inputs specifies a required absolute locktime.
25+
///
26+
/// It is best practive to set this to the latest block height to avoid fee sniping.
27+
pub fallback_locktime: absolute::LockTime,
28+
29+
/// Recommended.
30+
pub mandate_full_tx_for_segwit_v0: bool,
31+
}
32+
33+
impl Default for CreatePsbtParams {
34+
fn default() -> Self {
35+
Self {
36+
version: transaction::Version::TWO,
37+
inputs: Default::default(),
38+
outputs: Default::default(),
39+
fallback_locktime: absolute::LockTime::ZERO,
40+
mandate_full_tx_for_segwit_v0: true,
41+
}
42+
}
43+
}
44+
45+
/// Occurs when creating a psbt fails.
46+
#[derive(Debug)]
47+
pub enum CreatePsbtError {
48+
/// Attempted to mix locktime types.
49+
LockTypeMismatch,
50+
/// Missing tx for legacy input.
51+
MissingFullTxForLegacyInput(Input),
52+
/// Missing tx for segwit v0 input.
53+
MissingFullTxForSegwitV0Input(Input),
54+
/// Psbt error.
55+
Psbt(bitcoin::psbt::Error),
56+
/// Update psbt output with descriptor error.
57+
OutputUpdate(miniscript::psbt::OutputUpdateError),
58+
}
59+
60+
impl core::fmt::Display for CreatePsbtError {
61+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
62+
match self {
63+
CreatePsbtError::LockTypeMismatch => write!(f, "cannot mix locktime units"),
64+
CreatePsbtError::MissingFullTxForLegacyInput(input) => write!(
65+
f,
66+
"legacy input that spends {} requires PSBT_IN_NON_WITNESS_UTXO",
67+
input.prev_outpoint()
68+
),
69+
CreatePsbtError::MissingFullTxForSegwitV0Input(input) => write!(
70+
f,
71+
"segwit v0 input that spends {} requires PSBT_IN_NON_WITNESS_UTXO",
72+
input.prev_outpoint()
73+
),
74+
CreatePsbtError::Psbt(error) => error.fmt(f),
75+
CreatePsbtError::OutputUpdate(output_update_error) => output_update_error.fmt(f),
76+
}
77+
}
78+
}
79+
80+
#[cfg(feature = "std")]
81+
impl std::error::Error for CreatePsbtError {}
82+
83+
const FALLBACK_SEQUENCE: bitcoin::Sequence = bitcoin::Sequence::MAX;
84+
85+
/// Returns none if there is a mismatch of units in `locktimes`.
86+
///
87+
/// As according to BIP-64...
88+
pub fn accumulate_max_locktime(
89+
locktimes: impl IntoIterator<Item = absolute::LockTime>,
90+
fallback: absolute::LockTime,
91+
) -> Option<absolute::LockTime> {
92+
let mut acc = Option::<absolute::LockTime>::None;
93+
for locktime in locktimes {
94+
match &mut acc {
95+
Some(acc) => {
96+
if !acc.is_same_unit(locktime) {
97+
return None;
98+
}
99+
if acc.is_implied_by(locktime) {
100+
*acc = locktime;
101+
}
102+
}
103+
acc => *acc = Some(locktime),
104+
};
105+
}
106+
if acc.is_none() {
107+
acc = Some(fallback);
108+
}
109+
acc
110+
}
111+
112+
/// Create psbt.
113+
pub fn create_psbt(
114+
params: CreatePsbtParams,
115+
) -> Result<(bitcoin::Psbt, Finalizer), CreatePsbtError> {
116+
let mut psbt = bitcoin::Psbt::from_unsigned_tx(bitcoin::Transaction {
117+
version: params.version,
118+
lock_time: accumulate_max_locktime(
119+
params
120+
.inputs
121+
.iter()
122+
.filter_map(|input| input.plan().absolute_timelock),
123+
params.fallback_locktime,
124+
)
125+
.ok_or(CreatePsbtError::LockTypeMismatch)?,
126+
input: params
127+
.inputs
128+
.iter()
129+
.map(|input| bitcoin::TxIn {
130+
previous_output: input.prev_outpoint(),
131+
sequence: input
132+
.plan()
133+
.relative_timelock
134+
.map_or(FALLBACK_SEQUENCE, |locktime| locktime.to_sequence()),
135+
..Default::default()
136+
})
137+
.collect(),
138+
output: params.outputs.iter().map(|output| output.txout()).collect(),
139+
})
140+
.map_err(CreatePsbtError::Psbt)?;
141+
142+
for (plan_input, psbt_input) in params.inputs.iter().zip(psbt.inputs.iter_mut()) {
143+
let txout = plan_input.prev_txout();
144+
145+
plan_input.plan().update_psbt_input(psbt_input);
146+
147+
let witness_version = plan_input.plan().witness_version();
148+
if witness_version.is_some() {
149+
psbt_input.witness_utxo = Some(txout.clone());
150+
}
151+
152+
// We are allowed to have full tx for segwit inputs. Might as well include it.
153+
// If the caller does not wish to include the full tx in Segwit V0 inputs, they should not
154+
// include it in `crate::Input`.
155+
psbt_input.non_witness_utxo = plan_input.prev_tx().cloned();
156+
if psbt_input.non_witness_utxo.is_none() {
157+
if witness_version.is_none() {
158+
return Err(CreatePsbtError::MissingFullTxForLegacyInput(
159+
plan_input.clone(),
160+
));
161+
}
162+
if params.mandate_full_tx_for_segwit_v0
163+
&& witness_version == Some(bitcoin::WitnessVersion::V0)
164+
{
165+
return Err(CreatePsbtError::MissingFullTxForSegwitV0Input(
166+
plan_input.clone(),
167+
));
168+
}
169+
}
170+
}
171+
for (output_index, output) in params.outputs.iter().enumerate() {
172+
if let Some(desc) = output.descriptor() {
173+
psbt.update_output_with_descriptor(output_index, desc)
174+
.map_err(CreatePsbtError::OutputUpdate)?;
175+
}
176+
}
177+
178+
let finalizer = Finalizer {
179+
plans: params
180+
.inputs
181+
.into_iter()
182+
.map(|input| (input.prev_outpoint(), input.plan().clone()))
183+
.collect(),
184+
};
185+
186+
Ok((psbt, finalizer))
187+
}

0 commit comments

Comments
 (0)