Skip to content

Commit 6f525d1

Browse files
committed
wip,test: Add test_spend_non_canonical_txout
1 parent 7dcfb2a commit 6f525d1

File tree

3 files changed

+121
-8
lines changed

3 files changed

+121
-8
lines changed

wallet/src/test_utils.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ pub fn insert_checkpoint(wallet: &mut Wallet, block: BlockId) {
318318
}
319319

320320
/// Inserts a transaction with anchor (no last seen). This is useful for adding
321-
/// a coinbase tx to a wallet for testing.
321+
/// a coinbase tx to the wallet for testing.
322322
///
323323
/// This will also insert the anchor `block_id`. See [`insert_anchor`] for more.
324324
pub fn insert_tx_anchor(wallet: &mut Wallet, tx: Transaction, block_id: BlockId) {

wallet/src/wallet/mod.rs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1207,14 +1207,18 @@ impl Wallet {
12071207
/// To iterate over all canonical transactions, including those that are irrelevant, use
12081208
/// [`TxGraph::list_canonical_txs`].
12091209
pub fn transactions<'a>(&'a self) -> impl Iterator<Item = WalletTx<'a>> + 'a {
1210+
self.transactions_with_params(CanonicalizationParams::default())
1211+
}
1212+
1213+
/// Transactions with params.
1214+
pub fn transactions_with_params<'a>(
1215+
&'a self,
1216+
params: CanonicalizationParams,
1217+
) -> impl Iterator<Item = WalletTx<'a>> + 'a {
12101218
let tx_graph = self.indexed_graph.graph();
12111219
let tx_index = &self.indexed_graph.index;
12121220
tx_graph
1213-
.list_canonical_txs(
1214-
&self.chain,
1215-
self.chain.tip().block_id(),
1216-
CanonicalizationParams::default(),
1217-
)
1221+
.list_canonical_txs(&self.chain, self.chain.tip().block_id(), params)
12181222
.filter(|c_tx| tx_index.is_tx_relevant(&c_tx.tx_node.tx))
12191223
}
12201224

@@ -2482,6 +2486,30 @@ impl Wallet {
24822486
Ok(())
24832487
}
24842488

2489+
/// Inserts a transaction into the inner transaction graph with no additional metadata.
2490+
///
2491+
/// This is used to inform the wallet of newly created txs before they are known to exist
2492+
/// on chain (or in mempool), which is useful for discovering wallet-owned outputs of
2493+
/// not-yet-canonical transactions.
2494+
///
2495+
/// The effect of insertion depends on the [relevance] of `tx` as determined by the indexer.
2496+
/// If the transaction was newly inserted and a txout matches a derived SPK, then the index
2497+
/// is updated with the relevant outpoint. This means the output may be selected in
2498+
/// subsequent transactions (if selected manually), enabling chains of dependent spends to
2499+
/// occur prior to broadcast time. If none of the outputs are relevant, the transaction is
2500+
/// kept but the index remains unchanged. If the transaction was already present in-graph,
2501+
/// the effect is a no-op.
2502+
///
2503+
/// **You must persist the change set staged as a result of this call.**
2504+
///
2505+
/// [relevance]: Indexer::is_tx_relevant
2506+
pub fn insert_tx<T>(&mut self, tx: T)
2507+
where
2508+
T: Into<Arc<Transaction>>,
2509+
{
2510+
self.stage.merge(self.indexed_graph.insert_tx(tx).into());
2511+
}
2512+
24852513
/// Apply relevant unconfirmed transactions to the wallet.
24862514
///
24872515
/// Transactions that are not relevant are filtered out.
@@ -2746,8 +2774,12 @@ impl Wallet {
27462774
});
27472775

27482776
// Get wallet txouts.
2777+
let mut canon_params = params.canonical_params.clone();
2778+
canon_params
2779+
.assume_canonical
2780+
.extend(params.utxos.iter().map(|op| op.txid));
27492781
let txouts = self
2750-
.list_indexed_txouts(params.canonical_params.clone())
2782+
.list_indexed_txouts(canon_params)
27512783
.map(|(_, txo)| (txo.outpoint, txo))
27522784
.collect();
27532785

