Skip to content

Commit 69966b6

Browse files
committed
feat: expose detailed try_finalize_psbt outcomes
1 parent fb7681a commit 69966b6

2 files changed

Lines changed: 190 additions & 6 deletions

File tree

src/wallet/mod.rs

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,55 @@ impl fmt::Display for AddressInfo {
184184
/// A `CanonicalTx` managed by a `Wallet`.
185185
pub type WalletTx<'a> = CanonicalTx<'a, Arc<Transaction>, ConfirmationBlockTime>;
186186

187+
/// The finalization status for a single PSBT input.
188+
#[derive(Debug, PartialEq)]
189+
pub enum FinalizeInputOutcome {
190+
/// The input was already finalized before this call.
191+
AlreadyFinalized,
192+
/// The input was successfully finalized during this call.
193+
Finalized,
194+
/// The wallet could not derive a descriptor for the input.
195+
MissingDescriptor,
196+
/// The wallet found the descriptor but could not construct the input satisfaction.
197+
CouldNotSatisfy(miniscript::Error),
198+
}
199+
200+
impl FinalizeInputOutcome {
201+
/// Whether the input is finalized after this call.
202+
pub fn is_finalized(&self) -> bool {
203+
matches!(self, Self::AlreadyFinalized | Self::Finalized)
204+
}
205+
}
206+
207+
/// Holds per-input PSBT finalization outcomes.
208+
#[derive(Debug, PartialEq)]
209+
pub struct FinalizedInputs {
210+
outcomes: BTreeMap<usize, FinalizeInputOutcome>,
211+
}
212+
213+
impl FinalizedInputs {
214+
fn new(outcomes: BTreeMap<usize, FinalizeInputOutcome>) -> Self {
215+
Self { outcomes }
216+
}
217+
218+
/// Whether all inputs are finalized after this call.
219+
pub fn is_finalized(&self) -> bool {
220+
self.outcomes
221+
.values()
222+
.all(FinalizeInputOutcome::is_finalized)
223+
}
224+
225+
/// Borrow the per-input finalization outcomes.
226+
pub fn outcomes(&self) -> &BTreeMap<usize, FinalizeInputOutcome> {
227+
&self.outcomes
228+
}
229+
230+
/// Consume the collection and return the per-input finalization outcomes.
231+
pub fn into_outcomes(self) -> BTreeMap<usize, FinalizeInputOutcome> {
232+
self.outcomes
233+
}
234+
}
235+
187236
impl Wallet {
188237
/// Build a new single descriptor [`Wallet`].
189238
///
@@ -1842,6 +1891,19 @@ impl Wallet {
18421891
psbt: &mut Psbt,
18431892
sign_options: SignOptions,
18441893
) -> Result<bool, SignerError> {
1894+
Ok(self.try_finalize_psbt(psbt, sign_options)?.is_finalized())
1895+
}
1896+
1897+
/// Finalize a PSBT and return per-input finalization results. Use this method when you need to
1898+
/// inspect why a specific input could not be finalized.
1899+
///
1900+
/// The method should only return `Err` when the PSBT is malformed, for example if its inputs
1901+
/// are out of bounds.
1902+
pub fn try_finalize_psbt(
1903+
&self,
1904+
psbt: &mut Psbt,
1905+
sign_options: SignOptions,
1906+
) -> Result<FinalizedInputs, IndexOutOfBoundsError> {
18451907
let tx = &psbt.unsigned_tx;
18461908
let chain_tip = self.chain.tip().block_id();
18471909
let prev_txids = tx
@@ -1867,14 +1929,15 @@ impl Wallet {
18671929
})
18681930
.collect::<HashMap<Txid, u32>>();
18691931

1870-
let mut finished = true;
1932+
let mut outcomes = BTreeMap::new();
18711933

18721934
for (n, input) in tx.input.iter().enumerate() {
18731935
let psbt_input = &psbt
18741936
.inputs
18751937
.get(n)
18761938
.ok_or(IndexOutOfBoundsError::new(n, psbt.inputs.len()))?;
18771939
if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
1940+
outcomes.insert(n, FinalizeInputOutcome::AlreadyFinalized);
18781941
continue;
18791942
}
18801943
let confirmation_height = confirmation_heights
@@ -1927,23 +1990,29 @@ impl Wallet {
19271990
if !tmp_input.witness.is_empty() {
19281991
psbt_input.final_script_witness = Some(tmp_input.witness);
19291992
}
1993+
outcomes.insert(n, FinalizeInputOutcome::Finalized);
1994+
}
1995+
Err(err) => {
1996+
outcomes.insert(n, FinalizeInputOutcome::CouldNotSatisfy(err));
19301997
}
1931-
Err(_) => finished = false,
19321998
}
19331999
}
1934-
None => finished = false,
2000+
None => {
2001+
outcomes.insert(n, FinalizeInputOutcome::MissingDescriptor);
2002+
}
19352003
}
19362004
}
19372005

19382006
// Clear derivation paths from outputs.
1939-
if finished {
2007+
let finalized = FinalizedInputs::new(outcomes);
2008+
if finalized.is_finalized() {
19402009
for output in &mut psbt.outputs {
19412010
output.bip32_derivation.clear();
19422011
output.tap_key_origins.clear();
19432012
}
19442013
}
19452014

1946-
Ok(finished)
2015+
Ok(finalized)
19472016
}
19482017

