Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wallet sync_request and full_scan_request functions #1194

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ Cargo.lock

# Example persisted files.
*.db
bdk_wallet_esplora_async_example.dat
bdk_wallet_esplora_blocking_example.dat
2 changes: 0 additions & 2 deletions crates/bdk/src/wallet/coin_selection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,6 @@ impl CoinSelectionResult {
pub trait CoinSelectionAlgorithm: core::fmt::Debug {
/// Perform the coin selection
///
/// - `database`: a reference to the wallet's database that can be used to lookup additional
/// details for a specific UTXO
/// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their
/// weight cost
/// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their
Expand Down
6 changes: 2 additions & 4 deletions crates/bdk/src/wallet/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ impl std::error::Error for MiniscriptPsbtError {}
#[derive(Debug)]
/// Error returned from [`TxBuilder::finish`]
///
/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
/// [`TxBuilder::finish`]: super::tx_builder::TxBuilder::finish
pub enum CreateTxError<P> {
/// There was a problem with the descriptors passed in
Descriptor(DescriptorError),
Expand Down Expand Up @@ -248,9 +248,7 @@ impl<P> From<coin_selection::Error> for CreateTxError<P> {
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for CreateTxError<P> {}

#[derive(Debug)]
/// Error returned from [`Wallet::build_fee_bump`]
///
/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump
/// Error returned from [`crate::Wallet::build_fee_bump`]
pub enum BuildFeeBumpError {
/// Happens when trying to spend an UTXO that is not in the internal database
UnknownUtxo(OutPoint),
Expand Down
62 changes: 43 additions & 19 deletions crates/bdk/src/wallet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use alloc::{
vec::Vec,
};
pub use bdk_chain::keychain::Balance;
use bdk_chain::spk_client::{FullScanRequest, SyncRequest};
use bdk_chain::{
indexed_tx_graph,
keychain::{self, KeychainTxOutIndex},
Expand All @@ -28,7 +29,7 @@ use bdk_chain::{
},
tx_graph::{CanonicalTx, TxGraph},
Append, BlockId, ChainPosition, ConfirmationTime, ConfirmationTimeHeightAnchor, FullTxOut,
IndexedTxGraph, Persist, PersistBackend,
IndexedTxGraph, Persist, PersistBackend, SpkIterator,
};
use bitcoin::secp256k1::{All, Secp256k1};
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
Expand All @@ -42,6 +43,7 @@ use core::fmt;
use core::ops::Deref;
use descriptor::error::Error as DescriptorError;
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
use miniscript::{Descriptor, DescriptorPublicKey};

use bdk_chain::tx_graph::CalculateFeeError;

Expand Down Expand Up @@ -942,7 +944,7 @@ impl<D> Wallet<D> {
/// # let mut wallet: Wallet<()> = todo!();
/// # let txid:Txid = todo!();
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
/// let fee = wallet.calculate_fee(tx).expect("fee");
/// let fee = wallet.calculate_fee(&tx).expect("fee");
/// ```
///
/// ```rust, no_run
Expand Down Expand Up @@ -973,16 +975,16 @@ impl<D> Wallet<D> {
/// # let mut wallet: Wallet<()> = todo!();
/// # let txid:Txid = todo!();
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
/// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate");
/// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate");
/// ```
///
/// ```rust, no_run
/// # use bitcoin::psbt::PartiallySignedTransaction;
/// # use bdk::Wallet;
/// # let mut wallet: Wallet<()> = todo!();
/// # let mut psbt: PartiallySignedTransaction = todo!();
/// let tx = &psbt.clone().extract_tx();
/// let fee_rate = wallet.calculate_fee_rate(tx).expect("fee rate");
/// let tx = psbt.clone().extract_tx();
/// let fee_rate = wallet.calculate_fee_rate(&tx).expect("fee rate");
/// ```
/// [`insert_txout`]: Self::insert_txout
pub fn calculate_fee_rate(&self, tx: &Transaction) -> Result<FeeRate, CalculateFeeError> {
Expand All @@ -1003,8 +1005,8 @@ impl<D> Wallet<D> {
/// # use bdk::Wallet;
/// # let mut wallet: Wallet<()> = todo!();
/// # let txid:Txid = todo!();
/// let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
/// let (sent, received) = wallet.sent_and_received(tx);
/// let tx = wallet.get_tx(txid).expect("tx exists").tx_node.tx;
/// let (sent, received) = wallet.sent_and_received(&tx);
/// ```
///
/// ```rust, no_run
Expand Down Expand Up @@ -1065,7 +1067,7 @@ impl<D> Wallet<D> {
pub fn get_tx(
&self,
txid: Txid,
) -> Option<CanonicalTx<'_, Transaction, ConfirmationTimeHeightAnchor>> {
) -> Option<CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> {
let graph = self.indexed_graph.graph();

Some(CanonicalTx {
Expand Down Expand Up @@ -1128,18 +1130,13 @@ impl<D> Wallet<D> {
// anchor tx to checkpoint with lowest height that is >= position's height
let anchor = self
.chain
.blocks()
.range(height..)
.next()
.query_from(height)
.ok_or(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
tip_height: self.chain.tip().height(),
tx_height: height,
})
.map(|(&anchor_height, &hash)| ConfirmationTimeHeightAnchor {
anchor_block: BlockId {
height: anchor_height,
hash,
},
.map(|anchor_cp| ConfirmationTimeHeightAnchor {
anchor_block: anchor_cp.block_id(),
confirmation_height: height,
confirmation_time: time,
})?;
Expand Down Expand Up @@ -1167,7 +1164,8 @@ impl<D> Wallet<D> {
/// Iterate over the transactions in the wallet.
pub fn transactions(
&self,
) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeHeightAnchor>> + '_ {
) -> impl Iterator<Item = CanonicalTx<'_, Arc<Transaction>, ConfirmationTimeHeightAnchor>> + '_
{
self.indexed_graph
.graph()
.list_chain_txs(&self.chain, self.chain.tip().block_id())
Expand Down Expand Up @@ -1670,6 +1668,7 @@ impl<D> Wallet<D> {
let mut tx = graph
.get_tx(txid)
.ok_or(BuildFeeBumpError::TransactionNotFound(txid))?
.as_ref()
.clone();

let pos = graph
Expand Down Expand Up @@ -1739,7 +1738,7 @@ impl<D> Wallet<D> {
sequence: Some(txin.sequence),
psbt_input: Box::new(psbt::Input {
witness_utxo: Some(txout.clone()),
non_witness_utxo: Some(prev_tx.clone()),
non_witness_utxo: Some(prev_tx.as_ref().clone()),
..Default::default()
}),
},
Expand Down Expand Up @@ -2295,7 +2294,7 @@ impl<D> Wallet<D> {
psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
}
if !desc.is_taproot() && (!desc.is_witness() || !only_witness_utxo) {
psbt_input.non_witness_utxo = Some(prev_tx.clone());
psbt_input.non_witness_utxo = Some(prev_tx.as_ref().clone());
}
}
Ok(psbt_input)
Expand Down Expand Up @@ -2500,6 +2499,31 @@ impl<D> Wallet<D> {
.batch_insert_relevant_unconfirmed(unconfirmed_txs);
self.persist.stage(ChangeSet::from(indexed_graph_changeset));
}

/// Create a [`SyncRequest`] for this wallet for all revealed spks.
///
/// This is the first step when performing a spk-based wallet sync, the returned [`SyncRequest`] collects
/// all revealed script pub keys from the wallet keychain needed to start a blockchain sync with a spk based
/// blockchain client.
pub fn sync_revealed_spks_request(&self) -> SyncRequest {
let chain_tip = self.local_chain().tip();
self.spk_index().sync_revealed_spks_request(chain_tip)
}

/// Create a [`FullScanRequest] for this wallet.
///
/// This is the first step when performing a spk-based wallet full scan, the returned [`FullScanRequest]
/// collects iterators for the wallet's keychain script pub keys needed to start a blockchain full scan
/// with a spk based blockchain client.
///
/// This operation is generally only used when importing or restoring a previously used wallet
/// in which the list of used scripts is not known.
pub fn full_scan_request(
&self,
) -> FullScanRequest<KeychainKind, SpkIterator<Descriptor<DescriptorPublicKey>>> {
let chain_tip = self.local_chain().tip();
self.spk_index().full_scan_request(chain_tip)
}
}

impl<D> AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeHeightAnchor>> for Wallet<D> {
Expand Down
19 changes: 11 additions & 8 deletions crates/bdk/tests/wallet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,12 +208,12 @@ fn test_get_funded_wallet_sent_and_received() {

let mut tx_amounts: Vec<(Txid, (u64, u64))> = wallet
.transactions()
.map(|ct| (ct.tx_node.txid, wallet.sent_and_received(ct.tx_node.tx)))
.map(|ct| (ct.tx_node.txid, wallet.sent_and_received(&ct.tx_node)))
.collect();
tx_amounts.sort_by(|a1, a2| a1.0.cmp(&a2.0));

let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
let (sent, received) = wallet.sent_and_received(tx);
let (sent, received) = wallet.sent_and_received(&tx);

// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
Expand All @@ -227,7 +227,7 @@ fn test_get_funded_wallet_tx_fees() {
let (wallet, txid) = get_funded_wallet(get_test_wpkh());

let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
let tx_fee = wallet.calculate_fee(tx).expect("transaction fee");
let tx_fee = wallet.calculate_fee(&tx).expect("transaction fee");

// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
Expand All @@ -240,7 +240,9 @@ fn test_get_funded_wallet_tx_fee_rate() {
let (wallet, txid) = get_funded_wallet(get_test_wpkh());

let tx = wallet.get_tx(txid).expect("transaction").tx_node.tx;
let tx_fee_rate = wallet.calculate_fee_rate(tx).expect("transaction fee rate");
let tx_fee_rate = wallet
.calculate_fee_rate(&tx)
.expect("transaction fee rate");

// The funded wallet contains a tx with a 76_000 sats input and two outputs, one spending 25_000
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
Expand Down Expand Up @@ -1307,7 +1309,7 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
.add_foreign_utxo(
utxo2.outpoint,
psbt::Input {
non_witness_utxo: Some(tx1),
non_witness_utxo: Some(tx1.as_ref().clone()),
..Default::default()
},
satisfaction_weight
Expand All @@ -1320,7 +1322,7 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
.add_foreign_utxo(
utxo2.outpoint,
psbt::Input {
non_witness_utxo: Some(tx2),
non_witness_utxo: Some(tx2.as_ref().clone()),
..Default::default()
},
satisfaction_weight
Expand Down Expand Up @@ -1384,7 +1386,7 @@ fn test_add_foreign_utxo_only_witness_utxo() {
let mut builder = builder.clone();
let tx2 = wallet2.get_tx(txid2).unwrap().tx_node.tx;
let psbt_input = psbt::Input {
non_witness_utxo: Some(tx2.clone()),
non_witness_utxo: Some(tx2.as_ref().clone()),
..Default::default()
};
builder
Expand Down Expand Up @@ -3050,7 +3052,8 @@ fn test_taproot_sign_using_non_witness_utxo() {
let mut psbt = builder.finish().unwrap();

psbt.inputs[0].witness_utxo = None;
psbt.inputs[0].non_witness_utxo = Some(wallet.get_tx(prev_txid).unwrap().tx_node.tx.clone());
psbt.inputs[0].non_witness_utxo =
Some(wallet.get_tx(prev_txid).unwrap().tx_node.as_ref().clone());
assert!(
psbt.inputs[0].non_witness_utxo.is_some(),
"Previous tx should be present in the database"
Expand Down
18 changes: 12 additions & 6 deletions crates/bitcoind_rpc/tests/test_emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,15 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
}

assert_eq!(
local_chain.blocks(),
&exp_hashes
local_chain
.iter_checkpoints()
.map(|cp| (cp.height(), cp.hash()))
.collect::<BTreeSet<_>>(),
exp_hashes
.iter()
.enumerate()
.map(|(i, hash)| (i as u32, *hash))
.collect(),
.collect::<BTreeSet<_>>(),
"final local_chain state is unexpected",
);

Expand Down Expand Up @@ -110,12 +113,15 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
}

assert_eq!(
local_chain.blocks(),
&exp_hashes
local_chain
.iter_checkpoints()
.map(|cp| (cp.height(), cp.hash()))
.collect::<BTreeSet<_>>(),
exp_hashes
.iter()
.enumerate()
.map(|(i, hash)| (i as u32, *hash))
.collect(),
.collect::<BTreeSet<_>>(),
"final local_chain state is unexpected after reorg",
);

Expand Down
2 changes: 1 addition & 1 deletion crates/chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ readme = "README.md"
[dependencies]
# For no-std, remember to enable the bitcoin/no-std feature
bitcoin = { version = "0.30.0", default-features = false }
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive", "rc"] }

# Use hashbrown as a feature flag to have HashSet and HashMap from it.
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
Expand Down
Loading