Skip to content

Commit e046adc

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

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, Clone, Copy, PartialEq, Eq)]
189+
pub enum FinalizePsbtInputResult {
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,
198+
}
199+
200+
impl FinalizePsbtInputResult {
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 results.
208+
#[derive(Debug, Clone, PartialEq, Eq)]
209+
pub struct FinalizePsbtResult {
210+
results: BTreeMap<usize, FinalizePsbtInputResult>,
211+
}
212+
213+
impl FinalizePsbtResult {
214+
fn new(results: BTreeMap<usize, FinalizePsbtInputResult>) -> Self {
215+
Self { results }
216+
}
217+
218+
/// Whether all inputs are finalized after this call.
219+
pub fn is_finalized(&self) -> bool {
220+
self.results
221+
.values()
222+
.all(FinalizePsbtInputResult::is_finalized)
223+
}
224+
225+
/// Borrow the per-input finalization results.
226+
pub fn results(&self) -> &BTreeMap<usize, FinalizePsbtInputResult> {
227+
&self.results
228+
}
229+
230+
/// Consume the result and return the per-input finalization results.
231+
pub fn into_results(self) -> BTreeMap<usize, FinalizePsbtInputResult> {
232+
self.results
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<FinalizePsbtResult, SignerError> {
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 results = 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+
results.insert(n, FinalizePsbtInputResult::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+
results.insert(n, FinalizePsbtInputResult::Finalized);
1994+
}
1995+
Err(_) => {
1996+
results.insert(n, FinalizePsbtInputResult::CouldNotSatisfy);
19301997
}
1931-
Err(_) => finished = false,
19321998
}
19331999
}
1934-
None => finished = false,
2000+
None => {
2001+
results.insert(n, FinalizePsbtInputResult::MissingDescriptor);
2002+
}
19352003
}
19362004
}
19372005

19382006
// Clear derivation paths from outputs.
1939-
if finished {
2007+
let finalized = FinalizePsbtResult::new(results);
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,9 @@ 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, FinalizePsbtInputResult, PersistedWallet, Update, Wallet, WalletTx,
15+
};
1416
use bitcoin::constants::COINBASE_MATURITY;
1517
use bitcoin::hashes::Hash;
1618
use bitcoin::script::PushBytesBuf;
@@ -1526,6 +1528,119 @@ fn test_signing_only_one_of_multiple_inputs() {
15261528
)
15271529
}
15281530

1531+
#[test]
1532+
fn test_finalize_psbt_reports_per_input_results() {
1533+
let (mut wallet, _) = get_funded_wallet_wpkh();
1534+
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX")
1535+
.unwrap()
1536+
.assume_checked();
1537+
let mut builder = wallet.build_tx();
1538+
builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000));
1539+
let mut psbt = builder.finish().unwrap();
1540+
1541+
let dud_input = bitcoin::psbt::Input {
1542+
witness_utxo: Some(TxOut {
1543+
value: Amount::from_sat(100_000),
1544+
script_pubkey: miniscript::Descriptor::<bitcoin::PublicKey>::from_str(
1545+
"wpkh(025476c2e83188368da1ff3e292e7acafcdb3566bb0ad253f62fc70f07aeee6357)",
1546+
)
1547+
.unwrap()
1548+
.script_pubkey(),
1549+
}),
1550+
..Default::default()
1551+
};
1552+
1553+
psbt.inputs.push(dud_input);
1554+
psbt.unsigned_tx.input.push(bitcoin::TxIn::default());
1555+
1556+
wallet
1557+
.sign(
1558+
&mut psbt,
1559+
SignOptions {
1560+
try_finalize: false,
1561+
trust_witness_utxo: true,
1562+
..Default::default()
1563+
},
1564+
)
1565+
.unwrap();
1566+
1567+
let results = wallet
1568+
.try_finalize_psbt(&mut psbt, SignOptions::default())
1569+
.unwrap()
1570+
.into_results();
1571+
1572+
assert_eq!(results.get(&0), Some(&FinalizePsbtInputResult::Finalized));
1573+
assert_eq!(
1574+
results.get(&1),
1575+
Some(&FinalizePsbtInputResult::MissingDescriptor)
1576+
);
1577+
assert!(psbt.inputs[0].final_script_witness.is_some());
1578+
}
1579+
1580+
#[test]
1581+
fn test_finalize_psbt_reports_could_not_satisfy() {
1582+
let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
1583+
let addr = wallet.next_unused_address(KeychainKind::External);
1584+
let mut builder = wallet.build_tx();
1585+
builder.drain_to(addr.script_pubkey()).drain_wallet();
1586+
let mut psbt = builder.finish().unwrap();
1587+
1588+
let result = wallet
1589+
.try_finalize_psbt(&mut psbt, SignOptions::default())
1590+
.unwrap();
1591+
1592+
assert!(!result.is_finalized());
1593+
assert_eq!(
1594+
result.results().get(&0),
1595+
Some(&FinalizePsbtInputResult::CouldNotSatisfy)
1596+
);
1597+
assert!(psbt.inputs[0].final_script_witness.is_none());
1598+
}
1599+
1600+
#[test]
1601+
fn test_finalize_psbt_reports_already_finalized_inputs() {
1602+
let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
1603+
let addr = wallet.next_unused_address(KeychainKind::External);
1604+
let mut builder = wallet.build_tx();
1605+
builder.drain_to(addr.script_pubkey()).drain_wallet();
1606+
let mut psbt = builder.finish().unwrap();
1607+
1608+
assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap());
1609+
1610+
let result = wallet
1611+
.try_finalize_psbt(&mut psbt, SignOptions::default())
1612+
.unwrap();
1613+
1614+
assert!(result.is_finalized());
1615+
assert_eq!(
1616+
result.results().get(&0),
1617+
Some(&FinalizePsbtInputResult::AlreadyFinalized)
1618+
);
1619+
}
1620+
1621+
#[test]
1622+
fn test_finalize_psbt_preserves_bool_api() {
1623+
let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
1624+
let addr = wallet.next_unused_address(KeychainKind::External);
1625+
let mut builder = wallet.build_tx();
1626+
builder.drain_to(addr.script_pubkey()).drain_wallet();
1627+
let mut psbt = builder.finish().unwrap();
1628+
1629+
wallet
1630+
.sign(
1631+
&mut psbt,
1632+
SignOptions {
1633+
try_finalize: false,
1634+
..Default::default()
1635+
},
1636+
)
1637+
.unwrap();
1638+
1639+
assert!(wallet
1640+
.finalize_psbt(&mut psbt, SignOptions::default())
1641+
.unwrap());
1642+
}
1643+
15291644
#[test]
15301645
fn test_try_finalize_sign_option() {
15311646
let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");

0 commit comments

Comments
 (0)