Skip to content

Commit e53d849

Browse files
committed
feat: expose detailed finalize_psbt outcomes
1 parent fb7681a commit e53d849

2 files changed

Lines changed: 161 additions & 9 deletions

File tree

src/wallet/mod.rs

Lines changed: 65 additions & 8 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
///
@@ -1796,7 +1845,7 @@ impl Wallet {
17961845

17971846
// Attempt to finalize.
17981847
if sign_options.try_finalize {
1799-
self.finalize_psbt(psbt, sign_options)
1848+
Ok(self.finalize_psbt(psbt, sign_options)?.is_finalized())
18001849
} else {
18011850
Ok(false)
18021851
}
@@ -1834,14 +1883,15 @@ impl Wallet {
18341883
/// and [BIP371](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki)
18351884
/// for further information.
18361885
///
1837-
/// Returns `true` if the PSBT could be finalized, and `false` otherwise.
1886+
/// Returns per-input finalization results. Use `FinalizePsbtResult::is_finalized` to check
1887+
/// whether every input is finalized after this call.
18381888
///
18391889
/// The [`SignOptions`] can be used to tweak the behavior of the finalizer.
18401890
pub fn finalize_psbt(
18411891
&self,
18421892
psbt: &mut Psbt,
18431893
sign_options: SignOptions,
1844-
) -> Result<bool, SignerError> {
1894+
) -> Result<FinalizePsbtResult, SignerError> {
18451895
let tx = &psbt.unsigned_tx;
18461896
let chain_tip = self.chain.tip().block_id();
18471897
let prev_txids = tx
@@ -1867,14 +1917,15 @@ impl Wallet {
18671917
})
18681918
.collect::<HashMap<Txid, u32>>();
18691919

1870-
let mut finished = true;
1920+
let mut results = BTreeMap::new();
18711921

18721922
for (n, input) in tx.input.iter().enumerate() {
18731923
let psbt_input = &psbt
18741924
.inputs
18751925
.get(n)
18761926
.ok_or(IndexOutOfBoundsError::new(n, psbt.inputs.len()))?;
18771927
if psbt_input.final_script_sig.is_some() || psbt_input.final_script_witness.is_some() {
1928+
results.insert(n, FinalizePsbtInputResult::AlreadyFinalized);
18781929
continue;
18791930
}
18801931
let confirmation_height = confirmation_heights
@@ -1927,23 +1978,29 @@ impl Wallet {
19271978
if !tmp_input.witness.is_empty() {
19281979
psbt_input.final_script_witness = Some(tmp_input.witness);
19291980
}
1981+
results.insert(n, FinalizePsbtInputResult::Finalized);
1982+
}
1983+
Err(_) => {
1984+
results.insert(n, FinalizePsbtInputResult::CouldNotSatisfy);
19301985
}
1931-
Err(_) => finished = false,
19321986
}
19331987
}
1934-
None => finished = false,
1988+
None => {
1989+
results.insert(n, FinalizePsbtInputResult::MissingDescriptor);
1990+
}
19351991
}
19361992
}
19371993

19381994
// Clear derivation paths from outputs.
1939-
if finished {
1995+
let finalized = FinalizePsbtResult::new(results);
1996+
if finalized.is_finalized() {
19401997
for output in &mut psbt.outputs {
19411998
output.bip32_derivation.clear();
19421999
output.tap_key_origins.clear();
19432000
}
19442001
}
19452002

1946-
Ok(finished)
2003+
Ok(finalized)
19472004
}
19482005

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

tests/wallet.rs

Lines changed: 96 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,99 @@ 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 result = wallet
1568+
.finalize_psbt(&mut psbt, SignOptions::default())
1569+
.unwrap();
1570+
1571+
assert!(!result.is_finalized());
1572+
assert_eq!(
1573+
result.results().get(&0),
1574+
Some(&FinalizePsbtInputResult::Finalized)
1575+
);
1576+
assert_eq!(
1577+
result.results().get(&1),
1578+
Some(&FinalizePsbtInputResult::MissingDescriptor)
1579+
);
1580+
assert!(psbt.inputs[0].final_script_witness.is_some());
1581+
}
1582+
1583+
#[test]
1584+
fn test_finalize_psbt_reports_could_not_satisfy() {
1585+
let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
1586+
let addr = wallet.next_unused_address(KeychainKind::External);
1587+
let mut builder = wallet.build_tx();
1588+
builder.drain_to(addr.script_pubkey()).drain_wallet();
1589+
let mut psbt = builder.finish().unwrap();
1590+
1591+
let result = wallet
1592+
.finalize_psbt(&mut psbt, SignOptions::default())
1593+
.unwrap();
1594+
1595+
assert!(!result.is_finalized());
1596+
assert_eq!(
1597+
result.results().get(&0),
1598+
Some(&FinalizePsbtInputResult::CouldNotSatisfy)
1599+
);
1600+
assert!(psbt.inputs[0].final_script_witness.is_none());
1601+
}
1602+
1603+
#[test]
1604+
fn test_finalize_psbt_reports_already_finalized_inputs() {
1605+
let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
1606+
let addr = wallet.next_unused_address(KeychainKind::External);
1607+
let mut builder = wallet.build_tx();
1608+
builder.drain_to(addr.script_pubkey()).drain_wallet();
1609+
let mut psbt = builder.finish().unwrap();
1610+
1611+
assert!(wallet.sign(&mut psbt, SignOptions::default()).unwrap());
1612+
1613+
let result = wallet
1614+
.finalize_psbt(&mut psbt, SignOptions::default())
1615+
.unwrap();
1616+
1617+
assert!(result.is_finalized());
1618+
assert_eq!(
1619+
result.results().get(&0),
1620+
Some(&FinalizePsbtInputResult::AlreadyFinalized)
1621+
);
1622+
}
1623+
15291624
#[test]
15301625
fn test_try_finalize_sign_option() {
15311626
let (mut wallet, _) = get_funded_wallet_single("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");

0 commit comments

Comments
 (0)