19492018
/// Return the secp256k1 context used for all signing operations.

tests/wallet.rs

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ use bdk_wallet::psbt::PsbtUtils;
1010
use bdk_wallet::signer::{SignOptions, SignerError};
1111
use bdk_wallet::test_utils::*;
1212
use bdk_wallet::KeychainKind;
13-
use bdk_wallet::{AddressInfo, Balance, PersistedWallet, Update, Wallet, WalletTx};
13+
use bdk_wallet::{
14+
AddressInfo, Balance, FinalizeInputOutcome, IndexOutOfBoundsError, PersistedWallet, Update,
15+
Wallet, WalletTx,
16+
};
1417
use bitcoin::constants::COINBASE_MATURITY;
1518
use bitcoin::hashes::Hash;
1619
use bitcoin::script::PushBytesBuf;
@@ -1560,6 +1563,118 @@ fn test_try_finalize_sign_option() {
15601563
}
15611564
}
15621565

1566+
#[test]
1567+
fn test_try_finalize_psbt_outcomes() {
1568+
{
1569+
let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh());
1570+
let addr = wallet.next_unused_address(KeychainKind::External);
1571+
let mut builder = wallet.build_tx();
1572+
builder.drain_to(addr.script_pubkey()).drain_wallet();
1573+
let mut psbt = builder.finish().unwrap();
1574+
1575+
let is_final = wallet
1576+
.sign(
1577+
&mut psbt,
1578+
SignOptions {
1579+
try_finalize: false,
1580+
..Default::default()
1581+
},
1582+
)
1583+
.unwrap();
1584+
assert!(!is_final);
1585+
assert!(
1586+
psbt.outputs
1587+
.iter()
1588+
.any(|output| !output.bip32_derivation.is_empty()),
1589+
"expected wallet-owned outputs to retain derivation data before finalization"
1590+
);
1591+
1592+
let finalized = wallet
1593+
.try_finalize_psbt(&mut psbt, SignOptions::default())
1594+
.unwrap();
1595+
1596+
assert!(finalized.is_finalized());
1597+
assert_matches!(
1598+
finalized.outcomes().get(&0),
1599+
Some(FinalizeInputOutcome::Finalized)
1600+
);
1601+
assert!(
1602+
psbt.inputs[0].final_script_sig.is_some()
1603+
|| psbt.inputs[0].final_script_witness.is_some()
1604+
);
1605+
assert!(psbt
1606+
.outputs
1607+
.iter()
1608+
.all(|output| output.bip32_derivation.is_empty()));
1609+
1610+
let finalized = wallet
1611+
.try_finalize_psbt(&mut psbt, SignOptions::default())
1612+
.unwrap();
1613+
1614+
assert!(finalized.is_finalized());
1615+
assert_matches!(
1616+
finalized.outcomes().get(&0),
1617+
Some(FinalizeInputOutcome::AlreadyFinalized)
1618+
);
1619+
}
1620+
1621+
{
1622+
let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh());
1623+
let addr = wallet.next_unused_address(KeychainKind::External);
1624+
let mut builder = wallet.build_tx();
1625+
builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000));
1626+
let mut psbt = builder.finish().unwrap();
1627+
1628+
let dud_input = bitcoin::psbt::Input {
1629+
witness_utxo: Some(TxOut {
1630+
value: Amount::from_sat(100_000),
1631+
script_pubkey: miniscript::Descriptor::<bitcoin::PublicKey>::from_str(
1632+
"wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)",
1633+
)
1634+
.unwrap()
1635+
.script_pubkey(),
1636+
}),
1637+
..Default::default()
1638+
};
1639+
1640+
psbt.inputs.push(dud_input);
1641+
psbt.unsigned_tx.input.push(bitcoin::TxIn::default());
1642+
1643+
let finalized = wallet
1644+
.try_finalize_psbt(&mut psbt, SignOptions::default())
1645+
.unwrap();
1646+
1647+
assert!(!finalized.is_finalized());
1648+
assert_matches!(
1649+
finalized.outcomes().get(&0),
1650+
Some(FinalizeInputOutcome::CouldNotSatisfy(
1651+
bdk_wallet::miniscript::Error::MissingSig(_)
1652+
))
1653+
);
1654+
assert_matches!(
1655+
finalized.outcomes().get(&1),
1656+
Some(FinalizeInputOutcome::MissingDescriptor)
1657+
);
1658+
}
1659+
}
1660+
1661+
#[test]
1662+
fn test_try_finalize_psbt_returns_index_out_of_bounds_for_malformed_psbt() {
1663+
let (mut wallet, _) = get_funded_wallet_single(get_test_wpkh());
1664+
let addr = wallet.next_unused_address(KeychainKind::External);
1665+
let mut builder = wallet.build_tx();
1666+
builder.drain_to(addr.script_pubkey()).drain_wallet();
1667+
let mut psbt = builder.finish().unwrap();
1668+
1669+
psbt.inputs.clear();
1670+
1671+
let err = wallet
1672+
.try_finalize_psbt(&mut psbt, SignOptions::default())
1673+
.unwrap_err();
1674+
1675+
assert_eq!(err, IndexOutOfBoundsError::new(0, 0));
1676+
}
1677+
15631678
#[test]
15641679
fn test_taproot_try_finalize_sign_option() {
15651680
let (mut wallet, _) = get_funded_wallet_single(get_test_tr_with_taptree());

0 commit comments

Comments
 (0)