wallet/tests/wallet.rs

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bdk_chain::{BlockId, CanonicalizationParams, ConfirmationBlockTime};
66
use bdk_wallet::coin_selection;
77
use bdk_wallet::descriptor::{calc_checksum, DescriptorError};
88
use bdk_wallet::error::CreateTxError;
9-
use bdk_wallet::psbt::PsbtUtils;
9+
use bdk_wallet::psbt::{self, PsbtUtils};
1010
use bdk_wallet::signer::{SignOptions, SignerError};
1111
use bdk_wallet::test_utils::*;
1212
use bdk_wallet::KeychainKind;
@@ -25,6 +25,87 @@ use rand::SeedableRng;
2525

2626
mod common;
2727

28+
// Test we can select and spend an indexed but not-yet-canonical utxo
29+
#[test]
30+
fn test_spend_non_canonical_txout() -> anyhow::Result<()> {
31+
let (desc, change_desc) = get_test_wpkh_and_change_desc();
32+
let mut wallet = Wallet::create(desc, change_desc)
33+
.network(Network::Regtest)
34+
.create_wallet_no_persist()
35+
.unwrap();
36+
37+
let recip = ScriptBuf::from_hex("0014446906a6560d8ad760db3156706e72e171f3a2aa").unwrap();
38+
39+
// Receive tx0 (coinbase)
40+
let tx = Transaction {
41+
input: vec![TxIn::default()],
42+
output: vec![TxOut {
43+
value: Amount::ONE_BTC,
44+
script_pubkey: wallet
45+
.reveal_next_address(KeychainKind::External)
46+
.script_pubkey(),
47+
}],
48+
..new_tx(1)
49+
};
50+
let block = BlockId {
51+
height: 100,
52+
hash: Hash::hash(b"100"),
53+
};
54+
insert_tx_anchor(&mut wallet, tx, block);
55+
let block = BlockId {
56+
height: 1000,
57+
hash: Hash::hash(b"1000"),
58+
};
59+
insert_checkpoint(&mut wallet, block);
60+
61+
// Create tx1
62+
let mut params = psbt::PsbtParams::default();
63+
params.add_recipients([(recip.clone(), Amount::from_btc(0.01)?)]);
64+
let psbt = wallet.create_psbt(params)?.0;
65+
let txid = psbt.unsigned_tx.compute_txid();
66+
let (vout, _) = psbt
67+
.unsigned_tx
68+
.output
69+
.iter()
70+
.enumerate()
71+
.find(|(_, txo)| wallet.is_mine(txo.script_pubkey.clone()))
72+
.unwrap();
73+
let to_select_op = OutPoint::new(txid, vout as u32);
74+
75+
let txid1 = psbt.unsigned_tx.compute_txid();
76+
wallet.insert_tx(psbt.unsigned_tx);
77+
78+
// Create tx2, spending the change of tx1
79+
let mut params = psbt::PsbtParams::default();
80+
params
81+
.add_utxos(&[to_select_op])
82+
.feerate(FeeRate::from_sat_per_vb_unchecked(10))
83+
.add_recipients([(recip, Amount::from_btc(0.01)?)]);
84+
85+
let psbt = wallet.create_psbt(params)?.0;
86+
87+
assert_eq!(psbt.unsigned_tx.input.len(), 1);
88+
assert_eq!(psbt.unsigned_tx.input[0].previous_output, to_select_op);
89+
90+
let txid2 = psbt.unsigned_tx.compute_txid();
91+
wallet.insert_tx(psbt.unsigned_tx);
92+
93+
// Query the set of unbroadcasted txs
94+
let txs = wallet
95+
.transactions_with_params(CanonicalizationParams {
96+
assume_canonical: vec![txid2],
97+
})
98+
.filter(|c| c.chain_position.is_unconfirmed())
99+
.collect::<Vec<_>>();
100+
101+
assert_eq!(txs.len(), 2);
102+
103+
assert!(txs.iter().any(|c| c.tx_node.txid == txid1));
104+
assert!(txs.iter().any(|c| c.tx_node.txid == txid2));
105+
106+
Ok(())
107+
}
108+
28109
#[test]
29110
fn test_error_external_and_internal_are_the_same() {
30111
// identical descriptors should fail to create wallet

0 commit comments

Comments
 (0)