[wallet_redesign] Update Wallet with redesigned structures

This commit is contained in:
志宇 2023-05-09 21:49:33 +08:00
parent 8641847e6c
commit e69fccb15f
No known key found for this signature in database
GPG Key ID: F6345C9837C2BDE8
9 changed files with 757 additions and 528 deletions

View File

@ -56,7 +56,6 @@
use core::str::FromStr; use core::str::FromStr;
use alloc::string::{String, ToString}; use alloc::string::{String, ToString};
use bdk_chain::sparse_chain::ChainPosition;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use miniscript::descriptor::{ShInner, WshInner}; use miniscript::descriptor::{ShInner, WshInner};
@ -130,8 +129,10 @@ impl FullyNodedExport {
wallet wallet
.transactions() .transactions()
.next() .next()
.and_then(|(pos, _)| pos.height().into()) .map_or(0, |canonical_tx| match canonical_tx.observed_as {
.unwrap_or(0) bdk_chain::ObservedAs::Confirmed(a) => a.confirmation_height,
bdk_chain::ObservedAs::Unconfirmed(_) => 0,
})
} else { } else {
0 0
}; };
@ -246,6 +247,7 @@ mod test {
height: 5000, height: 5000,
time: 0, time: 0,
}, },
None,
) )
.unwrap(); .unwrap();
wallet wallet

View File

