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

View File

@ -21,9 +21,12 @@ use alloc::{
};
pub use bdk_chain::keychain::Balance;
use bdk_chain::{
chain_graph,
keychain::{persist, KeychainChangeSet, KeychainScan, KeychainTracker},
sparse_chain, BlockId, ConfirmationTime,
indexed_tx_graph::{IndexedAdditions, IndexedTxGraph},
keychain::{DerivationAdditions, KeychainTxOutIndex},
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::secp256k1::Secp256k1;
@ -83,19 +86,83 @@ const COINBASE_MATURITY: u32 = 100;
pub struct Wallet<D = ()> {
signers: Arc<SignersContainer>,
change_signers: Arc<SignersContainer>,
keychain_tracker: KeychainTracker<KeychainKind, ConfirmationTime>,
persist: persist::Persist<KeychainKind, ConfirmationTime, D>,
chain: LocalChain,
indexed_graph: IndexedTxGraph<ConfirmationTimeAnchor, KeychainTxOutIndex<KeychainKind>>,
persist: Persist<D, ChangeSet>, // [TODO] Use a different `ChangeSet`
network: Network,
secp: SecpCtx,
}
/// 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`].
pub type Update = KeychainScan<KeychainKind, ConfirmationTime>;
/// Error indicating that something was wrong with an [`Update<T>`].
pub type UpdateError = chain_graph::UpdateError<ConfirmationTime>;
#[derive(Debug, Default, PartialEq)]
pub struct Update<K = KeychainKind, A = ConfirmationTimeAnchor> {
keychain: BTreeMap<K, u32>,
graph: TxGraph<A>,
chain: LocalChain,
}
/// 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
/// 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")]
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for NewError<P> {}
@ -195,15 +270,17 @@ impl<D> Wallet<D> {
network: Network,
) -> Result<Self, NewError<D::LoadError>>
where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>,
D: PersistBackend<ChangeSet>,
{
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)
.map_err(NewError::Descriptor)?;
keychain_tracker
.txout_index
indexed_graph
.index
.add_keychain(KeychainKind::External, descriptor.clone());
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, &secp));
let change_signers = match change_descriptor {
@ -218,8 +295,8 @@ impl<D> Wallet<D> {
&secp,
));
keychain_tracker
.txout_index
indexed_graph
.index
.add_keychain(KeychainKind::Internal, change_descriptor);
change_signers
@ -227,18 +304,20 @@ impl<D> Wallet<D> {
None => Arc::new(SignersContainer::new()),
};
db.load_into_keychain_tracker(&mut keychain_tracker)
.map_err(NewError::Persist)?;
let changeset = db.load_from_persistence().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 {
signers,
change_signers,
network,
chain,
indexed_graph,
persist,
secp,
keychain_tracker,
})
}
@ -249,7 +328,7 @@ impl<D> Wallet<D> {
/// Iterator over all keychains in this wallet
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
@ -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`].
pub fn get_address(&mut self, address_index: AddressIndex) -> AddressInfo
where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>,
D: PersistBackend<ChangeSet>,
{
self._get_address(address_index, KeychainKind::External)
}
@ -271,17 +350,17 @@ impl<D> Wallet<D> {
/// be returned for any [`AddressIndex`].
pub fn get_internal_address(&mut self, address_index: AddressIndex) -> AddressInfo
where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>,
D: PersistBackend<ChangeSet>,
{
self._get_address(address_index, KeychainKind::Internal)
}
fn _get_address(&mut self, address_index: AddressIndex, keychain: KeychainKind) -> AddressInfo
where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>,
D: PersistBackend<ChangeSet>,
{
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 {
AddressIndex::New => {
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)
pub fn is_mine(&self, script: &Script) -> bool {
self.keychain_tracker
.txout_index
.index_of_spk(script)
.is_some()
self.indexed_graph.index.index_of_spk(script).is_some()
}
/// Finds how the wallet derived the script pubkey `spk`.
///
/// Will only return `Some(_)` if the wallet has given out the spk.
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
pub fn list_unspent(&self) -> Vec<LocalUtxo> {
self.keychain_tracker
.full_utxos()
.map(|(&(keychain, derivation_index), utxo)| LocalUtxo {
outpoint: utxo.outpoint,
txout: utxo.txout,
keychain,
is_spent: false,
derivation_index,
confirmation_time: utxo.chain_position,
})
.collect()
pub fn list_unspent(&self) -> impl Iterator<Item = LocalUtxo> + '_ {
self.indexed_graph
.graph()
.filter_chain_unspents(
&self.chain,
self.chain.tip().unwrap_or_default(),
self.indexed_graph.index.outpoints().iter().cloned(),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
}
/// Get all the checkpoints the wallet is currently storing indexed by height.
pub fn checkpoints(&self) -> &BTreeMap<u32, BlockHash> {
self.keychain_tracker.chain().checkpoints()
self.chain.blocks()
}
/// Returns the latest checkpoint.
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`.
@ -369,7 +442,7 @@ impl<D> Wallet<D> {
pub fn spks_of_all_keychains(
&self,
) -> 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.
@ -381,30 +454,22 @@ impl<D> Wallet<D> {
&self,
keychain: KeychainKind,
) -> impl Iterator<Item = (u32, Script)> + Clone {
self.keychain_tracker
.txout_index
.spks_of_keychain(&keychain)
self.indexed_graph.index.spks_of_keychain(&keychain)
}
/// Returns the utxo owned by this wallet corresponding to `outpoint` if it exists in the
/// wallet's database.
pub fn get_utxo(&self, op: OutPoint) -> Option<LocalUtxo> {
self.keychain_tracker
.full_utxos()
.find_map(|(&(keychain, derivation_index), txo)| {
if op == txo.outpoint {
Some(LocalUtxo {
outpoint: txo.outpoint,
txout: txo.txout,
keychain,
is_spent: txo.spent_by.is_none(),
derivation_index,
confirmation_time: txo.chain_position,
})
} else {
None
}
})
let (&spk_i, _) = self.indexed_graph.index.txout(op)?;
self.indexed_graph
.graph()
.filter_chain_unspents(
&self.chain,
self.chain.tip().unwrap_or_default(),
core::iter::once((spk_i, op)),
)
.map(|((k, i), full_txo)| new_local_utxo(k, i, full_txo))
.next()
}
/// 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
/// `include_raw` is `true`.
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.keychain_tracker.graph();
let txout_index = &self.keychain_tracker.txout_index;
let graph = self.indexed_graph.graph();
let received = tx
.output
.iter()
.map(|txout| {
if txout_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)) = 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 },
let canonical_tx = CanonicalTx {
observed_as: graph.get_chain_position(
&self.chain,
self.chain.tip().unwrap_or_default(),
txid,
received,
sent,
fee,
confirmation_time,
})
)?,
node: graph.get_tx_node(txid)?,
};
Some(new_tx_details(
&self.indexed_graph,
canonical_tx,
include_raw,
))
}
/// 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(
&mut self,
block_id: BlockId,
) -> Result<bool, sparse_chain::InsertCheckpointError> {
let changeset = self.keychain_tracker.insert_checkpoint(block_id)?;
let changed = changeset.is_empty();
self.persist.stage(changeset);
) -> Result<bool, local_chain::InsertBlockNotMatchingError>
where
D: PersistBackend<ChangeSet>,
{
let changeset = self.chain.insert_block(block_id)?;
let changed = !changeset.is_empty();
if changed {
self.persist.stage(changeset.into());
}
Ok(changed)
}
@ -497,41 +535,80 @@ impl<D> Wallet<D> {
&mut self,
tx: Transaction,
position: ConfirmationTime,
) -> Result<bool, chain_graph::InsertTxError<ConfirmationTime>> {
let changeset = self.keychain_tracker.insert_tx(tx, position)?;
let changed = changeset.is_empty();
seen_at: Option<u64>,
) -> Result<bool, InsertTxError>
where
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)
}
#[deprecated(note = "use Wallet::transactions instead")]
/// Deprecated. use `Wallet::transactions` instead.
pub fn list_transactions(&self, include_raw: bool) -> Vec<TransactionDetails> {
self.keychain_tracker
.chain()
.txids()
.map(|&(_, txid)| self.get_tx(txid, include_raw).expect("must exist"))
.collect()
pub fn list_transactions(
&self,
include_raw: bool,
) -> impl Iterator<Item = TransactionDetails> + '_ {
self.indexed_graph
.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
/// unconfirmed transactions last.
pub fn transactions(
&self,
) -> impl DoubleEndedIterator<Item = (ConfirmationTime, &Transaction)> + '_ {
self.keychain_tracker
.chain_graph()
.transactions_in_chain()
.map(|(pos, tx)| (*pos, tx))
) -> impl Iterator<Item = CanonicalTx<'_, Transaction, ConfirmationTimeAnchor>> + '_ {
self.indexed_graph
.graph()
.list_chain_txs(&self.chain, self.chain.tip().unwrap_or_default())
}
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
/// values.
pub fn get_balance(&self) -> Balance {
self.keychain_tracker.balance(|keychain| match keychain {
KeychainKind::External => false,
KeychainKind::Internal => true,
})
self.indexed_graph.graph().balance(
&self.chain,
self.chain.tip().unwrap_or_default(),
self.indexed_graph.index.outpoints().iter().cloned(),
|&(k, _), _| k == KeychainKind::Internal,
)
}
/// Add an external signer
@ -613,17 +690,17 @@ impl<D> Wallet<D> {
params: TxParams,
) -> Result<(psbt::PartiallySignedTransaction, TransactionDetails), Error>
where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>,
D: PersistBackend<ChangeSet>,
{
let external_descriptor = self
.keychain_tracker
.txout_index
.indexed_graph
.index
.keychains()
.get(&KeychainKind::External)
.expect("must exist");
let internal_descriptor = self
.keychain_tracker
.txout_index
.indexed_graph
.index
.keychains()
.get(&KeychainKind::Internal);
@ -700,9 +777,8 @@ impl<D> Wallet<D> {
let current_height = match params.current_height {
// If they didn't tell us the current height, we assume it's the latest sync height.
None => self
.keychain_tracker
.chain()
.latest_checkpoint()
.chain
.tip()
.and_then(|cp| cp.height.into())
.map(|height| LockTime::from_height(height).expect("Invalid height")),
h => h,
@ -874,14 +950,10 @@ impl<D> Wallet<D> {
Some(ref drain_recipient) => drain_recipient.clone(),
None => {
let change_keychain = self.map_keychain(KeychainKind::Internal);
let ((index, spk), changeset) = self
.keychain_tracker
.txout_index
.next_unused_spk(&change_keychain);
let ((index, spk), changeset) =
self.indexed_graph.index.next_unused_spk(&change_keychain);
let spk = spk.clone();
self.keychain_tracker
.txout_index
.mark_used(&change_keychain, index);
self.indexed_graph.index.mark_used(&change_keychain, index);
self.persist.stage(changeset.into());
self.persist.commit().expect("TODO");
spk
@ -1019,16 +1091,21 @@ impl<D> Wallet<D> {
&mut self,
txid: Txid,
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
let graph = self.keychain_tracker.graph();
let txout_index = &self.keychain_tracker.txout_index;
let tx_and_height = self.keychain_tracker.chain_graph().get_tx_in_chain(txid);
let mut tx = match tx_and_height {
None => return Err(Error::TransactionNotFound),
Some((ConfirmationTime::Confirmed { .. }, _tx)) => {
return Err(Error::TransactionConfirmed)
let graph = self.indexed_graph.graph();
let txout_index = &self.indexed_graph.index;
let chain_tip = self.chain.tip().unwrap_or_default();
let mut tx = graph
.get_tx(txid)
.ok_or(Error::TransactionNotFound)?
.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);
}
Some((_, tx)) => tx.clone(),
};
if !tx
.input
@ -1051,13 +1128,17 @@ impl<D> Wallet<D> {
let original_utxos = original_txin
.iter()
.map(|txin| -> Result<_, Error> {
let (&confirmation_time, prev_tx) = self
.keychain_tracker
.chain_graph()
.get_tx_in_chain(txin.previous_output.txid)
let prev_tx = graph
.get_tx(txin.previous_output.txid)
.ok_or(Error::UnknownUtxo)?;
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) {
Some(&(keychain, derivation_index)) => {
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
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
@ -1247,6 +1328,8 @@ impl<D> Wallet<D> {
psbt: &mut psbt::PartiallySignedTransaction,
sign_options: SignOptions,
) -> Result<bool, Error> {
let chain_tip = self.chain.tip().unwrap_or_default();
let tx = &psbt.unsigned_tx;
let mut finished = true;
@ -1259,19 +1342,16 @@ impl<D> Wallet<D> {
continue;
}
let confirmation_height = self
.keychain_tracker
.chain()
.tx_position(input.previous_output.txid)
.map(|conftime| match conftime {
&ConfirmationTime::Confirmed { height, .. } => height,
ConfirmationTime::Unconfirmed => u32::MAX,
.indexed_graph
.graph()
.get_chain_position(&self.chain, chain_tip, input.previous_output.txid)
.map(|observed_as| match observed_as {
ObservedAs::Confirmed(a) => a.confirmation_height,
ObservedAs::Unconfirmed(_) => u32::MAX,
});
let last_sync_height = self
.keychain_tracker
.chain()
.latest_checkpoint()
.map(|block_id| block_id.height);
let current_height = sign_options.assume_height.or(last_sync_height);
let current_height = sign_options
.assume_height
.or(self.chain.tip().map(|b| b.height));
debug!(
"Input #{} - {}, using `confirmation_height` = {:?}, `current_height` = {:?}",
@ -1288,8 +1368,8 @@ impl<D> Wallet<D> {
.get_utxo_for(n)
.and_then(|txout| self.get_descriptor_for_txout(&txout))
.or_else(|| {
self.keychain_tracker
.txout_index
self.indexed_graph
.index
.keychains()
.iter()
.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.
/// Otherwise, it will return the index of the highest address it has derived.
pub fn derivation_index(&self, keychain: KeychainKind) -> Option<u32> {
self.keychain_tracker
.txout_index
.last_revealed_index(&keychain)
self.indexed_graph.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
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.
@ -1362,7 +1440,7 @@ impl<D> Wallet<D> {
/// 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
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 {
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
@ -1384,8 +1462,8 @@ impl<D> Wallet<D> {
fn get_descriptor_for_txout(&self, txout: &TxOut) -> Option<DerivedDescriptor> {
let &(keychain, child) = self
.keychain_tracker
.txout_index
.indexed_graph
.index
.index_of_spk(&txout.script_pubkey)?;
let descriptor = self.get_descriptor_for_keychain(keychain);
Some(descriptor.at_derivation_index(child))
@ -1393,7 +1471,6 @@ impl<D> Wallet<D> {
fn get_available_utxos(&self) -> Vec<(LocalUtxo, usize)> {
self.list_unspent()
.into_iter()
.map(|utxo| {
let keychain = utxo.keychain;
(
@ -1419,6 +1496,7 @@ impl<D> Wallet<D> {
must_only_use_confirmed_tx: bool,
current_height: Option<u32>,
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
let chain_tip = self.chain.tip().unwrap_or_default();
// must_spend <- manually selected utxos
// may_spend <- all other available utxos
let mut may_spend = self.get_available_utxos();
@ -1438,15 +1516,21 @@ impl<D> Wallet<D> {
let satisfies_confirmed = may_spend
.iter()
.map(|u| {
.map(|u| -> bool {
let txid = u.0.outpoint.txid;
let tx = self.keychain_tracker.chain_graph().get_tx_in_chain(txid);
match tx {
// We don't have the tx in the db for some reason,
// so we can't know for sure if it's mature or not.
// We prefer not to spend it.
None => false,
Some((confirmation_time, tx)) => {
let tx = match self.indexed_graph.graph().get_tx(txid) {
Some(tx) => tx,
None => return false,
};
let confirmation_time: ConfirmationTime = match self
.indexed_graph
.graph()
.get_chain_position(&self.chain, chain_tip, txid)
{
Some(observed_as) => observed_as.cloned().into(),
None => return false,
};
// Whether the UTXO is mature and, if needed, confirmed
let mut spendable = true;
if must_only_use_confirmed_tx && !confirmation_time.is_confirmed() {
@ -1461,16 +1545,14 @@ impl<D> Wallet<D> {
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;
spendable &=
(current_height.saturating_sub(height)) >= COINBASE_MATURITY;
}
ConfirmationTime::Unconfirmed => spendable = false,
}
}
}
spendable
}
}
})
.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,
// and the derivation index
let &(keychain, child) = self
.keychain_tracker
.txout_index
.indexed_graph
.index
.index_of_spk(&utxo.txout.script_pubkey)
.ok_or(Error::UnknownUtxo)?;
@ -1608,7 +1690,7 @@ impl<D> Wallet<D> {
.map_err(MiniscriptPsbtError::Conversion)?;
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() {
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
for (is_input, index, out) in utxos.into_iter() {
if let Some(&(keychain, child)) = self
.keychain_tracker
.txout_index
.index_of_spk(&out.script_pubkey)
if let Some(&(keychain, child)) =
self.indexed_graph.index.index_of_spk(&out.script_pubkey)
{
debug!(
"Found descriptor for input #{} {:?}/{}",
@ -1685,52 +1765,62 @@ impl<D> Wallet<D> {
/// transactions related to your wallet into it.
///
/// [`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
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();
let (_, derivation_additions) = self
.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(())
}
Ok(changed)
}
/// Commits all curently [`staged`] changed to the persistence backend returning and error when this fails.
///
/// [`staged`]: Self::staged
pub fn commit(&mut self) -> Result<(), D::WriteError>
pub fn commit(&mut self) -> Result<bool, D::WriteError>
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`].
///
/// [`commit`]: Self::commit
pub fn staged(&self) -> &ChangeSet {
pub fn staged(&self) -> &ChangeSet
where
D: PersistBackend<ChangeSet>,
{
self.persist.staged()
}
/// Get a reference to the inner [`TxGraph`](bdk_chain::tx_graph::TxGraph).
pub fn as_graph(&self) -> &bdk_chain::tx_graph::TxGraph {
self.keychain_tracker.graph()
pub fn as_graph(&self) -> &TxGraph<ConfirmationTimeAnchor> {
self.indexed_graph.graph()
}
/// Get a reference to the inner [`ChainGraph`](bdk_chain::chain_graph::ChainGraph).
pub fn as_chain_graph(&self) -> &bdk_chain::chain_graph::ChainGraph<ConfirmationTime> {
self.keychain_tracker.chain_graph()
pub fn as_index(&self) -> &KeychainTxOutIndex<KeychainKind> {
&self.indexed_graph.index
}
pub fn as_chain(&self) -> &LocalChain {
&self.chain
}
}
impl<D> AsRef<bdk_chain::tx_graph::TxGraph> for Wallet<D> {
fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph {
self.keychain_tracker.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()
impl<D> AsRef<bdk_chain::tx_graph::TxGraph<ConfirmationTimeAnchor>> for Wallet<D> {
fn as_ref(&self) -> &bdk_chain::tx_graph::TxGraph<ConfirmationTimeAnchor> {
self.indexed_graph.graph()
}
}
@ -1765,6 +1855,76 @@ where
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]
#[doc(hidden)]
/// 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 {
height: 500,
time: 50_000
});
}, None);
wallet
}}

View File

@ -39,7 +39,7 @@
use crate::collections::BTreeMap;
use crate::collections::HashSet;
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
use bdk_chain::ConfirmationTime;
use bdk_chain::PersistBackend;
use core::cell::RefCell;
use core::marker::PhantomData;
@ -47,7 +47,7 @@ use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
use super::persist;
use super::ChangeSet;
use crate::{
types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo},
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
pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error>
where
D: persist::PersistBackend<KeychainKind, ConfirmationTime>,
D: PersistBackend<ChangeSet>,
{
self.wallet
.borrow_mut()

View File

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

View File

@ -44,6 +44,7 @@ fn receive_output(wallet: &mut Wallet, value: u64, height: TxHeight) -> OutPoint
},
TxHeight::Unconfirmed => ConfirmationTime::Unconfirmed,
},
None,
)
.unwrap();
@ -811,7 +812,7 @@ fn test_create_tx_add_utxo() {
lock_time: PackedLockTime(0),
};
wallet
.insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed)
.insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed, None)
.unwrap();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
@ -848,7 +849,7 @@ fn test_create_tx_manually_selected_insufficient() {
};
wallet
.insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed)
.insert_tx(small_output_tx.clone(), ConfirmationTime::Unconfirmed, None)
.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(),
}],
};
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 root_id = external_policy.id;
@ -972,7 +975,7 @@ fn test_add_foreign_utxo() {
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
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
.get_descriptor_for_keychain(KeychainKind::External)
.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\")")]
fn test_add_foreign_utxo_invalid_psbt_input() {
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
.get_descriptor_for_keychain(KeychainKind::External)
.max_satisfaction_weight()
@ -1054,7 +1057,7 @@ fn test_add_foreign_utxo_where_outpoint_doesnt_match_psbt_input() {
let (wallet2, txid2) =
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 tx2 = wallet2.get_tx(txid2, true).unwrap().transaction.unwrap();
@ -1098,7 +1101,7 @@ fn test_add_foreign_utxo_only_witness_utxo() {
let (wallet2, txid2) =
get_funded_wallet("wpkh(cVbZ8ovhye9AoAHFsqobCf7LxbXDAECy9Kb8TZdfsDYMZGBUyCnm)");
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
.get_descriptor_for_keychain(KeychainKind::External)
@ -1214,7 +1217,9 @@ fn test_bump_fee_irreplaceable_tx() {
let tx = psbt.extract_tx();
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();
}
@ -1237,6 +1242,7 @@ fn test_bump_fee_confirmed_tx() {
height: 42,
time: 42_000,
},
None,
)
.unwrap();
@ -1257,7 +1263,9 @@ fn test_bump_fee_low_fee_rate() {
let tx = psbt.extract_tx();
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();
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 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();
builder.fee_absolute(10);
@ -1298,7 +1308,9 @@ fn test_bump_fee_zero_abs() {
let tx = psbt.extract_tx();
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();
builder.fee_absolute(0);
@ -1316,7 +1328,9 @@ fn test_bump_fee_reduce_change() {
let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx();
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();
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 tx = psbt.extract_tx();
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();
builder
@ -1432,7 +1448,9 @@ fn test_bump_fee_absolute_reduce_single_recipient() {
let (psbt, original_details) = builder.finish().unwrap();
let tx = psbt.extract_tx();
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();
builder
@ -1471,6 +1489,7 @@ fn test_bump_fee_drain_wallet() {
height: wallet.latest_checkpoint().unwrap().height,
time: 42_000,
},
None,
)
.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 tx = psbt.extract_tx();
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);
// 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
.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();
let outpoint = OutPoint {
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 tx = psbt.extract_tx();
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);
let mut builder = wallet.build_fee_bump(txid).unwrap();
@ -1562,9 +1595,14 @@ fn test_bump_fee_add_input() {
value: 25_000,
}],
};
wallet
.insert_tx(init_tx, wallet.transactions().last().unwrap().0)
.unwrap();
let pos = wallet
.transactions()
.last()
.unwrap()
.observed_as
.cloned()
.into();
wallet.insert_tx(init_tx, pos, None).unwrap();
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
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 tx = psbt.extract_tx();
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();
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 tx = psbt.extract_tx();
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();
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 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
// 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.output.len(), 2);
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();
// 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
}
wallet
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed)
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed, None)
.unwrap();
// 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()`
@ -1839,7 +1885,7 @@ fn test_bump_fee_absolute_force_add_input() {
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
}
wallet
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed)
.insert_tx(tx.clone(), ConfirmationTime::Unconfirmed, None)
.unwrap();
// 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 {
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();
builder.fee_rate(FeeRate::from_sat_per_vb(25.0));
builder.finish().unwrap();
@ -1928,7 +1976,9 @@ fn test_bump_fee_unconfirmed_input() {
for txin in &mut tx.input {
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();
builder
@ -2660,7 +2710,7 @@ fn test_taproot_foreign_utxo() {
let (wallet2, _) = get_funded_wallet(get_test_tr_single_sig());
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 foreign_utxo_satisfaction = wallet2
.get_descriptor_for_keychain(KeychainKind::External)
@ -3022,6 +3072,7 @@ fn test_spend_coinbase() {
height: confirmation_height,
time: 30_000,
},
None,
)
.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.
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
#[cfg_attr(

View File

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

View File

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