@ -21,9 +21,12 @@ use alloc::{
}; };
pub use bdk_chain::keychain::Balance; pub use bdk_chain::keychain::Balance;
use bdk_chain::{ use bdk_chain::{
chain_graph, indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker}, keychain::{DerivationAdditions, KeychainTxOutIndex},
sparse_chain, BlockId, ConfirmationTime, local_chain::{self, LocalChain, UpdateNotConnectedError},
tx_graph::{CanonicalTx, TxGraph},
Anchor, Append, BlockId, ConfirmationTime, ConfirmationTimeAnchor, FullTxOut, ObservedAs,
Persist, PersistBackend,
}; };
use bitcoin::consensus::encode::serialize; use bitcoin::consensus::encode::serialize;
use bitcoin::secp256k1::Secp256k1; use bitcoin::secp256k1::Secp256k1;
@ -83,19 +86,83 @@ const COINBASE_MATURITY: u32 = 100;
pub struct Wallet<D = ()> { pub struct Wallet<D = ()> {
signers: Arc<SignersContainer>, signers: Arc<SignersContainer>,
change_signers: Arc<SignersContainer>, change_signers: Arc<SignersContainer>,
keychain_tracker: KeychainTracker<KeychainKind, ConfirmationTime>, chain: LocalChain,
persist: persist::Persist<KeychainKind, ConfirmationTime, D>, indexed_graph: IndexedTxGraph<ConfirmationTimeAnchor, KeychainTxOutIndex<KeychainKind>>,
persist: Persist<D, ChangeSet>, // [TODO] Use a different `ChangeSet`
network: Network, network: Network,
secp: SecpCtx, secp: SecpCtx,
} }
/// The update to a [`Wallet`] used in [`Wallet::apply_update`]. This is usually returned from blockchain data sources. /// The update to a [`Wallet`] used in [`Wallet::apply_update`]. This is usually returned from blockchain data sources.
/// The type parameter `T` indicates the kind of transaction contained in the update. It's usually a [`bitcoin::Transaction`]. /// The type parameter `T` indicates the kind of transaction contained in the update. It's usually a [`bitcoin::Transaction`].
pub type Update = KeychainScan<KeychainKind, ConfirmationTime>; #[derive(Debug, Default, PartialEq)]
/// Error indicating that something was wrong with an [`Update<T>`]. pub struct Update<K = KeychainKind, A = ConfirmationTimeAnchor> {
pub type UpdateError = chain_graph::UpdateError<ConfirmationTime>; keychain: BTreeMap<K, u32>,
graph: TxGraph<A>,
chain: LocalChain,
}
/// The changeset produced internally by applying an update /// The changeset produced internally by applying an update
pub(crate) type ChangeSet = KeychainChangeSet<KeychainKind, ConfirmationTime>; #[derive(Debug, PartialEq, serde::Deserialize, serde::Serialize)]
#[serde(bound(
deserialize = "A: Ord + serde::Deserialize<'de>, K: Ord + serde::Deserialize<'de>",
serialize = "A: Ord + serde::Serialize, K: Ord + serde::Serialize"
))]
// #[cfg_attr(predicate, attr)]
pub struct ChangeSet<K = KeychainKind, A = ConfirmationTimeAnchor> {
pub chain_changeset: local_chain::ChangeSet,
pub indexed_additions: IndexedAdditions<A, DerivationAdditions<K>>,
}
impl<K, A> Default for ChangeSet<K, A> {
fn default() -> Self {
Self {
chain_changeset: Default::default(),
indexed_additions: Default::default(),
}
}
}
impl<K: Ord, A: Anchor> Append for ChangeSet<K, A> {
fn append(&mut self, other: Self) {
Append::append(&mut self.chain_changeset, other.chain_changeset);
Append::append(&mut self.indexed_additions, other.indexed_additions);
}
fn is_empty(&self) -> bool {
self.chain_changeset.is_empty() && self.indexed_additions.is_empty()
}
}
impl<K, A> From<IndexedAdditions<A, DerivationAdditions<K>>> for ChangeSet<K, A> {
fn from(indexed_additions: IndexedAdditions<A, DerivationAdditions<K>>) -> Self {
Self {
indexed_additions,
..Default::default()
}
}
}
impl<K, A> From<DerivationAdditions<K>> for ChangeSet<K, A> {
fn from(index_additions: DerivationAdditions<K>) -> Self {
Self {
indexed_additions: IndexedAdditions {
index_additions,
..Default::default()
},
..Default::default()
}
}
}
impl<K, A> From<local_chain::ChangeSet> for ChangeSet<K, A> {
fn from(chain_changeset: local_chain::ChangeSet) -> Self {
Self {
chain_changeset,
..Default::default()
}
}
}
/// The address index selection strategy to use to derived an address from the wallet's external /// The address index selection strategy to use to derived an address from the wallet's external
/// descriptor. See [`Wallet::get_address`]. If you're unsure which one to use use `WalletIndex::New`. /// descriptor. See [`Wallet::get_address`]. If you're unsure which one to use use `WalletIndex::New`.
@ -182,6 +249,14 @@ where
} }
} }
#[derive(Debug)]
pub enum InsertTxError {
ConfirmationHeightCannotBeGreaterThanTip {
tip_height: Option<u32>,
tx_height: u32,
},
}
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for NewError<P> {} impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for NewError<P> {}
@ -195,15 +270,17 @@ impl<D> Wallet<D> {
network: Network, network: Network,
) -> Result<Self, NewError<D::LoadError>> ) -> Result<Self, NewError<D::LoadError>>
where where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>, D: PersistBackend<ChangeSet>,
{ {
let secp = Secp256k1::new(); let secp = Secp256k1::new();
let mut chain = LocalChain::default();
let mut indexed_graph =
IndexedTxGraph::<ConfirmationTimeAnchor, KeychainTxOutIndex<KeychainKind>>::default();
let mut keychain_tracker = KeychainTracker::default();
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network) let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)
.map_err(NewError::Descriptor)?; .map_err(NewError::Descriptor)?;
keychain_tracker indexed_graph
.txout_index .index
.add_keychain(KeychainKind::External, descriptor.clone()); .add_keychain(KeychainKind::External, descriptor.clone());
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp)); let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp));
let change_signers = match change_descriptor { let change_signers = match change_descriptor {
@ -218,8 +295,8 @@ impl<D> Wallet<D> {
&secp, &secp,
)); ));
keychain_tracker indexed_graph
.txout_index .index
.add_keychain(KeychainKind::Internal, change_descriptor); .add_keychain(KeychainKind::Internal, change_descriptor);
change_signers change_signers
@ -227,18 +304,20 @@ impl<D> Wallet<D> {
None => Arc::new(SignersContainer::new()), None => Arc::new(SignersContainer::new()),
}; };
db.load_into_keychain_tracker(&mut keychain_tracker) let changeset = db.load_from_persistence().map_err(NewError::Persist)?;
.map_err(NewError::Persist)?; chain.apply_changeset(changeset.chain_changeset);
indexed_graph.apply_additions(changeset.indexed_additions);
let persist = persist::Persist::new(db); let persist = Persist::new(db);
Ok(Wallet { Ok(Wallet {
signers, signers,
change_signers, change_signers,
network, network,
chain,
indexed_graph,
persist, persist,
secp, secp,
keychain_tracker,
}) })
} }
@ -249,7 +328,7 @@ impl<D> Wallet<D> {
/// Iterator over all keychains in this wallet /// Iterator over all keychains in this wallet
pub fn keychains(&self) -> &BTreeMap<KeychainKind, ExtendedDescriptor> { pub fn keychains(&self) -> &BTreeMap<KeychainKind, ExtendedDescriptor> {
self.keychain_tracker.txout_index.keychains() self.indexed_graph.index.keychains()
} }
/// Return a derived address using the external descriptor, see [`AddressIndex`] for /// Return a derived address using the external descriptor, see [`AddressIndex`] for
@ -257,7 +336,7 @@ impl<D> Wallet<D> {
/// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`]. /// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`].
pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo
where where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>, D: PersistBackend<ChangeSet>,
{ {
self._get_address(address_index, KeychainKind::External) self._get_address(address_index, KeychainKind::External)
} }
@ -271,17 +350,17 @@ impl<D> Wallet<D> {
/// be returned for any [`AddressIndex`]. /// be returned for any [`AddressIndex`].
pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo
where where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>, D: PersistBackend<ChangeSet>,
{ {
self._get_address(address_index, KeychainKind::Internal) self._get_address(address_index, KeychainKind::Internal)
} }
fn _get_address(&mut self, address_index: AddressIndex, keychain: KeychainKind) -> AddressInfo fn _get_address(&mut self, address_index: AddressIndex, keychain: KeychainKind) -> AddressInfo
where where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>, D: PersistBackend<ChangeSet>,
{ {
let keychain = self.map_keychain(keychain); let keychain = self.map_keychain(keychain);
let txout_index = &mut self.keychain_tracker.txout_index; let txout_index = &mut self.indexed_graph.index;
let (index, spk) = match address_index { let (index, spk) = match address_index {
AddressIndex::New => { AddressIndex::New => {
let ((index, spk), changeset) = txout_index.reveal_next_spk(&keychain); let ((index, spk), changeset) = txout_index.reveal_next_spk(&keychain);
@ -320,42 +399,36 @@ impl<D> Wallet<D> {
/// Return whether or not a `script` is part of this wallet (either internal or external) /// Return whether or not a `script` is part of this wallet (either internal or external)
pub fn is_mine(&self, script: &Script) -> bool { pub fn is_mine(&self, script: &Script) -> bool {
self.keychain_tracker self.indexed_graph.index.index_of_spk(script).is_some()
.txout_index
.index_of_spk(script)
.is_some()
} }
/// Finds how the wallet derived the script pubkey `spk`. /// Finds how the wallet derived the script pubkey `spk`.
/// ///
/// Will only return `Some(_)` if the wallet has given out the spk. /// Will only return `Some(_)` if the wallet has given out the spk.
pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> { pub fn derivation_of_spk(&self, spk: &Script) -> Option<(KeychainKind, u32)> {
self.keychain_tracker.txout_index.index_of_spk(spk).copied() self.indexed_graph.index.index_of_spk(spk).copied()
} }
/// Return the list of unspent outputs of this wallet /// Return the list of unspent outputs of this wallet
pub fn list_unspent(&self) -> Vec<LocalUtxo> { pub fn list_unspent(&self) -> impl Iterator<Item = LocalUtxo> + '_ {
self.keychain_tracker self.indexed_graph
.full_utxos() .graph()
.map(|(&(keychain, derivation_index), utxo)| LocalUtxo { .filter_chain_unspents(
outpoint: utxo.outpoint, &self.chain,
txout: utxo.txout, self.chain.tip().unwrap_or_default(),
keychain, self.indexed_graph.index.outpoints().iter().cloned(),
is_spent: false, )
derivation_index, .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
confirmation_time: utxo.chain_position,
})
.collect()
} }
/// Get all the checkpoints the wallet is currently storing indexed by height. /// Get all the checkpoints the wallet is currently storing indexed by height.
pub fn checkpoints(&self) -> &BTreeMap<u32, BlockHash> { pub fn checkpoints(&self) -> &BTreeMap<u32, BlockHash> {
self.keychain_tracker.chain().checkpoints() self.chain.blocks()
} }
/// Returns the latest checkpoint. /// Returns the latest checkpoint.
pub fn latest_checkpoint(&self) -> Option<BlockId> { pub fn latest_checkpoint(&self) -> Option<BlockId> {
self.keychain_tracker.chain().latest_checkpoint() self.chain.tip()
} }
/// Returns a iterators of all the script pubkeys for the `Internal` and External` variants in `KeychainKind`. /// Returns a iterators of all the script pubkeys for the `Internal` and External` variants in `KeychainKind`.
@ -369,7 +442,7 @@ impl<D> Wallet<D> {
pub fn spks_of_all_keychains( pub fn spks_of_all_keychains(
&self, &self,
) -> BTreeMap<KeychainKind, impl Iterator<Item = (u32, Script)> + Clone> { ) -> BTreeMap<KeychainKind, impl Iterator<Item = (u32, Script)> + Clone> {
self.keychain_tracker.txout_index.spks_of_all_keychains() self.indexed_graph.index.spks_of_all_keychains()
} }
/// Gets an iterator over all the script pubkeys in a single keychain. /// Gets an iterator over all the script pubkeys in a single keychain.
@ -381,30 +454,22 @@ impl<D> Wallet<D> {
&self, &self,
keychain: KeychainKind, keychain: KeychainKind,
) -> impl Iterator<Item = (u32, Script)> + Clone { ) -> impl Iterator<Item = (u32, Script)> + Clone {
self.keychain_tracker self.indexed_graph.index.spks_of_keychain(&keychain)
.txout_index
.spks_of_keychain(&keychain)
} }
/// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the /// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the
/// wallet's database. /// wallet's database.
pub fn get_utxo(&self, op: OutPoint) -> Option<LocalUtxo> { pub fn get_utxo(&self, op: OutPoint) -> Option<LocalUtxo> {
self.keychain_tracker let (&spk_i, _) = self.indexed_graph.index.txout(op)?;
.full_utxos() self.indexed_graph
.find_map(|(&(keychain, derivation_index), txo)| { .graph()
if op == txo.outpoint { .filter_chain_unspents(
Some(LocalUtxo { &self.chain,
outpoint: txo.outpoint, self.chain.tip().unwrap_or_default(),
txout: txo.txout, core::iter::once((spk_i, op)),
keychain, )
is_spent: txo.spent_by.is_none(), .map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
derivation_index, .next()
confirmation_time: txo.chain_position,
})
} else {
None
}
})
} }
/// Return a single transactions made and received by the wallet /// Return a single transactions made and received by the wallet
@ -412,54 +477,22 @@ impl<D> Wallet<D> {
/// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if /// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if
/// `include_raw` is `true`. /// `include_raw` is `true`.
pub fn get_tx(&self, txid: Txid, include_raw: bool) -> Option<TransactionDetails> { pub fn get_tx(&self, txid: Txid, include_raw: bool) -> Option<TransactionDetails> {
let (&confirmation_time, tx) = self.keychain_tracker.chain_graph().get_tx_in_chain(txid)?; let graph = self.indexed_graph.graph();
let graph = self.keychain_tracker.graph();
let txout_index = &self.keychain_tracker.txout_index;
let received = tx let canonical_tx = CanonicalTx {
.output observed_as: graph.get_chain_position(
.iter() &self.chain,
.map(|txout| { self.chain.tip().unwrap_or_default(),
if txout_index.index_of_spk(&txout.script_pubkey).is_some() { txid,
txout.value )?,
} else { node: graph.get_tx_node(txid)?,
0 };
}
})
.sum();
let sent = tx Some(new_tx_details(
.input &self.indexed_graph,
.iter() canonical_tx,
.map(|txin| { include_raw,
if let Some((_, txout)) = txout_index.txout(txin.previous_output) { ))
txout.value
} else {
0
}
})
.sum();
let inputs = tx
.input
.iter()
.map(|txin| {
graph
.get_txout(txin.previous_output)
.map(|txout| txout.value)
})
.sum::<Option<u64>>();
let outputs = tx.output.iter().map(|txout| txout.value).sum();
let fee = inputs.map(|inputs| inputs.saturating_sub(outputs));
Some(TransactionDetails {
transaction: if include_raw { Some(tx.clone()) } else { None },
txid,
received,
sent,
fee,
confirmation_time,
})
} }
/// Add a new checkpoint to the wallet's internal view of the chain. /// Add a new checkpoint to the wallet's internal view of the chain.
@ -472,10 +505,15 @@ impl<D> Wallet<D> {
pub fn insert_checkpoint( pub fn insert_checkpoint(
&mut self, &mut self,
block_id: BlockId, block_id: BlockId,
) -> Result<bool, sparse_chain::InsertCheckpointError> { ) -> Result<bool, local_chain::InsertBlockNotMatchingError>
let changeset = self.keychain_tracker.insert_checkpoint(block_id)?; where
let changed = changeset.is_empty(); D: PersistBackend<ChangeSet>,
self.persist.stage(changeset); {
let changeset = self.chain.insert_block(block_id)?;
let changed = !changeset.is_empty();
if changed {
self.persist.stage(changeset.into());
}
Ok(changed) Ok(changed)
} }
@ -497,41 +535,80 @@ impl<D> Wallet<D> {
&mut self, &mut self,
tx: Transaction, tx: Transaction,
position: ConfirmationTime, position: ConfirmationTime,
) -> Result<bool, chain_graph::InsertTxError<ConfirmationTime>> { seen_at: Option<u64>,
let changeset = self.keychain_tracker.insert_tx(tx, position)?; ) -> Result<bool, InsertTxError>
let changed = changeset.is_empty(); where
self.persist.stage(changeset); D: PersistBackend<ChangeSet>,
{
let tip = self.chain.tip();
if let ConfirmationTime::Confirmed { height, .. } = position {
let tip_height = tip.map(|b| b.height);
if Some(height) > tip_height {
return Err(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
tip_height,
tx_height: height,
});
}
}
let anchor = match position {
ConfirmationTime::Confirmed { height, time } => {
let tip_height = tip.map(|b| b.height);
if Some(height) > tip_height {
return Err(InsertTxError::ConfirmationHeightCannotBeGreaterThanTip {
tip_height,
tx_height: height,
});
}
Some(ConfirmationTimeAnchor {
anchor_block: tip.expect("already checked if tip_height > height"),
confirmation_height: height,
confirmation_time: time,
})
}
ConfirmationTime::Unconfirmed => None,
};
let changeset: ChangeSet = self.indexed_graph.insert_tx(&tx, anchor, seen_at).into();
let changed = !changeset.is_empty();
if changed {
self.persist.stage(changeset);
}
Ok(changed) Ok(changed)
} }
#[deprecated(note = "use Wallet::transactions instead")] #[deprecated(note = "use Wallet::transactions instead")]
/// Deprecated. use `Wallet::transactions` instead. /// Deprecated. use `Wallet::transactions` instead.
pub fn list_transactions(&self, include_raw: bool) -> Vec<TransactionDetails> { pub fn list_transactions(
self.keychain_tracker &self,
.chain() include_raw: bool,
.txids() ) -> impl Iterator<Item = TransactionDetails> + '_ {
.map(|&(_, txid)| self.get_tx(txid, include_raw).expect("must exist")) self.indexed_graph
.collect() .graph()
.list_chain_txs(&self.chain, self.chain.tip().unwrap_or_default())
.map(move |canonical_tx| new_tx_details(&self.indexed_graph, canonical_tx, include_raw))
} }
/// Iterate over the transactions in the wallet in order of ascending confirmation time with /// Iterate over the transactions in the wallet in order of ascending confirmation time with
/// unconfirmed transactions last. /// unconfirmed transactions last.
pub fn transactions( pub fn transactions(
&self, &self,
) -> impl DoubleEndedIterator<Item = (ConfirmationTime, &Transaction)> + '_ { ) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>> + '_ {
self.keychain_tracker self.indexed_graph
.chain_graph() .graph()
.transactions_in_chain() .list_chain_txs(&self.chain, self.chain.tip().unwrap_or_default())
.map(|(pos, tx)| (*pos, tx))
} }
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
/// values. /// values.
pub fn get_balance(&self) -> Balance { pub fn get_balance(&self) -> Balance {
self.keychain_tracker.balance(|keychain| match keychain { self.indexed_graph.graph().balance(
KeychainKind::External => false, &self.chain,
KeychainKind::Internal => true, self.chain.tip().unwrap_or_default(),
}) self.indexed_graph.index.outpoints().iter().cloned(),
|&(k, _), _| k == KeychainKind::Internal,
)
} }
/// Add an external signer /// Add an external signer
@ -613,17 +690,17 @@ impl<D> Wallet<D> {
params: TxParams, params: TxParams,
) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error> ) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error>
where where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>, D: PersistBackend<ChangeSet>,
{ {
let external_descriptor = self let external_descriptor = self
.keychain_tracker .indexed_graph
.txout_index .index
.keychains() .keychains()
.get(&KeychainKind::External) .get(&KeychainKind::External)
.expect("must exist"); .expect("must exist");
let internal_descriptor = self let internal_descriptor = self
.keychain_tracker .indexed_graph
.txout_index .index
.keychains() .keychains()
.get(&KeychainKind::Internal); .get(&KeychainKind::Internal);
@ -700,9 +777,8 @@ impl<D> Wallet<D> {
let current_height = match params.current_height { let current_height = match params.current_height {
// If they didn't tell us the current height, we assume it's the latest sync height. // If they didn't tell us the current height, we assume it's the latest sync height.
None => self None => self
.keychain_tracker .chain
.chain() .tip()
.latest_checkpoint()
.and_then(|cp| cp.height.into()) .and_then(|cp| cp.height.into())
.map(|height| LockTime::from_height(height).expect("Invalid height")), .map(|height| LockTime::from_height(height).expect("Invalid height")),
h => h, h => h,
@ -874,14 +950,10 @@ impl<D> Wallet<D> {
Some(ref drain_recipient) => drain_recipient.clone(), Some(ref drain_recipient) => drain_recipient.clone(),
None => { None => {
let change_keychain = self.map_keychain(KeychainKind::Internal); let change_keychain = self.map_keychain(KeychainKind::Internal);
let ((index, spk), changeset) = self let ((index, spk), changeset) =
.keychain_tracker self.indexed_graph.index.next_unused_spk(&change_keychain);
.txout_index
.next_unused_spk(&change_keychain);
let spk = spk.clone(); let spk = spk.clone();
self.keychain_tracker self.indexed_graph.index.mark_used(&change_keychain, index);
.txout_index
.mark_used(&change_keychain, index);
self.persist.stage(changeset.into()); self.persist.stage(changeset.into());
self.persist.commit().expect("TODO"); self.persist.commit().expect("TODO");
spk spk
@ -1019,16 +1091,21 @@ impl<D> Wallet<D> {
&mut self, &mut self,
txid: Txid, txid: Txid,
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> { ) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
let graph = self.keychain_tracker.graph(); let graph = self.indexed_graph.graph();
let txout_index = &self.keychain_tracker.txout_index; let txout_index = &self.indexed_graph.index;
let tx_and_height = self.keychain_tracker.chain_graph().get_tx_in_chain(txid); let chain_tip = self.chain.tip().unwrap_or_default();
let mut tx = match tx_and_height {
None => return Err(Error::TransactionNotFound), let mut tx = graph
Some((ConfirmationTime::Confirmed { .. }, _tx)) => { .get_tx(txid)
return Err(Error::TransactionConfirmed) .ok_or(Error::TransactionNotFound)?
} .clone();
Some((_, tx)) => tx.clone(),
}; let pos = graph
.get_chain_position(&self.chain, chain_tip, txid)
.ok_or(Error::TransactionNotFound)?;
if let ObservedAs::Confirmed(_) = pos {
return Err(Error::TransactionConfirmed);
}
if !tx if !tx
.input .input
@ -1051,13 +1128,17 @@ impl<D> Wallet<D> {
let original_utxos = original_txin let original_utxos = original_txin
.iter() .iter()
.map(|txin| -> Result<_, Error> { .map(|txin| -> Result<_, Error> {
let (&confirmation_time, prev_tx) = self let prev_tx = graph
.keychain_tracker .get_tx(txin.previous_output.txid)
.chain_graph()
.get_tx_in_chain(txin.previous_output.txid)
.ok_or(Error::UnknownUtxo)?; .ok_or(Error::UnknownUtxo)?;
let txout = &prev_tx.output[txin.previous_output.vout as usize]; let txout = &prev_tx.output[txin.previous_output.vout as usize];
let confirmation_time: ConfirmationTime = graph
.get_chain_position(&self.chain, chain_tip, txin.previous_output.txid)
.ok_or(Error::UnknownUtxo)?
.cloned()
.into();
let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) { let weighted_utxo = match txout_index.index_of_spk(&txout.script_pubkey) {
Some(&(keychain, derivation_index)) => { Some(&(keychain, derivation_index)) => {
let satisfaction_weight = self let satisfaction_weight = self
@ -1231,7 +1312,7 @@ impl<D> Wallet<D> {
/// ///
/// This can be used to build a watch-only version of a wallet /// This can be used to build a watch-only version of a wallet
pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> { pub fn public_descriptor(&self, keychain: KeychainKind) -> Option<&ExtendedDescriptor> {
self.keychain_tracker.txout_index.keychains().get(&keychain) self.indexed_graph.index.keychains().get(&keychain)
} }
/// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass /// Finalize a PSBT, i.e., for each input determine if sufficient data is available to pass
@ -1247,6 +1328,8 @@ impl<D> Wallet<D> {
psbt: &mut psbt::PartiallySignedTransaction, psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions, sign_options: SignOptions,
) -> Result<bool, Error> { ) -> Result<bool, Error> {
let chain_tip = self.chain.tip().unwrap_or_default();
let tx = &psbt.unsigned_tx; let tx = &psbt.unsigned_tx;
let mut finished = true; let mut finished = true;
@ -1259,19 +1342,16 @@ impl<D> Wallet<D> {
continue; continue;
} }
let confirmation_height = self let confirmation_height = self
.keychain_tracker .indexed_graph
.chain() .graph()
.tx_position(input.previous_output.txid) .get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
.map(|conftime| match conftime { .map(|observed_as| match observed_as {
&ConfirmationTime::Confirmed { height, .. } => height, ObservedAs::Confirmed(a) => a.confirmation_height,
ConfirmationTime::Unconfirmed => u32::MAX, ObservedAs::Unconfirmed(_) => u32::MAX,
}); });
let last_sync_height = self let current_height = sign_options
.keychain_tracker .assume_height
.chain() .or(self.chain.tip().map(|b| b.height));
.latest_checkpoint()
.map(|block_id| block_id.height);
let current_height = sign_options.assume_height.or(last_sync_height);
debug!( debug!(
"Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}", "Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}",
@ -1288,8 +1368,8 @@ impl<D> Wallet<D> {
.get_utxo_for(n) .get_utxo_for(n)
.and_then(|txout| self.get_descriptor_for_txout(&txout)) .and_then(|txout| self.get_descriptor_for_txout(&txout))
.or_else(|| { .or_else(|| {
self.keychain_tracker self.indexed_graph
.txout_index .index
.keychains() .keychains()
.iter() .iter()
.find_map(|(_, desc)| { .find_map(|(_, desc)| {
@ -1347,14 +1427,12 @@ impl<D> Wallet<D> {
/// The derivation index of this wallet. It will return `None` if it has not derived any addresses. /// The derivation index of this wallet. It will return `None` if it has not derived any addresses.
/// Otherwise, it will return the index of the highest address it has derived. /// Otherwise, it will return the index of the highest address it has derived.
pub fn derivation_index(&self, keychain: KeychainKind) -> Option<u32> { pub fn derivation_index(&self, keychain: KeychainKind) -> Option<u32> {
self.keychain_tracker self.indexed_graph.index.last_revealed_index(&keychain)
.txout_index
.last_revealed_index(&keychain)
} }
/// The index of the next address that you would get if you were to ask the wallet for a new address /// The index of the next address that you would get if you were to ask the wallet for a new address
pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 { pub fn next_derivation_index(&self, keychain: KeychainKind) -> u32 {
self.keychain_tracker.txout_index.next_index(&keychain).0 self.indexed_graph.index.next_index(&keychain).0
} }
/// Informs the wallet that you no longer intend to broadcast a tx that was built from it. /// Informs the wallet that you no longer intend to broadcast a tx that was built from it.
@ -1362,7 +1440,7 @@ impl<D> Wallet<D> {
/// This frees up the change address used when creating the tx for use in future transactions. /// This frees up the change address used when creating the tx for use in future transactions.
// TODO: Make this free up reserved utxos when that's implemented // TODO: Make this free up reserved utxos when that's implemented
pub fn cancel_tx(&mut self, tx: &Transaction) { pub fn cancel_tx(&mut self, tx: &Transaction) {
let txout_index = &mut self.keychain_tracker.txout_index; let txout_index = &mut self.indexed_graph.index;
for txout in &tx.output { for txout in &tx.output {
if let Some(&(keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) { if let Some(&(keychain, index)) = txout_index.index_of_spk(&txout.script_pubkey) {
// NOTE: unmark_used will **not** make something unused if it has actually been used // NOTE: unmark_used will **not** make something unused if it has actually been used
@ -1384,8 +1462,8 @@ impl<D> Wallet<D> {
fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<DerivedDescriptor> { fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<DerivedDescriptor> {
let &(keychain, child) = self let &(keychain, child) = self
.keychain_tracker .indexed_graph
.txout_index .index
.index_of_spk(&txout.script_pubkey)?; .index_of_spk(&txout.script_pubkey)?;
let descriptor = self.get_descriptor_for_keychain(keychain); let descriptor = self.get_descriptor_for_keychain(keychain);
Some(descriptor.at_derivation_index(child)) Some(descriptor.at_derivation_index(child))
@ -1393,7 +1471,6 @@ impl<D> Wallet<D> {
fn get_available_utxos(&self) -> Vec<(LocalUtxo, usize)> { fn get_available_utxos(&self) -> Vec<(LocalUtxo, usize)> {
self.list_unspent() self.list_unspent()
.into_iter()
.map(|utxo| { .map(|utxo| {
let keychain = utxo.keychain; let keychain = utxo.keychain;
( (
@ -1419,6 +1496,7 @@ impl<D> Wallet<D> {
must_only_use_confirmed_tx: bool, must_only_use_confirmed_tx: bool,
current_height: Option<u32>, current_height: Option<u32>,
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) { ) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
let chain_tip = self.chain.tip().unwrap_or_default();
// must_spend <- manually selected utxos // must_spend <- manually selected utxos
// may_spend <- all other available utxos // may_spend <- all other available utxos
let mut may_spend = self.get_available_utxos(); let mut may_spend = self.get_available_utxos();
@ -1438,39 +1516,43 @@ impl<D> Wallet<D> {
let satisfies_confirmed = may_spend let satisfies_confirmed = may_spend
.iter() .iter()
.map(|u| { .map(|u| -> bool {
let txid = u.0.outpoint.txid; let txid = u.0.outpoint.txid;
let tx = self.keychain_tracker.chain_graph().get_tx_in_chain(txid); let tx = match self.indexed_graph.graph().get_tx(txid) {
match tx { Some(tx) => tx,
// We don't have the tx in the db for some reason, None => return false,
// so we can't know for sure if it's mature or not. };
// We prefer not to spend it. let confirmation_time: ConfirmationTime = match self
None => false, .indexed_graph
Some((confirmation_time, tx)) => { .graph()
// Whether the UTXO is mature and, if needed, confirmed .get_chain_position(&self.chain, chain_tip, txid)
let mut spendable = true; {
if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() { Some(observed_as) => observed_as.cloned().into(),
return false; None => return false,
} };
if tx.is_coin_base() {
debug_assert!( // Whether the UTXO is mature and, if needed, confirmed
confirmation_time.is_confirmed(), let mut spendable = true;
"coinbase must always be confirmed" if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() {
); return false;
if let Some(current_height) = current_height { }
match confirmation_time { if tx.is_coin_base() {
ConfirmationTime::Confirmed { height, .. } => { debug_assert!(
// https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375 confirmation_time.is_confirmed(),
spendable &= (current_height.saturating_sub(*height)) "coinbase must always be confirmed"
>= COINBASE_MATURITY; );
} if let Some(current_height) = current_height {
ConfirmationTime::Unconfirmed => spendable = false, match confirmation_time {
} ConfirmationTime::Confirmed { height, .. } => {
// https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
spendable &=
(current_height.saturating_sub(height)) >= COINBASE_MATURITY;
} }
ConfirmationTime::Unconfirmed => spendable = false,
} }
spendable
} }
} }
spendable
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
@ -1590,8 +1672,8 @@ impl<D> Wallet<D> {
// Try to find the prev_script in our db to figure out if this is internal or external, // Try to find the prev_script in our db to figure out if this is internal or external,
// and the derivation index // and the derivation index
let &(keychain, child) = self let &(keychain, child) = self
.keychain_tracker .indexed_graph
.txout_index .index
.index_of_spk(&utxo.txout.script_pubkey) .index_of_spk(&utxo.txout.script_pubkey)
.ok_or(Error::UnknownUtxo)?; .ok_or(Error::UnknownUtxo)?;
@ -1608,7 +1690,7 @@ impl<D> Wallet<D> {
.map_err(MiniscriptPsbtError::Conversion)?; .map_err(MiniscriptPsbtError::Conversion)?;
let prev_output = utxo.outpoint; let prev_output = utxo.outpoint;
if let Some(prev_tx) = self.keychain_tracker.graph().get_tx(prev_output.txid) { if let Some(prev_tx) = self.indexed_graph.graph().get_tx(prev_output.txid) {
if desc.is_witness() || desc.is_taproot() { if desc.is_witness() || desc.is_taproot() {
psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone()); psbt_input.witness_utxo = Some(prev_tx.output[prev_output.vout as usize].clone());
} }
@ -1641,10 +1723,8 @@ impl<D> Wallet<D> {
// Try to figure out the keychain and derivation for every input and output // Try to figure out the keychain and derivation for every input and output
for (is_input, index, out) in utxos.into_iter() { for (is_input, index, out) in utxos.into_iter() {
if let Some(&(keychain, child)) = self if let Some(&(keychain, child)) =
.keychain_tracker self.indexed_graph.index.index_of_spk(&out.script_pubkey)
.txout_index
.index_of_spk(&out.script_pubkey)
{ {
debug!( debug!(
"Found descriptor for input #{} {:?}/{}", "Found descriptor for input #{} {:?}/{}",
@ -1685,52 +1765,62 @@ impl<D> Wallet<D> {
/// transactions related to your wallet into it. /// transactions related to your wallet into it.
/// ///
/// [`commit`]: Self::commit /// [`commit`]: Self::commit
pub fn apply_update(&mut self, update: Update) -> Result<(), UpdateError> pub fn apply_update(&mut self, update: Update) -> Result<bool, UpdateNotConnectedError>
where where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>, D: PersistBackend<ChangeSet>,
{ {
let changeset = self.keychain_tracker.apply_update(update)?; let mut changeset: ChangeSet = self.chain.apply_update(update.chain)?.into();
self.persist.stage(changeset); let (_, derivation_additions) = self
Ok(()) .indexed_graph
.index
.reveal_to_target_multi(&update.keychain);
changeset.append(derivation_additions.into());
changeset.append(self.indexed_graph.apply_update(update.graph).into());
let changed = !changeset.is_empty();
if changed {
self.persist.stage(changeset);
}
Ok(changed)
} }
/// Commits all curently [`staged`] changed to the persistence backend returning and error when this fails. /// Commits all curently [`staged`] changed to the persistence backend returning and error when this fails.
/// ///
/// [`staged`]: Self::staged /// [`staged`]: Self::staged
pub fn commit(&mut self) -> Result<(), D::WriteError> pub fn commit(&mut self) -> Result<bool, D::WriteError>
where where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>, D: PersistBackend<ChangeSet>,
{ {
self.persist.commit() self.persist.commit().map(|c| c.is_some())
} }
/// Returns the changes that will be staged with the next call to [`commit`]. /// Returns the changes that will be staged with the next call to [`commit`].
/// ///
/// [`commit`]: Self::commit /// [`commit`]: Self::commit
pub fn staged(&self) -> &ChangeSet { pub fn staged(&self) -> &ChangeSet
where
D: PersistBackend<ChangeSet>,
{
self.persist.staged() self.persist.staged()
} }
/// Get a reference to the inner [`TxGraph`](bdk_chain::tx_graph::TxGraph). /// Get a reference to the inner [`TxGraph`](bdk_chain::tx_graph::TxGraph).
pub fn as_graph(&self) -> &bdk_chain::tx_graph::TxGraph { pub fn as_graph(&self) -> &TxGraph<ConfirmationTimeAnchor> {
self.keychain_tracker.graph() self.indexed_graph.graph()
} }
/// Get a reference to the inner [`ChainGraph`](bdk_chain::chain_graph::ChainGraph). pub fn as_index(&self) -> &KeychainTxOutIndex<KeychainKind> {
pub fn as_chain_graph(&self) -> &bdk_chain::chain_graph::ChainGraph<ConfirmationTime> { &self.indexed_graph.index
self.keychain_tracker.chain_graph() }
pub fn as_chain(&self) -> &LocalChain {
&self.chain
} }
} }
impl<D> AsRef<bdk_chain::tx_graph::TxGraph> for Wallet<D> { impl<D> AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeAnchor>> for Wallet<D> {
fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph { fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph<ConfirmationTimeAnchor> {
self.keychain_tracker.graph() self.indexed_graph.graph()
}
}
impl<D> AsRef<bdk_chain::chain_graph::ChainGraph<ConfirmationTime>> for Wallet<D> {
fn as_ref(&self) -> &bdk_chain::chain_graph::ChainGraph<ConfirmationTime> {
self.keychain_tracker.chain_graph()
} }
} }
@ -1765,6 +1855,76 @@ where
Ok(wallet_name) Ok(wallet_name)
} }
fn new_local_utxo(
keychain: KeychainKind,
derivation_index: u32,
full_txo: FullTxOut<ObservedAs<ConfirmationTimeAnchor>>,
) -> LocalUtxo {
LocalUtxo {
outpoint: full_txo.outpoint,
txout: full_txo.txout,
is_spent: full_txo.spent_by.is_some(),
confirmation_time: full_txo.chain_position.into(),
keychain,
derivation_index,
}
}
fn new_tx_details(
indexed_graph: &IndexedTxGraph<ConfirmationTimeAnchor, KeychainTxOutIndex<KeychainKind>>,
canonical_tx: CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>,
include_raw: bool,
) -> TransactionDetails {
let graph = indexed_graph.graph();
let index = &indexed_graph.index;
let tx = canonical_tx.node.tx;
let received = tx
.output
.iter()
.map(|txout| {
if index.index_of_spk(&txout.script_pubkey).is_some() {
txout.value
} else {
0
}
})
.sum();
let sent = tx
.input
.iter()
.map(|txin| {
if let Some((_, txout)) = index.txout(txin.previous_output) {
txout.value
} else {
0
}
})
.sum();
let inputs = tx
.input
.iter()
.map(|txin| {
graph
.get_txout(txin.previous_output)
.map(|txout| txout.value)
})
.sum::<Option<u64>>();
let outputs = tx.output.iter().map(|txout| txout.value).sum();
let fee = inputs.map(|inputs| inputs.saturating_sub(outputs));
TransactionDetails {
transaction: if include_raw { Some(tx.clone()) } else { None },
txid: canonical_tx.node.txid,
received,
sent,
fee,
confirmation_time: canonical_tx.observed_as.cloned().into(),
}
}
#[macro_export] #[macro_export]
#[doc(hidden)] #[doc(hidden)]
/// Macro for getting a wallet for use in a doctest /// Macro for getting a wallet for use in a doctest
@ -1796,7 +1956,7 @@ macro_rules! doctest_wallet {
let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed { let _ = wallet.insert_tx(tx.clone(), ConfirmationTime::Confirmed {
height: 500, height: 500,
time: 50_000 time: 50_000
}); }, None);
wallet wallet
}} }}

View File

@ -39,7 +39,7 @@
use crate::collections::BTreeMap; use crate::collections::BTreeMap;
use crate::collections::HashSet; use crate::collections::HashSet;
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec}; use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use bdk_chain::ConfirmationTime; use bdk_chain::PersistBackend;
use core::cell::RefCell; use core::cell::RefCell;
use core::marker::PhantomData; use core::marker::PhantomData;
@ -47,7 +47,7 @@ use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction}; use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm}; use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use super::persist; use super::ChangeSet;
use crate::{ use crate::{
types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo}, types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo},
TransactionDetails, TransactionDetails,
@ -529,7 +529,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki /// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error>
where where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>, D: PersistBackend<ChangeSet>,
{ {
self.wallet self.wallet
.borrow_mut() .borrow_mut()

View File

@ -35,6 +35,7 @@ pub fn get_funded_wallet_with_change(
height: 1_000, height: 1_000,
time: 100, time: 100,
}, },
None,
) )
.unwrap(); .unwrap();

View File

@ -44,6 +44,7 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: TxHeight) -> OutPoint
}, },
TxHeight::Unconfirmed => ConfirmationTime::Unconfirmed, TxHeight::Unconfirmed => ConfirmationTime::Unconfirmed,
}, },
None,
) )
.unwrap(); .unwrap();
@ -811,7 +812,7 @@ fn test_create_tx_add_utxo() {
lock_time: PackedLockTime(0), lock_time: PackedLockTime(0),
}; };
wallet wallet
.insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed) .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed, None)
.unwrap(); .unwrap();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
@ -848,7 +849,7 @@ fn test_create_tx_manually_selected_insufficient() {
}; };
wallet wallet
.insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed) .insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed, None)
.unwrap(); .unwrap();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
@ -889,7 +890,9 @@ fn test_create_tx_policy_path_no_csv() {
script_pubkey: wallet.get_address(New).script_pubkey(), script_pubkey: wallet.get_address(New).script_pubkey(),
}], }],
}; };
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap(); let external_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
let root_id = external_policy.id; let root_id = external_policy.id;
@ -972,7 +975,7 @@ fn test_add_foreign_utxo() {
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let utxo = wallet2.list_unspent().remove(0); let utxo = wallet2.list_unspent().next().expect("must take!");
let foreign_utxo_satisfaction = wallet2 let foreign_utxo_satisfaction = wallet2
.get_descriptor_for_keychain(KeychainKind::External) .get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight() .max_satisfaction_weight()
@ -1036,7 +1039,7 @@ fn test_add_foreign_utxo() {
#[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")] #[should_panic(expected = "Generic(\"Foreign utxo missing witness_utxo or non_witness_utxo\")")]
fn test_add_foreign_utxo_invalid_psbt_input() { fn test_add_foreign_utxo_invalid_psbt_input() {
let (mut wallet, _) = get_funded_wallet(get_test_wpkh()); let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
let outpoint = wallet.list_unspent()[0].outpoint; let outpoint = wallet.list_unspent().next().expect("must exist").outpoint;
let foreign_utxo_satisfaction = wallet let foreign_utxo_satisfaction = wallet
.get_descriptor_for_keychain(KeychainKind::External) .get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight() .max_satisfaction_weight()
@ -1054,7 +1057,7 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
let (wallet2, txid2) = let (wallet2, txid2) =
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
let utxo2 = wallet2.list_unspent().remove(0); let utxo2 = wallet2.list_unspent().next().unwrap();
let tx1 = wallet1.get_tx(txid1, true).unwrap().transaction.unwrap(); let tx1 = wallet1.get_tx(txid1, true).unwrap().transaction.unwrap();
let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap(); let tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap();
@ -1098,7 +1101,7 @@ fn test_add_foreign_utxo_only_witness_utxo() {
let (wallet2, txid2) = let (wallet2, txid2) =
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)"); get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let utxo2 = wallet2.list_unspent().remove(0); let utxo2 = wallet2.list_unspent().next().unwrap();
let satisfaction_weight = wallet2 let satisfaction_weight = wallet2
.get_descriptor_for_keychain(KeychainKind::External) .get_descriptor_for_keychain(KeychainKind::External)
@ -1214,7 +1217,9 @@ fn test_bump_fee_irreplaceable_tx() {
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
wallet.build_fee_bump(txid).unwrap().finish().unwrap(); wallet.build_fee_bump(txid).unwrap().finish().unwrap();
} }
@ -1237,6 +1242,7 @@ fn test_bump_fee_confirmed_tx() {
height: 42, height: 42,
time: 42_000, time: 42_000,
}, },
None,
) )
.unwrap(); .unwrap();
@ -1257,7 +1263,9 @@ fn test_bump_fee_low_fee_rate() {
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(1.0)); builder.fee_rate(FeeRate::from_sat_per_vb(1.0));
@ -1278,7 +1286,9 @@ fn test_bump_fee_low_abs() {
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(10); builder.fee_absolute(10);
@ -1298,7 +1308,9 @@ fn test_bump_fee_zero_abs() {
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(0); builder.fee_absolute(0);
@ -1316,7 +1328,9 @@ fn test_bump_fee_reduce_change() {
let (psbt, original_details) = builder.finish().unwrap(); let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf(); builder.fee_rate(FeeRate::from_sat_per_vb(2.5)).enable_rbf();
@ -1401,7 +1415,9 @@ fn test_bump_fee_reduce_single_recipient() {
let (psbt, original_details) = builder.finish().unwrap(); let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder builder
@ -1432,7 +1448,9 @@ fn test_bump_fee_absolute_reduce_single_recipient() {
let (psbt, original_details) = builder.finish().unwrap(); let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder builder
@ -1471,6 +1489,7 @@ fn test_bump_fee_drain_wallet() {
height: wallet.latest_checkpoint().unwrap().height, height: wallet.latest_checkpoint().unwrap().height,
time: 42_000, time: 42_000,
}, },
None,
) )
.unwrap(); .unwrap();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
@ -1488,7 +1507,9 @@ fn test_bump_fee_drain_wallet() {
let (psbt, original_details) = builder.finish().unwrap(); let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
assert_eq!(original_details.sent, 25_000); assert_eq!(original_details.sent, 25_000);
// for the new feerate, it should be enough to reduce the output, but since we specify // for the new feerate, it should be enough to reduce the output, but since we specify
@ -1523,7 +1544,17 @@ fn test_bump_fee_remove_output_manually_selected_only() {
}], }],
}; };
wallet wallet
.insert_tx(init_tx.clone(), wallet.transactions().last().unwrap().0) .insert_tx(
init_tx.clone(),
wallet
.transactions()
.last()
.unwrap()
.observed_as
.cloned()
.into(),
None,
)
.unwrap(); .unwrap();
let outpoint = OutPoint { let outpoint = OutPoint {
txid: init_tx.txid(), txid: init_tx.txid(),
@ -1540,7 +1571,9 @@ fn test_bump_fee_remove_output_manually_selected_only() {
let (psbt, original_details) = builder.finish().unwrap(); let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
assert_eq!(original_details.sent, 25_000); assert_eq!(original_details.sent, 25_000);
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
@ -1562,9 +1595,14 @@ fn test_bump_fee_add_input() {
value: 25_000, value: 25_000,
}], }],
}; };
wallet let pos = wallet
.insert_tx(init_tx, wallet.transactions().last().unwrap().0) .transactions()
.unwrap(); .last()
.unwrap()
.observed_as
.cloned()
.into();
wallet.insert_tx(init_tx, pos, None).unwrap();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection);
@ -1574,7 +1612,9 @@ fn test_bump_fee_add_input() {
let (psbt, original_details) = builder.finish().unwrap(); let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(50.0)); builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
@ -1618,7 +1658,9 @@ fn test_bump_fee_absolute_add_input() {
let (psbt, original_details) = builder.finish().unwrap(); let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_absolute(6_000); builder.fee_absolute(6_000);
@ -1668,7 +1710,9 @@ fn test_bump_fee_no_change_add_input_and_change() {
let tx = psbt.extract_tx(); let tx = psbt.extract_tx();
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
// now bump the fees without using `allow_shrinking`. the wallet should add an // now bump the fees without using `allow_shrinking`. the wallet should add an
// extra input and a change output, and leave the original output untouched // extra input and a change output, and leave the original output untouched
@ -1724,7 +1768,9 @@ fn test_bump_fee_add_input_change_dust() {
assert_eq!(tx.input.len(), 1); assert_eq!(tx.input.len(), 1);
assert_eq!(tx.output.len(), 2); assert_eq!(tx.output.len(), 2);
let txid = tx.txid(); let txid = tx.txid();
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
// We set a fee high enough that during rbf we are forced to add // We set a fee high enough that during rbf we are forced to add
@ -1784,7 +1830,7 @@ fn test_bump_fee_force_add_input() {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
} }
wallet wallet
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed) .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed, None)
.unwrap(); .unwrap();
// the new fee_rate is low enough that just reducing the change would be fine, but we force // the new fee_rate is low enough that just reducing the change would be fine, but we force
// the addition of an extra input with `add_utxo()` // the addition of an extra input with `add_utxo()`
@ -1839,7 +1885,7 @@ fn test_bump_fee_absolute_force_add_input() {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
} }
wallet wallet
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed) .insert_tx(tx.clone(), ConfirmationTime::Unconfirmed, None)
.unwrap(); .unwrap();
// the new fee_rate is low enough that just reducing the change would be fine, but we force // the new fee_rate is low enough that just reducing the change would be fine, but we force
@ -1899,7 +1945,9 @@ fn test_bump_fee_unconfirmed_inputs_only() {
for txin in &mut tx.input { for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
} }
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(25.0)); builder.fee_rate(FeeRate::from_sat_per_vb(25.0));
builder.finish().unwrap(); builder.finish().unwrap();
@ -1928,7 +1976,9 @@ fn test_bump_fee_unconfirmed_input() {
for txin in &mut tx.input { for txin in &mut tx.input {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
} }
wallet.insert_tx(tx, ConfirmationTime::Unconfirmed).unwrap(); wallet
.insert_tx(tx, ConfirmationTime::Unconfirmed, None)
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap(); let mut builder = wallet.build_fee_bump(txid).unwrap();
builder builder
@ -2660,7 +2710,7 @@ fn test_taproot_foreign_utxo() {
let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig()); let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig());
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap(); let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
let utxo = wallet2.list_unspent().remove(0); let utxo = wallet2.list_unspent().next().unwrap();
let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap(); let psbt_input = wallet2.get_psbt_input(utxo.clone(), None, false).unwrap();
let foreign_utxo_satisfaction = wallet2 let foreign_utxo_satisfaction = wallet2
.get_descriptor_for_keychain(KeychainKind::External) .get_descriptor_for_keychain(KeychainKind::External)
@ -3022,6 +3072,7 @@ fn test_spend_coinbase() {
height: confirmation_height, height: confirmation_height,
time: 30_000, time: 30_000,
}, },
None,
) )
.unwrap(); .unwrap();

View File

@ -137,6 +137,18 @@ impl ConfirmationTime {
} }
} }
impl From<ObservedAs<ConfirmationTimeAnchor>> for ConfirmationTime {
fn from(observed_as: ObservedAs<ConfirmationTimeAnchor>) -> Self {
match observed_as {
ObservedAs::Confirmed(a) => Self::Confirmed {
height: a.confirmation_height,
time: a.confirmation_time,
},
ObservedAs::Unconfirmed(_) => Self::Unconfirmed,
}
}
}
/// A reference to a block in the canonical chain. /// A reference to a block in the canonical chain.
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] #[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr( #[cfg_attr(

View File

@ -1,104 +1,105 @@
use std::{io::Write, str::FromStr}; // use std::{io::Write, str::FromStr};
use bdk::{ // use bdk::{
bitcoin::{Address, Network}, // bitcoin::{Address, Network},
SignOptions, Wallet, // SignOptions, Wallet,
}; // };
use bdk_electrum::{ // use bdk_electrum::{
electrum_client::{self, ElectrumApi}, // electrum_client::{self, ElectrumApi},
ElectrumExt, // ElectrumExt,
}; // };
use bdk_file_store::KeychainStore; // use bdk_file_store::KeychainStore;
const SEND_AMOUNT: u64 = 5000; // const SEND_AMOUNT: u64 = 5000;
const STOP_GAP: usize = 50; // const STOP_GAP: usize = 50;
const BATCH_SIZE: usize = 5; // const BATCH_SIZE: usize = 5;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Hello, world!"); todo!("update this example!");
// println!("Hello, world!");
let db_path = std::env::temp_dir().join("bdk-electrum-example"); // let db_path = std::env::temp_dir().join("bdk-electrum-example");
let db = KeychainStore::new_from_path(db_path)?; // let db = KeychainStore::new_from_path(db_path)?;
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; // let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; // let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)";
let mut wallet = Wallet::new( // let mut wallet = Wallet::new(
external_descriptor, // external_descriptor,
Some(internal_descriptor), // Some(internal_descriptor),
db, // db,
Network::Testnet, // Network::Testnet,
)?; // )?;
let address = wallet.get_address(bdk::wallet::AddressIndex::New); // let address = wallet.get_address(bdk::wallet::AddressIndex::New);
println!("Generated Address: {}", address); // println!("Generated Address: {}", address);
let balance = wallet.get_balance(); // let balance = wallet.get_balance();
println!("Wallet balance before syncing: {} sats", balance.total()); // println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing..."); // print!("Syncing...");
// Scanning the chain... // // Scanning the chain...
let electrum_url = "ssl://electrum.blockstream.info:60002"; // let electrum_url = "ssl://electrum.blockstream.info:60002";
let client = electrum_client::Client::new(electrum_url)?; // let client = electrum_client::Client::new(electrum_url)?;
let local_chain = wallet.checkpoints(); // let local_chain = wallet.checkpoints();
let spks = wallet // let spks = wallet
.spks_of_all_keychains() // .spks_of_all_keychains()
.into_iter() // .into_iter()
.map(|(k, spks)| { // .map(|(k, spks)| {
let mut first = true; // let mut first = true;
( // (
k, // k,
spks.inspect(move |(spk_i, _)| { // spks.inspect(move |(spk_i, _)| {
if first { // if first {
first = false; // first = false;
print!("\nScanning keychain [{:?}]:", k); // print!("\nScanning keychain [{:?}]:", k);
} // }
print!(" {}", spk_i); // print!(" {}", spk_i);
let _ = std::io::stdout().flush(); // let _ = std::io::stdout().flush();
}), // }),
) // )
}) // })
.collect(); // .collect();
let electrum_update = client // let electrum_update = client
.scan( // .scan(
local_chain, // local_chain,
spks, // spks,
core::iter::empty(), // core::iter::empty(),
core::iter::empty(), // core::iter::empty(),
STOP_GAP, // STOP_GAP,
BATCH_SIZE, // BATCH_SIZE,
)? // )?
.into_confirmation_time_update(&client)?; // .into_confirmation_time_update(&client)?;
println!(); // println!();
let new_txs = client.batch_transaction_get(electrum_update.missing_full_txs(&wallet))?; // let new_txs = client.batch_transaction_get(electrum_update.missing_full_txs(&wallet))?;
let update = electrum_update.into_keychain_scan(new_txs, &wallet)?; // let update = electrum_update.into_keychain_scan(new_txs, &wallet)?;
wallet.apply_update(update)?; // wallet.apply_update(update)?;
wallet.commit()?; // wallet.commit()?;
let balance = wallet.get_balance(); // let balance = wallet.get_balance();
println!("Wallet balance after syncing: {} sats", balance.total()); // println!("Wallet balance after syncing: {} sats", balance.total());
if balance.total() < SEND_AMOUNT { // if balance.total() < SEND_AMOUNT {
println!( // println!(
"Please send at least {} sats to the receiving address", // "Please send at least {} sats to the receiving address",
SEND_AMOUNT // SEND_AMOUNT
); // );
std::process::exit(0); // std::process::exit(0);
} // }
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; // let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?;
let mut tx_builder = wallet.build_tx(); // let mut tx_builder = wallet.build_tx();
tx_builder // tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) // .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.enable_rbf(); // .enable_rbf();
let (mut psbt, _) = tx_builder.finish()?; // let (mut psbt, _) = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?; // let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized); // assert!(finalized);
let tx = psbt.extract_tx(); // let tx = psbt.extract_tx();
client.transaction_broadcast(&tx)?; // client.transaction_broadcast(&tx)?;
println!("Tx broadcasted! Txid: {}", tx.txid()); // println!("Tx broadcasted! Txid: {}", tx.txid());
Ok(()) // Ok(())
} }

View File

@ -1,96 +1,97 @@
use bdk::{ // use bdk::{
bitcoin::{Address, Network}, // bitcoin::{Address, Network},
wallet::AddressIndex, // wallet::AddressIndex,
SignOptions, Wallet, // SignOptions, Wallet,
}; // };
use bdk_esplora::esplora_client; // use bdk_esplora::esplora_client;
use bdk_esplora::EsploraExt; // use bdk_esplora::EsploraExt;
use bdk_file_store::KeychainStore; // use bdk_file_store::KeychainStore;
use std::{io::Write, str::FromStr}; // use std::{io::Write, str::FromStr};
const SEND_AMOUNT: u64 = 5000; // const SEND_AMOUNT: u64 = 5000;
const STOP_GAP: usize = 50; // const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5; // const PARALLEL_REQUESTS: usize = 5;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-esplora-example"); todo!("update this exampe!");
let db = KeychainStore::new_from_path(db_path)?; // let db_path = std::env::temp_dir().join("bdk-esplora-example");
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; // let db = KeychainStore::new_from_path(db_path)?;
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; // let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
// let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)";
let mut wallet = Wallet::new( // let mut wallet = Wallet::new(
external_descriptor, // external_descriptor,
Some(internal_descriptor), // Some(internal_descriptor),
db, // db,
Network::Testnet, // Network::Testnet,
)?; // )?;
let address = wallet.get_address(AddressIndex::New); // let address = wallet.get_address(AddressIndex::New);
println!("Generated Address: {}", address); // println!("Generated Address: {}", address);
let balance = wallet.get_balance(); // let balance = wallet.get_balance();
println!("Wallet balance before syncing: {} sats", balance.total()); // println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing..."); // print!("Syncing...");
// Scanning the chain... // // Scanning the chain...
let esplora_url = "https://mempool.space/testnet/api"; // let esplora_url = "https://mempool.space/testnet/api";
let client = esplora_client::Builder::new(esplora_url).build_blocking()?; // let client = esplora_client::Builder::new(esplora_url).build_blocking()?;
let checkpoints = wallet.checkpoints(); // let checkpoints = wallet.checkpoints();
let spks = wallet // let spks = wallet
.spks_of_all_keychains() // .spks_of_all_keychains()
.into_iter() // .into_iter()
.map(|(k, spks)| { // .map(|(k, spks)| {
let mut first = true; // let mut first = true;
( // (
k, // k,
spks.inspect(move |(spk_i, _)| { // spks.inspect(move |(spk_i, _)| {
if first { // if first {
first = false; // first = false;
print!("\nScanning keychain [{:?}]:", k); // print!("\nScanning keychain [{:?}]:", k);
} // }
print!(" {}", spk_i); // print!(" {}", spk_i);
let _ = std::io::stdout().flush(); // let _ = std::io::stdout().flush();
}), // }),
) // )
}) // })
.collect(); // .collect();
let update = client.scan( // let update = client.scan(
checkpoints, // checkpoints,
spks, // spks,
core::iter::empty(), // core::iter::empty(),
core::iter::empty(), // core::iter::empty(),
STOP_GAP, // STOP_GAP,
PARALLEL_REQUESTS, // PARALLEL_REQUESTS,
)?; // )?;
println!(); // println!();
wallet.apply_update(update)?; // wallet.apply_update(update)?;
wallet.commit()?; // wallet.commit()?;
let balance = wallet.get_balance(); // let balance = wallet.get_balance();
println!("Wallet balance after syncing: {} sats", balance.total()); // println!("Wallet balance after syncing: {} sats", balance.total());
if balance.total() < SEND_AMOUNT { // if balance.total() < SEND_AMOUNT {
println!( // println!(
"Please send at least {} sats to the receiving address", // "Please send at least {} sats to the receiving address",
SEND_AMOUNT // SEND_AMOUNT
); // );
std::process::exit(0); // std::process::exit(0);
} // }
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; // let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?;
let mut tx_builder = wallet.build_tx(); // let mut tx_builder = wallet.build_tx();
tx_builder // tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) // .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.enable_rbf(); // .enable_rbf();
let (mut psbt, _) = tx_builder.finish()?; // let (mut psbt, _) = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?; // let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized); // assert!(finalized);
let tx = psbt.extract_tx(); // let tx = psbt.extract_tx();
client.broadcast(&tx)?; // client.broadcast(&tx)?;
println!("Tx broadcasted! Txid: {}", tx.txid()); // println!("Tx broadcasted! Txid: {}", tx.txid());
Ok(()) // Ok(())
} }

View File

@ -1,99 +1,100 @@
use std::{io::Write, str::FromStr}; // use std::{io::Write, str::FromStr};
use bdk::{ // use bdk::{
bitcoin::{Address, Network}, // bitcoin::{Address, Network},
wallet::AddressIndex, // wallet::AddressIndex,
SignOptions, Wallet, // SignOptions, Wallet,
}; // };
use bdk_esplora::{esplora_client, EsploraAsyncExt}; // use bdk_esplora::{esplora_client, EsploraAsyncExt};
use bdk_file_store::KeychainStore; // use bdk_file_store::KeychainStore;
const SEND_AMOUNT: u64 = 5000; // const SEND_AMOUNT: u64 = 5000;
const STOP_GAP: usize = 50; // const STOP_GAP: usize = 50;
const PARALLEL_REQUESTS: usize = 5; // const PARALLEL_REQUESTS: usize = 5;
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let db_path = std::env::temp_dir().join("bdk-esplora-example"); todo!("update this example!");
let db = KeychainStore::new_from_path(db_path)?; // let db_path = std::env::temp_dir().join("bdk-esplora-example");
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)"; // let db = KeychainStore::new_from_path(db_path)?;
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)"; // let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
// let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/1/*)";
let mut wallet = Wallet::new( // let mut wallet = Wallet::new(
external_descriptor, // external_descriptor,
Some(internal_descriptor), // Some(internal_descriptor),
db, // db,
Network::Testnet, // Network::Testnet,
)?; // )?;
let address = wallet.get_address(AddressIndex::New); // let address = wallet.get_address(AddressIndex::New);
println!("Generated Address: {}", address); // println!("Generated Address: {}", address);
let balance = wallet.get_balance(); // let balance = wallet.get_balance();
println!("Wallet balance before syncing: {} sats", balance.total()); // println!("Wallet balance before syncing: {} sats", balance.total());
print!("Syncing..."); // print!("Syncing...");
// Scanning the blockchain // // Scanning the blockchain
let esplora_url = "https://mempool.space/testnet/api"; // let esplora_url = "https://mempool.space/testnet/api";
let client = esplora_client::Builder::new(esplora_url).build_async()?; // let client = esplora_client::Builder::new(esplora_url).build_async()?;
let checkpoints = wallet.checkpoints(); // let checkpoints = wallet.checkpoints();
let spks = wallet // let spks = wallet
.spks_of_all_keychains() // .spks_of_all_keychains()
.into_iter() // .into_iter()
.map(|(k, spks)| { // .map(|(k, spks)| {
let mut first = true; // let mut first = true;
( // (
k, // k,
spks.inspect(move |(spk_i, _)| { // spks.inspect(move |(spk_i, _)| {
if first { // if first {
first = false; // first = false;
print!("\nScanning keychain [{:?}]:", k); // print!("\nScanning keychain [{:?}]:", k);
} // }
print!(" {}", spk_i); // print!(" {}", spk_i);
let _ = std::io::stdout().flush(); // let _ = std::io::stdout().flush();
}), // }),
) // )
}) // })
.collect(); // .collect();
let update = client // let update = client
.scan( // .scan(
checkpoints, // checkpoints,
spks, // spks,
std::iter::empty(), // std::iter::empty(),
std::iter::empty(), // std::iter::empty(),
STOP_GAP, // STOP_GAP,
PARALLEL_REQUESTS, // PARALLEL_REQUESTS,
) // )
.await?; // .await?;
println!(); // println!();
wallet.apply_update(update)?; // wallet.apply_update(update)?;
wallet.commit()?; // wallet.commit()?;
let balance = wallet.get_balance(); // let balance = wallet.get_balance();
println!("Wallet balance after syncing: {} sats", balance.total()); // println!("Wallet balance after syncing: {} sats", balance.total());
if balance.total() < SEND_AMOUNT { // if balance.total() < SEND_AMOUNT {
println!( // println!(
"Please send at least {} sats to the receiving address", // "Please send at least {} sats to the receiving address",
SEND_AMOUNT // SEND_AMOUNT
); // );
std::process::exit(0); // std::process::exit(0);
} // }
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?; // let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?;
let mut tx_builder = wallet.build_tx(); // let mut tx_builder = wallet.build_tx();
tx_builder // tx_builder
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) // .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
.enable_rbf(); // .enable_rbf();
let (mut psbt, _) = tx_builder.finish()?; // let (mut psbt, _) = tx_builder.finish()?;
let finalized = wallet.sign(&mut psbt, SignOptions::default())?; // let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
assert!(finalized); // assert!(finalized);
let tx = psbt.extract_tx(); // let tx = psbt.extract_tx();
client.broadcast(&tx).await?; // client.broadcast(&tx).await?;
println!("Tx broadcasted! Txid: {}", tx.txid()); // println!("Tx broadcasted! Txid: {}", tx.txid());
Ok(()) // Ok(())
} }