From 1adf63cd3bce77b2a6bc2edebf5f54c2d8a191d7 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Mon, 12 Aug 2024 17:26:33 -0500 Subject: [PATCH 01/77] fix(example_cli): add bitcoin and rand dependencies The bitcoin and rand dependencies are required to build examples independently and not from the top level bdk workspace. --- example-crates/example_cli/Cargo.toml | 2 ++ example-crates/example_cli/src/lib.rs | 16 ++++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/example-crates/example_cli/Cargo.toml b/example-crates/example_cli/Cargo.toml index 9b8c4debb..09f093ebf 100644 --- a/example-crates/example_cli/Cargo.toml +++ b/example-crates/example_cli/Cargo.toml @@ -9,8 +9,10 @@ edition = "2021" bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]} bdk_coin_select = "0.3.0" bdk_file_store = { path = "../../crates/file_store" } +bitcoin = { version = "0.32.0", features = ["base64"], default-features = false } anyhow = "1" clap = { version = "3.2.23", features = ["derive", "env"] } +rand = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1.0" diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index ee0c9b376..393f9d3fb 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -10,14 +10,9 @@ use std::sync::Mutex; use anyhow::bail; use anyhow::Context; use bdk_chain::bitcoin::{ - absolute, - address::NetworkUnchecked, - bip32, consensus, constants, - hex::DisplayHex, - relative, - secp256k1::{rand::prelude::*, Secp256k1}, - transaction, Address, Amount, Network, NetworkKind, PrivateKey, Psbt, PublicKey, Sequence, - Transaction, TxIn, TxOut, + absolute, address::NetworkUnchecked, bip32, consensus, constants, hex::DisplayHex, relative, + secp256k1::Secp256k1, transaction, Address, Amount, Network, NetworkKind, PrivateKey, Psbt, + PublicKey, Sequence, Transaction, TxIn, TxOut, }; use bdk_chain::miniscript::{ descriptor::{DescriptorSecretKey, SinglePubKey}, @@ -37,6 +32,7 @@ use bdk_coin_select::{ }; use bdk_file_store::Store; use clap::{Parser, Subcommand}; +use rand::prelude::*; pub use anyhow; pub use clap; @@ -675,7 +671,7 @@ pub fn handle_commands( Ok(()) } PsbtCmd::Sign { psbt, descriptor } => { - let mut psbt = Psbt::from_str(&psbt.unwrap_or_default())?; + let mut psbt = Psbt::from_str(psbt.unwrap_or_default().as_str())?; let desc_str = match descriptor { Some(s) => s, @@ -717,7 +713,7 @@ pub fn handle_commands( chain_specific, psbt, } => { - let mut psbt = Psbt::from_str(&psbt)?; + let mut psbt = Psbt::from_str(psbt.as_str())?; psbt.finalize_mut(&Secp256k1::new()) .map_err(|errors| anyhow::anyhow!("failed to finalize PSBT {errors:?}"))?; From 3675a9e4921f2002f23a32d2f76d65549899edd4 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Mon, 12 Aug 2024 17:30:11 -0500 Subject: [PATCH 02/77] ci: add job to build example-crates independently --- .github/workflows/cont_integration.yml | 29 ++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index f793f7ad6..ef6fc8da1 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -130,3 +130,32 @@ jobs: with: token: ${{ secrets.GITHUB_TOKEN }} args: --all-features --all-targets -- -D warnings + + build-examples: + name: Build Examples + runs-on: ubuntu-latest + strategy: + matrix: + example-dir: + - example_cli + - example_bitcoind_rpc_polling + - example_electrum + - example_esplora + - wallet_electrum + - wallet_esplora_async + - wallet_esplora_blocking + - wallet_rpc + steps: + - name: checkout + uses: actions/checkout@v2 + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + profile: minimal + - name: Rust Cache + uses: Swatinem/rust-cache@v2.2.1 + - name: Build + working-directory: example-crates/${{ matrix.example-dir }} + run: cargo build From 039622fd1de7ee331eceb7a4c77751bd8ecccda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 9 Aug 2024 16:14:15 +0000 Subject: [PATCH 03/77] feat(wallet)!: introduce `WalletPersister` This replaces `bdk_chain::PersistWith` which wanted to persist anything (not only `Wallet`), hence, requiring a whole bunch of generic parameters. Having `WalletPersister` dedicated to persisting `Wallet` simplifies the trait by a lot. In addition, `AsyncWalletPersister` has proper lifetime bounds whereas `bdk_chain::PersistAsyncWith` did not. --- crates/wallet/src/wallet/params.rs | 45 +-- crates/wallet/src/wallet/persisted.rs | 382 +++++++++++++++++++------- crates/wallet/tests/wallet.rs | 31 +-- 3 files changed, 316 insertions(+), 142 deletions(-) diff --git a/crates/wallet/src/wallet/params.rs b/crates/wallet/src/wallet/params.rs index 9b0795395..22e7a5b73 100644 --- a/crates/wallet/src/wallet/params.rs +++ b/crates/wallet/src/wallet/params.rs @@ -1,12 +1,13 @@ use alloc::boxed::Box; -use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith}; +use bdk_chain::keychain_txout::DEFAULT_LOOKAHEAD; use bitcoin::{BlockHash, Network}; use miniscript::descriptor::KeyMap; use crate::{ descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor}, utils::SecpCtx, - KeychainKind, Wallet, + AsyncWalletPersister, CreateWithPersistError, KeychainKind, LoadWithPersistError, Wallet, + WalletPersister, }; use super::{ChangeSet, LoadError, PersistedWallet}; @@ -109,25 +110,25 @@ impl CreateParams { } /// Create [`PersistedWallet`] with the given `Db`. - pub fn create_wallet( + pub fn create_wallet

( self, - db: &mut Db, - ) -> Result>::CreateError> + persister: &mut P, + ) -> Result> where - Wallet: PersistWith, + P: WalletPersister, { - PersistedWallet::create(db, self) + PersistedWallet::create(persister, self) } /// Create [`PersistedWallet`] with the given async `Db`. - pub async fn create_wallet_async( + pub async fn create_wallet_async

( self, - db: &mut Db, - ) -> Result>::CreateError> + persister: &mut P, + ) -> Result> where - Wallet: PersistAsyncWith, + P: AsyncWalletPersister, { - PersistedWallet::create_async(db, self).await + PersistedWallet::create_async(persister, self).await } /// Create [`Wallet`] without persistence. @@ -220,25 +221,25 @@ impl LoadParams { } /// Load [`PersistedWallet`] with the given `Db`. - pub fn load_wallet( + pub fn load_wallet

( self, - db: &mut Db, - ) -> Result, >::LoadError> + persister: &mut P, + ) -> Result, LoadWithPersistError> where - Wallet: PersistWith, + P: WalletPersister, { - PersistedWallet::load(db, self) + PersistedWallet::load(persister, self) } /// Load [`PersistedWallet`] with the given async `Db`. - pub async fn load_wallet_async( + pub async fn load_wallet_async

( self, - db: &mut Db, - ) -> Result, >::LoadError> + persister: &mut P, + ) -> Result, LoadWithPersistError> where - Wallet: PersistAsyncWith, + P: AsyncWalletPersister, { - PersistedWallet::load_async(db, self).await + PersistedWallet::load_async(persister, self).await } /// Load [`Wallet`] without persistence. diff --git a/crates/wallet/src/wallet/persisted.rs b/crates/wallet/src/wallet/persisted.rs index cc9f267f4..a7181a3c0 100644 --- a/crates/wallet/src/wallet/persisted.rs +++ b/crates/wallet/src/wallet/persisted.rs @@ -1,130 +1,305 @@ -use core::fmt; +use core::{ + fmt, + future::Future, + ops::{Deref, DerefMut}, + pin::Pin, +}; -use crate::{descriptor::DescriptorError, Wallet}; +use alloc::boxed::Box; +use chain::{Merge, Staged}; + +use crate::{descriptor::DescriptorError, ChangeSet, CreateParams, LoadParams, Wallet}; + +/// Trait that persists [`Wallet`]. +/// +/// For an async version, use [`AsyncWalletPersister`]. +/// +/// Associated functions of this trait should not be called directly, and the trait is designed so +/// that associated functions are hard to find (since they are not methods!). [`WalletPersister`] is +/// used by [`PersistedWallet`] (a light wrapper around [`Wallet`]) which enforces some level of +/// safety. Refer to [`PersistedWallet`] for more about the safety checks. +pub trait WalletPersister { + /// Error type of the persister. + type Error; + + /// Initialize the `persister` and load all data. + /// + /// This is called by [`PersistedWallet::create`] and [`PersistedWallet::load`] to ensure + /// the [`WalletPersister`] is initialized and returns all data in the `persister`. + /// + /// # Implementation Details + /// + /// The database schema of the `persister` (if any), should be initialized and migrated here. + /// + /// The implementation must return all data currently stored in the `persister`. If there is no + /// data, return an empty changeset (using [`ChangeSet::default()`]). + /// + /// Error should only occur on database failure. Multiple calls to `initialize` should not + /// error. Calling [`persist`] before calling `initialize` should not error either. + /// + /// [`persist`]: WalletPersister::persist + fn initialize(persister: &mut Self) -> Result; + + /// Persist the given `changeset` to the `persister`. + /// + /// This method can fail if the `persister` is not [`initialize`]d. + /// + /// [`initialize`]: WalletPersister::initialize + fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error>; +} + +type FutureResult<'a, T, E> = Pin> + Send + 'a>>; + +/// Async trait that persists [`Wallet`]. +/// +/// For a blocking version, use [`WalletPersister`]. +/// +/// Associated functions of this trait should not be called directly, and the trait is designed so +/// that associated functions are hard to find (since they are not methods!). [`WalletPersister`] is +/// used by [`PersistedWallet`] (a light wrapper around [`Wallet`]) which enforces some level of +/// safety. Refer to [`PersistedWallet`] for more about the safety checks. +pub trait AsyncWalletPersister { + /// Error type of the persister. + type Error; + + /// Initialize the `persister` and load all data. + /// + /// This is called by [`PersistedWallet::create_async`] and [`PersistedWallet::load_async`] to + /// ensure the [`WalletPersister`] is initialized and returns all data in the `persister`. + /// + /// # Implementation Details + /// + /// The database schema of the `persister` (if any), should be initialized and migrated here. + /// + /// The implementation must return all data currently stored in the `persister`. If there is no + /// data, return an empty changeset (using [`ChangeSet::default()`]). + /// + /// Error should only occur on database failure. Multiple calls to `initialize` should not + /// error. Calling [`persist`] before calling `initialize` should not error either. + /// + /// [`persist`]: AsyncWalletPersister::persist + fn initialize<'a>(persister: &'a mut Self) -> FutureResult<'a, ChangeSet, Self::Error> + where + Self: 'a; + + /// Persist the given `changeset` to the `persister`. + /// + /// This method can fail if the `persister` is not [`initialize`]d. + /// + /// [`initialize`]: AsyncWalletPersister::initialize + fn persist<'a>( + persister: &'a mut Self, + changeset: &'a ChangeSet, + ) -> FutureResult<'a, (), Self::Error> + where + Self: 'a; +} /// Represents a persisted wallet. -pub type PersistedWallet = bdk_chain::Persisted; +/// +/// This is a light wrapper around [`Wallet`] that enforces some level of safety-checking when used +/// with a [`WalletPersister`] or [`AsyncWalletPersister`] implementation. Safety checks assume that +/// [`WalletPersister`] and/or [`AsyncWalletPersister`] are implemented correctly. +/// +/// Checks include: +/// +/// * Ensure the persister is initialized before data is persisted. +/// * Ensure there were no previously persisted wallet data before creating a fresh wallet and +/// persisting it. +/// * Only clear the staged changes of [`Wallet`] after persisting succeeds. +#[derive(Debug)] +pub struct PersistedWallet(pub(crate) Wallet); -#[cfg(feature = "rusqlite")] -impl<'c> chain::PersistWith> for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; - - type CreateError = CreateWithPersistError; - type LoadError = LoadWithPersistError; - type PersistError = bdk_chain::rusqlite::Error; - - fn create( - db: &mut bdk_chain::rusqlite::Transaction<'c>, - params: Self::CreateParams, - ) -> Result { - let mut wallet = - Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?; - if let Some(changeset) = wallet.take_staged() { - changeset - .persist_to_sqlite(db) +impl Deref for PersistedWallet { + type Target = Wallet; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for PersistedWallet { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl PersistedWallet { + /// Create a new [`PersistedWallet`] with the given `persister` and `params`. + pub fn create

( + persister: &mut P, + params: CreateParams, + ) -> Result> + where + P: WalletPersister, + { + let existing = P::initialize(persister).map_err(CreateWithPersistError::Persist)?; + if !existing.is_empty() { + return Err(CreateWithPersistError::DataAlreadyExists(existing)); + } + let mut inner = + Wallet::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?; + if let Some(changeset) = inner.take_staged() { + P::persist(persister, &changeset).map_err(CreateWithPersistError::Persist)?; + } + Ok(Self(inner)) + } + + /// Create a new [`PersistedWallet`] witht the given async `persister` and `params`. + pub async fn create_async

( + persister: &mut P, + params: CreateParams, + ) -> Result> + where + P: AsyncWalletPersister, + { + let existing = P::initialize(persister) + .await + .map_err(CreateWithPersistError::Persist)?; + if !existing.is_empty() { + return Err(CreateWithPersistError::DataAlreadyExists(existing)); + } + let mut inner = + Wallet::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?; + if let Some(changeset) = inner.take_staged() { + P::persist(persister, &changeset) + .await .map_err(CreateWithPersistError::Persist)?; } - Ok(wallet) + Ok(Self(inner)) + } + + /// Load a previously [`PersistedWallet`] from the given `persister` and `params`. + pub fn load

( + persister: &mut P, + params: LoadParams, + ) -> Result, LoadWithPersistError> + where + P: WalletPersister, + { + let changeset = P::initialize(persister).map_err(LoadWithPersistError::Persist)?; + Wallet::load_with_params(changeset, params) + .map(|opt| opt.map(PersistedWallet)) + .map_err(LoadWithPersistError::InvalidChangeSet) } - fn load( - conn: &mut bdk_chain::rusqlite::Transaction<'c>, - params: Self::LoadParams, - ) -> Result, Self::LoadError> { - let changeset = - crate::ChangeSet::from_sqlite(conn).map_err(LoadWithPersistError::Persist)?; - if chain::Merge::is_empty(&changeset) { - return Ok(None); + /// Load a previously [`PersistedWallet`] from the given async `persister` and `params`. + pub async fn load_async

( + persister: &mut P, + params: LoadParams, + ) -> Result, LoadWithPersistError> + where + P: AsyncWalletPersister, + { + let changeset = P::initialize(persister) + .await + .map_err(LoadWithPersistError::Persist)?; + Wallet::load_with_params(changeset, params) + .map(|opt| opt.map(PersistedWallet)) + .map_err(LoadWithPersistError::InvalidChangeSet) + } + + /// Persist staged changes of wallet into `persister`. + /// + /// If the `persister` errors, the staged changes will not be cleared. + pub fn persist

(&mut self, persister: &mut P) -> Result + where + P: WalletPersister, + { + let stage = Staged::staged(&mut self.0); + if stage.is_empty() { + return Ok(false); } - Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet) + P::persist(persister, &*stage)?; + stage.take(); + Ok(true) } - fn persist( - db: &mut bdk_chain::rusqlite::Transaction<'c>, - changeset: &::ChangeSet, - ) -> Result<(), Self::PersistError> { - changeset.persist_to_sqlite(db) + /// Persist staged changes of wallet into an async `persister`. + /// + /// If the `persister` errors, the staged changes will not be cleared. + pub async fn persist_async<'a, P>(&'a mut self, persister: &mut P) -> Result + where + P: AsyncWalletPersister, + { + let stage = Staged::staged(&mut self.0); + if stage.is_empty() { + return Ok(false); + } + P::persist(persister, &*stage).await?; + stage.take(); + Ok(true) } } #[cfg(feature = "rusqlite")] -impl chain::PersistWith for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; - - type CreateError = CreateWithPersistError; - type LoadError = LoadWithPersistError; - type PersistError = bdk_chain::rusqlite::Error; - - fn create( - db: &mut bdk_chain::rusqlite::Connection, - params: Self::CreateParams, - ) -> Result { - let mut db_tx = db.transaction().map_err(CreateWithPersistError::Persist)?; - let wallet = chain::PersistWith::create(&mut db_tx, params)?; - db_tx.commit().map_err(CreateWithPersistError::Persist)?; - Ok(wallet) - } - - fn load( - db: &mut bdk_chain::rusqlite::Connection, - params: Self::LoadParams, - ) -> Result, Self::LoadError> { - let mut db_tx = db.transaction().map_err(LoadWithPersistError::Persist)?; - let wallet_opt = chain::PersistWith::load(&mut db_tx, params)?; - db_tx.commit().map_err(LoadWithPersistError::Persist)?; - Ok(wallet_opt) - } - - fn persist( - db: &mut bdk_chain::rusqlite::Connection, - changeset: &::ChangeSet, - ) -> Result<(), Self::PersistError> { - let db_tx = db.transaction()?; +impl<'c> WalletPersister for bdk_chain::rusqlite::Transaction<'c> { + type Error = bdk_chain::rusqlite::Error; + + fn initialize(persister: &mut Self) -> Result { + ChangeSet::from_sqlite(persister) + } + + fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { + changeset.persist_to_sqlite(persister) + } +} + +#[cfg(feature = "rusqlite")] +impl WalletPersister for bdk_chain::rusqlite::Connection { + type Error = bdk_chain::rusqlite::Error; + + fn initialize(persister: &mut Self) -> Result { + let db_tx = persister.transaction()?; + let changeset = ChangeSet::from_sqlite(&db_tx)?; + db_tx.commit()?; + Ok(changeset) + } + + fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { + let db_tx = persister.transaction()?; changeset.persist_to_sqlite(&db_tx)?; db_tx.commit() } } +/// Error for [`bdk_file_store`]'s implementation of [`WalletPersister`]. #[cfg(feature = "file_store")] -impl chain::PersistWith> for Wallet { - type CreateParams = crate::CreateParams; - type LoadParams = crate::LoadParams; - type CreateError = CreateWithPersistError; - type LoadError = - LoadWithPersistError>; - type PersistError = std::io::Error; - - fn create( - db: &mut bdk_file_store::Store, - params: Self::CreateParams, - ) -> Result { - let mut wallet = - Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?; - if let Some(changeset) = wallet.take_staged() { - db.append_changeset(&changeset) - .map_err(CreateWithPersistError::Persist)?; +#[derive(Debug)] +pub enum FileStoreError { + /// Error when loading from the store. + Load(bdk_file_store::AggregateChangesetsError), + /// Error when writing to the store. + Write(std::io::Error), +} + +#[cfg(feature = "file_store")] +impl core::fmt::Display for FileStoreError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use core::fmt::Display; + match self { + FileStoreError::Load(e) => Display::fmt(e, f), + FileStoreError::Write(e) => Display::fmt(e, f), } - Ok(wallet) } +} + +#[cfg(feature = "file_store")] +impl std::error::Error for FileStoreError {} + +#[cfg(feature = "file_store")] +impl WalletPersister for bdk_file_store::Store { + type Error = FileStoreError; - fn load( - db: &mut bdk_file_store::Store, - params: Self::LoadParams, - ) -> Result, Self::LoadError> { - let changeset = db + fn initialize(persister: &mut Self) -> Result { + persister .aggregate_changesets() - .map_err(LoadWithPersistError::Persist)? - .unwrap_or_default(); - Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet) + .map(Option::unwrap_or_default) + .map_err(FileStoreError::Load) } - fn persist( - db: &mut bdk_file_store::Store, - changeset: &::ChangeSet, - ) -> Result<(), Self::PersistError> { - db.append_changeset(changeset) + fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { + persister.append_changeset(changeset).map_err(FileStoreError::Write) } } @@ -154,6 +329,8 @@ impl std::error::Error for LoadWithPersistError pub enum CreateWithPersistError { /// Error from persistence. Persist(E), + /// Persister already has wallet data. + DataAlreadyExists(ChangeSet), /// Occurs when the loaded changeset cannot construct [`Wallet`]. Descriptor(DescriptorError), } @@ -162,6 +339,11 @@ impl fmt::Display for CreateWithPersistError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Persist(err) => fmt::Display::fmt(err, f), + Self::DataAlreadyExists(changeset) => write!( + f, + "Cannot create wallet in persister which already contains wallet data: {:?}", + changeset + ), Self::Descriptor(err) => fmt::Display::fmt(&err, f), } } diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index d41544a1d..32b7a0f77 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -5,15 +5,15 @@ use std::str::FromStr; use anyhow::Context; use assert_matches::assert_matches; +use bdk_chain::COINBASE_MATURITY; use bdk_chain::{BlockId, ConfirmationTime}; -use bdk_chain::{PersistWith, COINBASE_MATURITY}; use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; use bdk_wallet::error::CreateTxError; use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::signer::{SignOptions, SignerError}; use bdk_wallet::tx_builder::AddForeignUtxoError; -use bdk_wallet::{AddressInfo, Balance, CreateParams, LoadParams, Wallet}; +use bdk_wallet::{AddressInfo, Balance, ChangeSet, Wallet, WalletPersister}; use bdk_wallet::{KeychainKind, LoadError, LoadMismatch, LoadWithPersistError}; use bitcoin::constants::ChainHash; use bitcoin::hashes::Hash; @@ -111,10 +111,8 @@ fn wallet_is_persisted() -> anyhow::Result<()> { where CreateDb: Fn(&Path) -> anyhow::Result, OpenDb: Fn(&Path) -> anyhow::Result, - Wallet: PersistWith, - >::CreateError: std::error::Error + Send + Sync + 'static, - >::LoadError: std::error::Error + Send + Sync + 'static, - >::PersistError: std::error::Error + Send + Sync + 'static, + Db: WalletPersister, + Db::Error: std::error::Error + Send + Sync + 'static, { let temp_dir = tempfile::tempdir().expect("must create tempdir"); let file_path = temp_dir.path().join(filename); @@ -188,7 +186,7 @@ fn wallet_is_persisted() -> anyhow::Result<()> { #[test] fn wallet_load_checks() -> anyhow::Result<()> { - fn run( + fn run( filename: &str, create_db: CreateDb, open_db: OpenDb, @@ -196,15 +194,8 @@ fn wallet_load_checks() -> anyhow::Result<()> { where CreateDb: Fn(&Path) -> anyhow::Result, OpenDb: Fn(&Path) -> anyhow::Result, - Wallet: PersistWith< - Db, - CreateParams = CreateParams, - LoadParams = LoadParams, - LoadError = LoadWithPersistError, - >, - >::CreateError: std::error::Error + Send + Sync + 'static, - >::LoadError: std::error::Error + Send + Sync + 'static, - >::PersistError: std::error::Error + Send + Sync + 'static, + Db: WalletPersister, + Db::Error: std::error::Error + Send + Sync + 'static, { let temp_dir = tempfile::tempdir().expect("must create tempdir"); let file_path = temp_dir.path().join(filename); @@ -258,8 +249,8 @@ fn wallet_load_checks() -> anyhow::Result<()> { run( "store.db", - |path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?), - |path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?), + |path| Ok(bdk_file_store::Store::::create_new(DB_MAGIC, path)?), + |path| Ok(bdk_file_store::Store::::open(DB_MAGIC, path)?), )?; run( "store.sqlite", @@ -280,7 +271,7 @@ fn single_descriptor_wallet_persist_and_recover() { let mut db = rusqlite::Connection::open(db_path).unwrap(); let desc = get_test_tr_single_sig_xprv(); - let mut wallet = CreateParams::new_single(desc) + let mut wallet = Wallet::create_single(desc) .network(Network::Testnet) .create_wallet(&mut db) .unwrap(); @@ -4174,7 +4165,7 @@ fn test_insert_tx_balance_and_utxos() { #[test] fn single_descriptor_wallet_can_create_tx_and_receive_change() { // create single descriptor wallet and fund it - let mut wallet = CreateParams::new_single(get_test_tr_single_sig_xprv()) + let mut wallet = Wallet::create_single(get_test_tr_single_sig_xprv()) .network(Network::Testnet) .create_wallet_no_persist() .unwrap(); From 06a9d6c722dd3e324d81ea5b9c2b65ab9f171a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sun, 11 Aug 2024 08:50:54 +0000 Subject: [PATCH 04/77] feat(chain,wallet)!: publicize `.init_sqlite_tables` changeset methods Changeset methods `.persist_to_sqlite` and `from_sqlite` no longer internally call `.init_sqlite_tables`. Instead, it is up to the caller to call `.init_sqlite_tables` beforehand. This allows us to utilize `WalletPersister::initialize`, instead of calling `.init_sqlite_tables` every time we persist/load. --- crates/chain/src/rusqlite_impl.rs | 30 +++++++++++++-------------- crates/wallet/src/wallet/changeset.rs | 16 +++++++------- crates/wallet/src/wallet/persisted.rs | 6 +++++- crates/wallet/tests/wallet.rs | 6 +++++- 4 files changed, 34 insertions(+), 24 deletions(-) diff --git a/crates/chain/src/rusqlite_impl.rs b/crates/chain/src/rusqlite_impl.rs index a52c491c6..d8ef65c42 100644 --- a/crates/chain/src/rusqlite_impl.rs +++ b/crates/chain/src/rusqlite_impl.rs @@ -225,7 +225,7 @@ where pub const ANCHORS_TABLE_NAME: &'static str = "bdk_anchors"; /// Initialize sqlite tables. - fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { + pub fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { let schema_v0: &[&str] = &[ // full transactions &format!( @@ -264,9 +264,9 @@ where } /// Construct a [`TxGraph`] from an sqlite database. + /// + /// Remember to call [`Self::init_sqlite_tables`] beforehand. pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result { - Self::init_sqlite_tables(db_tx)?; - let mut changeset = Self::default(); let mut statement = db_tx.prepare(&format!( @@ -332,9 +332,9 @@ where } /// Persist `changeset` to the sqlite database. + /// + /// Remember to call [`Self::init_sqlite_tables`] beforehand. pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { - Self::init_sqlite_tables(db_tx)?; - let mut statement = db_tx.prepare_cached(&format!( "INSERT INTO {}(txid, raw_tx) VALUES(:txid, :raw_tx) ON CONFLICT(txid) DO UPDATE SET raw_tx=:raw_tx", Self::TXS_TABLE_NAME, @@ -396,7 +396,7 @@ impl local_chain::ChangeSet { pub const BLOCKS_TABLE_NAME: &'static str = "bdk_blocks"; /// Initialize sqlite tables for persisting [`local_chain::LocalChain`]. - fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { + pub fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { let schema_v0: &[&str] = &[ // blocks &format!( @@ -411,9 +411,9 @@ impl local_chain::ChangeSet { } /// Construct a [`LocalChain`](local_chain::LocalChain) from sqlite database. + /// + /// Remember to call [`Self::init_sqlite_tables`] beforehand. pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result { - Self::init_sqlite_tables(db_tx)?; - let mut changeset = Self::default(); let mut statement = db_tx.prepare(&format!( @@ -435,9 +435,9 @@ impl local_chain::ChangeSet { } /// Persist `changeset` to the sqlite database. + /// + /// Remember to call [`Self::init_sqlite_tables`] beforehand. pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { - Self::init_sqlite_tables(db_tx)?; - let mut replace_statement = db_tx.prepare_cached(&format!( "REPLACE INTO {}(block_height, block_hash) VALUES(:block_height, :block_hash)", Self::BLOCKS_TABLE_NAME, @@ -471,7 +471,7 @@ impl keychain_txout::ChangeSet { /// Initialize sqlite tables for persisting /// [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex). - fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { + pub fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { let schema_v0: &[&str] = &[ // last revealed &format!( @@ -487,9 +487,9 @@ impl keychain_txout::ChangeSet { /// Construct [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex) from sqlite database /// and given parameters. + /// + /// Remember to call [`Self::init_sqlite_tables`] beforehand. pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result { - Self::init_sqlite_tables(db_tx)?; - let mut changeset = Self::default(); let mut statement = db_tx.prepare(&format!( @@ -511,9 +511,9 @@ impl keychain_txout::ChangeSet { } /// Persist `changeset` to the sqlite database. + /// + /// Remember to call [`Self::init_sqlite_tables`] beforehand. pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> { - Self::init_sqlite_tables(db_tx)?; - let mut statement = db_tx.prepare_cached(&format!( "REPLACE INTO {}(descriptor_id, last_revealed) VALUES(:descriptor_id, :last_revealed)", Self::LAST_REVEALED_TABLE_NAME, diff --git a/crates/wallet/src/wallet/changeset.rs b/crates/wallet/src/wallet/changeset.rs index 5f3b9b3dc..2d4b700ed 100644 --- a/crates/wallet/src/wallet/changeset.rs +++ b/crates/wallet/src/wallet/changeset.rs @@ -72,10 +72,8 @@ impl ChangeSet { /// Name of table to store wallet descriptors and network. pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet"; - /// Initialize sqlite tables for wallet schema & table. - fn init_wallet_sqlite_tables( - db_tx: &chain::rusqlite::Transaction, - ) -> chain::rusqlite::Result<()> { + /// Initialize sqlite tables for wallet tables. + pub fn init_sqlite_tables(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result<()> { let schema_v0: &[&str] = &[&format!( "CREATE TABLE {} ( \ id INTEGER PRIMARY KEY NOT NULL CHECK (id = 0), \ @@ -85,12 +83,17 @@ impl ChangeSet { ) STRICT;", Self::WALLET_TABLE_NAME, )]; - crate::rusqlite_impl::migrate_schema(db_tx, Self::WALLET_SCHEMA_NAME, &[schema_v0]) + crate::rusqlite_impl::migrate_schema(db_tx, Self::WALLET_SCHEMA_NAME, &[schema_v0])?; + + bdk_chain::local_chain::ChangeSet::init_sqlite_tables(db_tx)?; + bdk_chain::tx_graph::ChangeSet::::init_sqlite_tables(db_tx)?; + bdk_chain::keychain_txout::ChangeSet::init_sqlite_tables(db_tx)?; + + Ok(()) } /// Recover a [`ChangeSet`] from sqlite database. pub fn from_sqlite(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result { - Self::init_wallet_sqlite_tables(db_tx)?; use chain::rusqlite::OptionalExtension; use chain::Impl; @@ -129,7 +132,6 @@ impl ChangeSet { &self, db_tx: &chain::rusqlite::Transaction, ) -> chain::rusqlite::Result<()> { - Self::init_wallet_sqlite_tables(db_tx)?; use chain::rusqlite::named_params; use chain::Impl; diff --git a/crates/wallet/src/wallet/persisted.rs b/crates/wallet/src/wallet/persisted.rs index a7181a3c0..bef34a1f3 100644 --- a/crates/wallet/src/wallet/persisted.rs +++ b/crates/wallet/src/wallet/persisted.rs @@ -237,6 +237,7 @@ impl<'c> WalletPersister for bdk_chain::rusqlite::Transaction<'c> { type Error = bdk_chain::rusqlite::Error; fn initialize(persister: &mut Self) -> Result { + ChangeSet::init_sqlite_tables(&*persister)?; ChangeSet::from_sqlite(persister) } @@ -251,6 +252,7 @@ impl WalletPersister for bdk_chain::rusqlite::Connection { fn initialize(persister: &mut Self) -> Result { let db_tx = persister.transaction()?; + ChangeSet::init_sqlite_tables(&db_tx)?; let changeset = ChangeSet::from_sqlite(&db_tx)?; db_tx.commit()?; Ok(changeset) @@ -299,7 +301,9 @@ impl WalletPersister for bdk_file_store::Store { } fn persist(persister: &mut Self, changeset: &ChangeSet) -> Result<(), Self::Error> { - persister.append_changeset(changeset).map_err(FileStoreError::Write) + persister + .append_changeset(changeset) + .map_err(FileStoreError::Write) } } diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 32b7a0f77..53edd8215 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -249,7 +249,11 @@ fn wallet_load_checks() -> anyhow::Result<()> { run( "store.db", - |path| Ok(bdk_file_store::Store::::create_new(DB_MAGIC, path)?), + |path| { + Ok(bdk_file_store::Store::::create_new( + DB_MAGIC, path, + )?) + }, |path| Ok(bdk_file_store::Store::::open(DB_MAGIC, path)?), )?; run( From a9c5f761c4c6834f8a05f2eb64feb39d23b74c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sun, 11 Aug 2024 09:28:53 +0000 Subject: [PATCH 05/77] feat(wallet)!: remove dependency on `bdk_chain::Staged` Introduce `Wallet::staged_mut` method. --- crates/wallet/src/wallet/mod.rs | 20 +++++++++---------- crates/wallet/src/wallet/persisted.rs | 28 ++++++++++++++------------- 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index f98b16e91..4cd721ba1 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -45,7 +45,6 @@ use bitcoin::{ use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt}; use bitcoin::{constants::genesis_block, Amount}; use bitcoin::{secp256k1::Secp256k1, Weight}; -use chain::Staged; use core::fmt; use core::mem; use core::ops::Deref; @@ -123,14 +122,6 @@ pub struct Wallet { secp: SecpCtx, } -impl Staged for Wallet { - type ChangeSet = ChangeSet; - - fn staged(&mut self) -> &mut Self::ChangeSet { - &mut self.stage - } -} - /// An update to [`Wallet`]. /// /// It updates [`KeychainTxOutIndex`], [`bdk_chain::TxGraph`] and [`local_chain::LocalChain`] atomically. @@ -2303,7 +2294,7 @@ impl Wallet { Ok(()) } - /// Get a reference of the staged [`ChangeSet`] that are yet to be committed (if any). + /// Get a reference of the staged [`ChangeSet`] that is yet to be committed (if any). pub fn staged(&self) -> Option<&ChangeSet> { if self.stage.is_empty() { None @@ -2312,6 +2303,15 @@ impl Wallet { } } + /// Get a mutable reference of the staged [`ChangeSet`] that is yet to be commited (if any). + pub fn staged_mut(&mut self) -> Option<&mut ChangeSet> { + if self.stage.is_empty() { + None + } else { + Some(&mut self.stage) + } + } + /// Take the staged [`ChangeSet`] to be persisted now (if any). pub fn take_staged(&mut self) -> Option { self.stage.take() diff --git a/crates/wallet/src/wallet/persisted.rs b/crates/wallet/src/wallet/persisted.rs index bef34a1f3..cebc3fbd0 100644 --- a/crates/wallet/src/wallet/persisted.rs +++ b/crates/wallet/src/wallet/persisted.rs @@ -6,7 +6,7 @@ use core::{ }; use alloc::boxed::Box; -use chain::{Merge, Staged}; +use chain::Merge; use crate::{descriptor::DescriptorError, ChangeSet, CreateParams, LoadParams, Wallet}; @@ -206,13 +206,14 @@ impl PersistedWallet { where P: WalletPersister, { - let stage = Staged::staged(&mut self.0); - if stage.is_empty() { - return Ok(false); + match self.0.staged_mut() { + Some(stage) => { + P::persist(persister, &*stage)?; + let _ = stage.take(); + Ok(true) + } + None => Ok(false), } - P::persist(persister, &*stage)?; - stage.take(); - Ok(true) } /// Persist staged changes of wallet into an async `persister`. @@ -222,13 +223,14 @@ impl PersistedWallet { where P: AsyncWalletPersister, { - let stage = Staged::staged(&mut self.0); - if stage.is_empty() { - return Ok(false); + match self.0.staged_mut() { + Some(stage) => { + P::persist(persister, &*stage).await?; + let _ = stage.take(); + Ok(true) + } + None => Ok(false), } - P::persist(persister, &*stage).await?; - stage.take(); - Ok(true) } } From 06160574ba3997fa49449a251a8bc046fb1a96a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Sun, 11 Aug 2024 12:40:47 +0000 Subject: [PATCH 06/77] revert(chain)!: rm `persit` module --- crates/chain/src/lib.rs | 2 - crates/chain/src/persist.rs | 169 ------------------------------------ 2 files changed, 171 deletions(-) delete mode 100644 crates/chain/src/persist.rs diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index a756ab11c..3fb8c0eda 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -37,8 +37,6 @@ pub use tx_data_traits::*; pub use tx_graph::TxGraph; mod chain_oracle; pub use chain_oracle::*; -mod persist; -pub use persist::*; #[doc(hidden)] pub mod example_utils; diff --git a/crates/chain/src/persist.rs b/crates/chain/src/persist.rs deleted file mode 100644 index 2ec88f636..000000000 --- a/crates/chain/src/persist.rs +++ /dev/null @@ -1,169 +0,0 @@ -use core::{ - future::Future, - ops::{Deref, DerefMut}, - pin::Pin, -}; - -use alloc::boxed::Box; - -use crate::Merge; - -/// Represents a type that contains staged changes. -pub trait Staged { - /// Type for staged changes. - type ChangeSet: Merge; - - /// Get mutable reference of staged changes. - fn staged(&mut self) -> &mut Self::ChangeSet; -} - -/// Trait that persists the type with `Db`. -/// -/// Methods of this trait should not be called directly. -pub trait PersistWith: Staged + Sized { - /// Parameters for [`PersistWith::create`]. - type CreateParams; - /// Parameters for [`PersistWith::load`]. - type LoadParams; - /// Error type of [`PersistWith::create`]. - type CreateError; - /// Error type of [`PersistWith::load`]. - type LoadError; - /// Error type of [`PersistWith::persist`]. - type PersistError; - - /// Initialize the `Db` and create `Self`. - fn create(db: &mut Db, params: Self::CreateParams) -> Result; - - /// Initialize the `Db` and load a previously-persisted `Self`. - fn load(db: &mut Db, params: Self::LoadParams) -> Result, Self::LoadError>; - - /// Persist changes to the `Db`. - fn persist( - db: &mut Db, - changeset: &::ChangeSet, - ) -> Result<(), Self::PersistError>; -} - -type FutureResult<'a, T, E> = Pin> + Send + 'a>>; - -/// Trait that persists the type with an async `Db`. -pub trait PersistAsyncWith: Staged + Sized { - /// Parameters for [`PersistAsyncWith::create`]. - type CreateParams; - /// Parameters for [`PersistAsyncWith::load`]. - type LoadParams; - /// Error type of [`PersistAsyncWith::create`]. - type CreateError; - /// Error type of [`PersistAsyncWith::load`]. - type LoadError; - /// Error type of [`PersistAsyncWith::persist`]. - type PersistError; - - /// Initialize the `Db` and create `Self`. - fn create(db: &mut Db, params: Self::CreateParams) -> FutureResult; - - /// Initialize the `Db` and load a previously-persisted `Self`. - fn load(db: &mut Db, params: Self::LoadParams) -> FutureResult, Self::LoadError>; - - /// Persist changes to the `Db`. - fn persist<'a>( - db: &'a mut Db, - changeset: &'a ::ChangeSet, - ) -> FutureResult<'a, (), Self::PersistError>; -} - -/// Represents a persisted `T`. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct Persisted { - inner: T, -} - -impl Persisted { - /// Create a new persisted `T`. - pub fn create(db: &mut Db, params: T::CreateParams) -> Result - where - T: PersistWith, - { - T::create(db, params).map(|inner| Self { inner }) - } - - /// Create a new persisted `T` with async `Db`. - pub async fn create_async( - db: &mut Db, - params: T::CreateParams, - ) -> Result - where - T: PersistAsyncWith, - { - T::create(db, params).await.map(|inner| Self { inner }) - } - - /// Construct a persisted `T` from `Db`. - pub fn load(db: &mut Db, params: T::LoadParams) -> Result, T::LoadError> - where - T: PersistWith, - { - Ok(T::load(db, params)?.map(|inner| Self { inner })) - } - - /// Construct a persisted `T` from an async `Db`. - pub async fn load_async( - db: &mut Db, - params: T::LoadParams, - ) -> Result, T::LoadError> - where - T: PersistAsyncWith, - { - Ok(T::load(db, params).await?.map(|inner| Self { inner })) - } - - /// Persist staged changes of `T` into `Db`. - /// - /// If the database errors, the staged changes will not be cleared. - pub fn persist(&mut self, db: &mut Db) -> Result - where - T: PersistWith, - { - let stage = T::staged(&mut self.inner); - if stage.is_empty() { - return Ok(false); - } - T::persist(db, &*stage)?; - stage.take(); - Ok(true) - } - - /// Persist staged changes of `T` into an async `Db`. - /// - /// If the database errors, the staged changes will not be cleared. - pub async fn persist_async<'a, Db>( - &'a mut self, - db: &'a mut Db, - ) -> Result - where - T: PersistAsyncWith, - { - let stage = T::staged(&mut self.inner); - if stage.is_empty() { - return Ok(false); - } - T::persist(db, &*stage).await?; - stage.take(); - Ok(true) - } -} - -impl Deref for Persisted { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.inner - } -} - -impl DerefMut for Persisted { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } -} From 960029324d18dbb51408034efc05b0450867c15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 15 Aug 2024 05:49:18 +0000 Subject: [PATCH 07/77] feat(wallet)!: add persister (`P`) type param to `PersistedWallet

` This forces the caller to use the same persister type that they used for loading/creating when calling `.persist` on `PersistedWallet`. This is not totally fool-proof since we can have multiple instances of the same persister type persisting to different databases. However, it does further enforce some level of safety. --- crates/wallet/src/wallet/params.rs | 12 +-- crates/wallet/src/wallet/persisted.rs | 135 ++++++++++++++------------ crates/wallet/tests/wallet.rs | 2 +- 3 files changed, 79 insertions(+), 70 deletions(-) diff --git a/crates/wallet/src/wallet/params.rs b/crates/wallet/src/wallet/params.rs index 22e7a5b73..d90124724 100644 --- a/crates/wallet/src/wallet/params.rs +++ b/crates/wallet/src/wallet/params.rs @@ -113,7 +113,7 @@ impl CreateParams { pub fn create_wallet

( self, persister: &mut P, - ) -> Result> + ) -> Result, CreateWithPersistError> where P: WalletPersister, { @@ -124,7 +124,7 @@ impl CreateParams { pub async fn create_wallet_async

( self, persister: &mut P, - ) -> Result> + ) -> Result, CreateWithPersistError> where P: AsyncWalletPersister, { @@ -220,22 +220,22 @@ impl LoadParams { self } - /// Load [`PersistedWallet`] with the given `Db`. + /// Load [`PersistedWallet`] with the given `persister`. pub fn load_wallet

( self, persister: &mut P, - ) -> Result, LoadWithPersistError> + ) -> Result>, LoadWithPersistError> where P: WalletPersister, { PersistedWallet::load(persister, self) } - /// Load [`PersistedWallet`] with the given async `Db`. + /// Load [`PersistedWallet`] with the given async `persister`. pub async fn load_wallet_async

( self, persister: &mut P, - ) -> Result, LoadWithPersistError> + ) -> Result>, LoadWithPersistError> where P: AsyncWalletPersister, { diff --git a/crates/wallet/src/wallet/persisted.rs b/crates/wallet/src/wallet/persisted.rs index cebc3fbd0..38d489e27 100644 --- a/crates/wallet/src/wallet/persisted.rs +++ b/crates/wallet/src/wallet/persisted.rs @@ -1,6 +1,7 @@ use core::{ fmt, future::Future, + marker::PhantomData, ops::{Deref, DerefMut}, pin::Pin, }; @@ -10,7 +11,7 @@ use chain::Merge; use crate::{descriptor::DescriptorError, ChangeSet, CreateParams, LoadParams, Wallet}; -/// Trait that persists [`Wallet`]. +/// Trait that persists [`PersistedWallet`]. /// /// For an async version, use [`AsyncWalletPersister`]. /// @@ -50,7 +51,7 @@ pub trait WalletPersister { type FutureResult<'a, T, E> = Pin> + Send + 'a>>; -/// Async trait that persists [`Wallet`]. +/// Async trait that persists [`PersistedWallet`]. /// /// For a blocking version, use [`WalletPersister`]. /// @@ -95,7 +96,7 @@ pub trait AsyncWalletPersister { Self: 'a; } -/// Represents a persisted wallet. +/// Represents a persisted wallet which persists into type `P`. /// /// This is a light wrapper around [`Wallet`] that enforces some level of safety-checking when used /// with a [`WalletPersister`] or [`AsyncWalletPersister`] implementation. Safety checks assume that @@ -107,32 +108,36 @@ pub trait AsyncWalletPersister { /// * Ensure there were no previously persisted wallet data before creating a fresh wallet and /// persisting it. /// * Only clear the staged changes of [`Wallet`] after persisting succeeds. +/// * Ensure the wallet is persisted to the same `P` type as when created/loaded. Note that this is +/// not completely fool-proof as you can have multiple instances of the same `P` type that are +/// connected to different databases. #[derive(Debug)] -pub struct PersistedWallet(pub(crate) Wallet); +pub struct PersistedWallet

{ + inner: Wallet, + marker: PhantomData

, +} -impl Deref for PersistedWallet { +impl

Deref for PersistedWallet

{ type Target = Wallet; fn deref(&self) -> &Self::Target { - &self.0 + &self.inner } } -impl DerefMut for PersistedWallet { +impl

DerefMut for PersistedWallet

{ fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + &mut self.inner } } -impl PersistedWallet { +/// Methods when `P` is a [`WalletPersister`]. +impl PersistedWallet

{ /// Create a new [`PersistedWallet`] with the given `persister` and `params`. - pub fn create

( + pub fn create( persister: &mut P, params: CreateParams, - ) -> Result> - where - P: WalletPersister, - { + ) -> Result> { let existing = P::initialize(persister).map_err(CreateWithPersistError::Persist)?; if !existing.is_empty() { return Err(CreateWithPersistError::DataAlreadyExists(existing)); @@ -142,17 +147,50 @@ impl PersistedWallet { if let Some(changeset) = inner.take_staged() { P::persist(persister, &changeset).map_err(CreateWithPersistError::Persist)?; } - Ok(Self(inner)) + Ok(Self { + inner, + marker: PhantomData, + }) + } + + /// Load a previously [`PersistedWallet`] from the given `persister` and `params`. + pub fn load( + persister: &mut P, + params: LoadParams, + ) -> Result, LoadWithPersistError> { + let changeset = P::initialize(persister).map_err(LoadWithPersistError::Persist)?; + Wallet::load_with_params(changeset, params) + .map(|opt| { + opt.map(|inner| PersistedWallet { + inner, + marker: PhantomData, + }) + }) + .map_err(LoadWithPersistError::InvalidChangeSet) + } + + /// Persist staged changes of wallet into `persister`. + /// + /// If the `persister` errors, the staged changes will not be cleared. + pub fn persist(&mut self, persister: &mut P) -> Result { + match self.inner.staged_mut() { + Some(stage) => { + P::persist(persister, &*stage)?; + let _ = stage.take(); + Ok(true) + } + None => Ok(false), + } } +} +/// Methods when `P` is an [`AsyncWalletPersister`]. +impl PersistedWallet

{ /// Create a new [`PersistedWallet`] witht the given async `persister` and `params`. - pub async fn create_async

( + pub async fn create_async( persister: &mut P, params: CreateParams, - ) -> Result> - where - P: AsyncWalletPersister, - { + ) -> Result> { let existing = P::initialize(persister) .await .map_err(CreateWithPersistError::Persist)?; @@ -166,64 +204,35 @@ impl PersistedWallet { .await .map_err(CreateWithPersistError::Persist)?; } - Ok(Self(inner)) - } - - /// Load a previously [`PersistedWallet`] from the given `persister` and `params`. - pub fn load

( - persister: &mut P, - params: LoadParams, - ) -> Result, LoadWithPersistError> - where - P: WalletPersister, - { - let changeset = P::initialize(persister).map_err(LoadWithPersistError::Persist)?; - Wallet::load_with_params(changeset, params) - .map(|opt| opt.map(PersistedWallet)) - .map_err(LoadWithPersistError::InvalidChangeSet) + Ok(Self { + inner, + marker: PhantomData, + }) } /// Load a previously [`PersistedWallet`] from the given async `persister` and `params`. - pub async fn load_async

( + pub async fn load_async( persister: &mut P, params: LoadParams, - ) -> Result, LoadWithPersistError> - where - P: AsyncWalletPersister, - { + ) -> Result, LoadWithPersistError> { let changeset = P::initialize(persister) .await .map_err(LoadWithPersistError::Persist)?; Wallet::load_with_params(changeset, params) - .map(|opt| opt.map(PersistedWallet)) + .map(|opt| { + opt.map(|inner| PersistedWallet { + inner, + marker: PhantomData, + }) + }) .map_err(LoadWithPersistError::InvalidChangeSet) } - /// Persist staged changes of wallet into `persister`. - /// - /// If the `persister` errors, the staged changes will not be cleared. - pub fn persist

(&mut self, persister: &mut P) -> Result - where - P: WalletPersister, - { - match self.0.staged_mut() { - Some(stage) => { - P::persist(persister, &*stage)?; - let _ = stage.take(); - Ok(true) - } - None => Ok(false), - } - } - /// Persist staged changes of wallet into an async `persister`. /// /// If the `persister` errors, the staged changes will not be cleared. - pub async fn persist_async<'a, P>(&'a mut self, persister: &mut P) -> Result - where - P: AsyncWalletPersister, - { - match self.0.staged_mut() { + pub async fn persist_async<'a>(&'a mut self, persister: &mut P) -> Result { + match self.inner.staged_mut() { Some(stage) => { P::persist(persister, &*stage).await?; let _ = stage.take(); diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 53edd8215..c530e779c 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -194,7 +194,7 @@ fn wallet_load_checks() -> anyhow::Result<()> { where CreateDb: Fn(&Path) -> anyhow::Result, OpenDb: Fn(&Path) -> anyhow::Result, - Db: WalletPersister, + Db: WalletPersister + std::fmt::Debug, Db::Error: std::error::Error + Send + Sync + 'static, { let temp_dir = tempfile::tempdir().expect("must create tempdir"); From eaa1917a46245e177ad3e65b53e34c681f0043d0 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Sun, 5 May 2024 12:24:33 -0300 Subject: [PATCH 08/77] chore: add `print_stdout`/`print_stderr` lints to workspace level --- Cargo.toml | 4 ++++ clippy.toml | 2 +- crates/bitcoind_rpc/Cargo.toml | 3 +++ crates/chain/Cargo.toml | 3 +++ crates/electrum/Cargo.toml | 3 +++ crates/esplora/Cargo.toml | 3 +++ crates/file_store/Cargo.toml | 3 +++ crates/hwi/Cargo.toml | 3 +++ crates/testenv/Cargo.toml | 3 +++ crates/wallet/Cargo.toml | 3 +++ 10 files changed, 29 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 1c29bbaf5..1256dd2cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,7 @@ members = [ [workspace.package] authors = ["Bitcoin Dev Kit Developers"] + +[workspace.lints.clippy] +print_stdout = "deny" +print_stderr = "deny" diff --git a/clippy.toml b/clippy.toml index e3b99604d..69478ceab 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv="1.63.0" \ No newline at end of file +msrv="1.63.0" diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index bee58efa1..09a76b6fa 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -12,6 +12,9 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true + [dependencies] bitcoin = { version = "0.32.0", default-features = false } bitcoincore-rpc = { version = "0.19.0" } diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 7261bdfa2..0651e7180 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -12,6 +12,9 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true + [dependencies] bitcoin = { version = "0.32.0", default-features = false } serde = { version = "1", optional = true, features = ["derive", "rc"] } diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index eff11daac..489d35a5c 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -9,6 +9,9 @@ description = "Fetch data from electrum in the form BDK accepts" license = "MIT OR Apache-2.0" readme = "README.md" +[lints] +workspace = true + [dependencies] bdk_chain = { path = "../chain", version = "0.17.0" } electrum-client = { version = "0.21", features = ["proxy"], default-features = false } diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 9148a0f86..422cf99fb 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -11,6 +11,9 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true + [dependencies] bdk_chain = { path = "../chain", version = "0.17.0", default-features = false } esplora-client = { version = "0.9.0", default-features = false } diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index 3e4c11d0c..50f86c5b8 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -10,6 +10,9 @@ keywords = ["bitcoin", "persist", "persistence", "bdk", "file"] authors = ["Bitcoin Dev Kit Developers"] readme = "README.md" +[lints] +workspace = true + [dependencies] bdk_chain = { path = "../chain", version = "0.17.0", features = [ "serde", "miniscript" ] } bincode = { version = "1" } diff --git a/crates/hwi/Cargo.toml b/crates/hwi/Cargo.toml index b4ae39fe9..154833f1e 100644 --- a/crates/hwi/Cargo.toml +++ b/crates/hwi/Cargo.toml @@ -8,6 +8,9 @@ description = "Utilities to use bdk with hardware wallets" license = "MIT OR Apache-2.0" readme = "README.md" +[lints] +workspace = true + [dependencies] bdk_wallet = { path = "../wallet", version = "1.0.0-beta.1" } hwi = { version = "0.9.0", features = [ "miniscript"] } diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml index 2d9c26caa..48fa1f6b0 100644 --- a/crates/testenv/Cargo.toml +++ b/crates/testenv/Cargo.toml @@ -12,6 +12,9 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lints] +workspace = true + [dependencies] bdk_chain = { path = "../chain", version = "0.17", default-features = false } electrsd = { version = "0.28.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] } diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index 44314a968..aa3531709 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -12,6 +12,9 @@ authors = ["Bitcoin Dev Kit Developers"] edition = "2021" rust-version = "1.63" +[lints] +workspace = true + [dependencies] rand_core = { version = "0.6.0" } miniscript = { version = "12.0.0", features = ["serde"], default-features = false } From b614237127ec1e760414fcc8ee7c4b0c77c62d46 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 15 Aug 2024 11:46:29 -0300 Subject: [PATCH 09/77] fix(tests)!: remove println! usage from tests --- crates/bitcoind_rpc/tests/test_emitter.rs | 10 ++-------- crates/chain/tests/test_local_chain.rs | 12 +++--------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/crates/bitcoind_rpc/tests/test_emitter.rs b/crates/bitcoind_rpc/tests/test_emitter.rs index 3a5c67055..8c41efc03 100644 --- a/crates/bitcoind_rpc/tests/test_emitter.rs +++ b/crates/bitcoind_rpc/tests/test_emitter.rs @@ -36,7 +36,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> { }; // See if the emitter outputs the right blocks. - println!("first sync:"); + while let Some(emission) = emitter.next_block()? { let height = emission.block_height(); let hash = emission.block_hash(); @@ -76,7 +76,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> { .collect::>(); // See if the emitter outputs the right blocks. - println!("after reorg:"); + let mut exp_height = exp_hashes.len() - reorged_blocks.len(); while let Some(emission) = emitter.next_block()? { let height = emission.block_height(); @@ -132,7 +132,6 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> { fn test_into_tx_graph() -> anyhow::Result<()> { let env = TestEnv::new()?; - println!("getting new addresses!"); let addr_0 = env .rpc_client() .get_new_address(None, None)? @@ -145,11 +144,8 @@ fn test_into_tx_graph() -> anyhow::Result<()> { .rpc_client() .get_new_address(None, None)? .assume_checked(); - println!("got new addresses!"); - println!("mining block!"); env.mine_blocks(101, None)?; - println!("mined blocks!"); let (mut chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?); let mut indexed_tx_graph = IndexedTxGraph::::new({ @@ -609,7 +605,6 @@ fn mempool_during_reorg() -> anyhow::Result<()> { // perform reorgs at different heights, these reorgs will not confirm transactions in the // mempool for reorg_count in 1..TIP_DIFF { - println!("REORG COUNT: {}", reorg_count); env.reorg_empty_blocks(reorg_count)?; // This is a map of mempool txids to tip height where the tx was introduced to the mempool @@ -627,7 +622,6 @@ fn mempool_during_reorg() -> anyhow::Result<()> { // `next_header` emits the replacement block of the reorg if let Some(emission) = emitter.next_header()? { let height = emission.block_height(); - println!("\t- replacement height: {}", height); // the mempool emission (that follows the first block emission after reorg) should only // include mempool txs introduced at reorg height or greater diff --git a/crates/chain/tests/test_local_chain.rs b/crates/chain/tests/test_local_chain.rs index 6819e3da1..29686564d 100644 --- a/crates/chain/tests/test_local_chain.rs +++ b/crates/chain/tests/test_local_chain.rs @@ -34,7 +34,6 @@ enum ExpectedResult<'a> { impl<'a> TestLocalChain<'a> { fn run(mut self) { - println!("[TestLocalChain] test: {}", self.name); let got_changeset = match self.chain.apply_update(self.update) { Ok(changeset) => changeset, Err(got_err) => { @@ -255,7 +254,7 @@ fn update_local_chain() { (4, None) ], init_changeset: &[ - (0, Some(h!("_"))), + (0, Some(h!("_"))), (1, Some(h!("B'"))), (2, Some(h!("C'"))), (3, Some(h!("D"))), @@ -437,8 +436,6 @@ fn local_chain_disconnect_from() { ]; for (i, t) in test_cases.into_iter().enumerate() { - println!("Case {}: {}", i, t.name); - let mut chain = t.original; let result = chain.disconnect_from(t.disconnect_from.into()); assert_eq!( @@ -491,7 +488,6 @@ fn checkpoint_from_block_ids() { ]; for (i, t) in test_cases.into_iter().enumerate() { - println!("running test case {}: '{}'", i, t.name); let result = CheckPoint::from_block_ids( t.blocks .iter() @@ -583,6 +579,7 @@ fn checkpoint_query() { fn checkpoint_insert() { struct TestCase<'a> { /// The name of the test. + #[allow(dead_code)] name: &'a str, /// The original checkpoint chain to call [`CheckPoint::insert`] on. chain: &'a [(u32, BlockHash)], @@ -629,9 +626,7 @@ fn checkpoint_insert() { core::iter::once((0, h!("_"))).map(BlockId::from) } - for (i, t) in test_cases.into_iter().enumerate() { - println!("Running [{}] '{}'", i, t.name); - + for t in test_cases.into_iter() { let chain = CheckPoint::from_block_ids( genesis_block().chain(t.chain.iter().copied().map(BlockId::from)), ) @@ -792,7 +787,6 @@ fn local_chain_apply_header_connected_to() { ]; for (i, t) in test_cases.into_iter().enumerate() { - println!("running test case {}: '{}'", i, t.name); let mut chain = t.chain; let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to); let exp_result = t From b32b9447e089709b66f24d5700750f0aa6b8d6e1 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 15 Aug 2024 11:50:20 -0300 Subject: [PATCH 10/77] chore(examples): allow `clippy::print_stdout` in examples --- crates/wallet/examples/compiler.rs | 1 + crates/wallet/examples/mnemonic_to_descriptors.rs | 1 + crates/wallet/examples/policy.rs | 1 + 3 files changed, 3 insertions(+) diff --git a/crates/wallet/examples/compiler.rs b/crates/wallet/examples/compiler.rs index 72bed012e..d0922fa4e 100644 --- a/crates/wallet/examples/compiler.rs +++ b/crates/wallet/examples/compiler.rs @@ -31,6 +31,7 @@ use bdk_wallet::{KeychainKind, Wallet}; /// /// This example demonstrates the interaction between a bdk wallet and miniscript policy. +#[allow(clippy::print_stdout)] fn main() -> Result<(), Box> { // We start with a miniscript policy string let policy_str = "or( diff --git a/crates/wallet/examples/mnemonic_to_descriptors.rs b/crates/wallet/examples/mnemonic_to_descriptors.rs index 76c53cf29..19154c2d7 100644 --- a/crates/wallet/examples/mnemonic_to_descriptors.rs +++ b/crates/wallet/examples/mnemonic_to_descriptors.rs @@ -19,6 +19,7 @@ use std::str::FromStr; /// This example demonstrates how to generate a mnemonic phrase /// using BDK and use that to generate a descriptor string. +#[allow(clippy::print_stdout)] fn main() -> Result<(), anyhow::Error> { let secp = Secp256k1::new(); diff --git a/crates/wallet/examples/policy.rs b/crates/wallet/examples/policy.rs index 6e0c82690..e64d47b53 100644 --- a/crates/wallet/examples/policy.rs +++ b/crates/wallet/examples/policy.rs @@ -26,6 +26,7 @@ use bdk_wallet::signer::SignersContainer; /// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds /// one of the Extend Private key. +#[allow(clippy::print_stdout)] fn main() -> Result<(), Box> { let secp = bitcoin::secp256k1::Secp256k1::new(); From e063ad89bde62e60b10435260e736c66515bf447 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 15 Aug 2024 11:52:49 -0300 Subject: [PATCH 11/77] fix(esplora+wallet+file_store): remove remaining `println!` usage --- crates/esplora/src/async_ext.rs | 10 ++-------- crates/esplora/src/blocking_ext.rs | 11 ++--------- crates/file_store/src/store.rs | 2 -- crates/wallet/src/wallet/coin_selection.rs | 1 - 4 files changed, 4 insertions(+), 20 deletions(-) diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 066b91e17..c236d4908 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -490,6 +490,7 @@ mod test { #[tokio::test] pub async fn test_finalize_chain_update() -> anyhow::Result<()> { struct TestCase<'a> { + #[allow(dead_code)] name: &'a str, /// Initial blockchain height to start the env with. initial_env_height: u32, @@ -526,9 +527,7 @@ mod test { }, ]; - for (i, t) in test_cases.into_iter().enumerate() { - println!("[{}] running test case: {}", i, t.name); - + for t in test_cases.into_iter() { let env = TestEnv::new()?; let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap()); let client = Builder::new(base_url.as_str()).build_async()?; @@ -571,7 +570,6 @@ mod test { chain.apply_update(update)?; chain }; - println!("local chain height: {}", local_chain.tip().height()); // extend env chain if let Some(to_mine) = t @@ -611,10 +609,6 @@ mod test { // apply update let mut updated_local_chain = local_chain.clone(); updated_local_chain.apply_update(update)?; - println!( - "updated local chain height: {}", - updated_local_chain.tip().height() - ); assert!( { diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 6e3e25afe..0806995f1 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -490,6 +490,7 @@ mod test { #[test] pub fn test_finalize_chain_update() -> anyhow::Result<()> { struct TestCase<'a> { + #[allow(dead_code)] name: &'a str, /// Initial blockchain height to start the env with. initial_env_height: u32, @@ -526,9 +527,7 @@ mod test { }, ]; - for (i, t) in test_cases.into_iter().enumerate() { - println!("[{}] running test case: {}", i, t.name); - + for t in test_cases.into_iter() { let env = TestEnv::new()?; let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap()); let client = Builder::new(base_url.as_str()).build_blocking(); @@ -570,7 +569,6 @@ mod test { chain.apply_update(update)?; chain }; - println!("local chain height: {}", local_chain.tip().height()); // extend env chain if let Some(to_mine) = t @@ -609,10 +607,6 @@ mod test { // apply update let mut updated_local_chain = local_chain.clone(); updated_local_chain.apply_update(update)?; - println!( - "updated local chain height: {}", - updated_local_chain.tip().height() - ); assert!( { @@ -773,7 +767,6 @@ mod test { ]; for (i, t) in test_cases.into_iter().enumerate() { - println!("Case {}: {}", i, t.name); let mut chain = t.chain; let mock_anchors = t diff --git a/crates/file_store/src/store.rs b/crates/file_store/src/store.rs index d1bd3c40c..62c3d91b6 100644 --- a/crates/file_store/src/store.rs +++ b/crates/file_store/src/store.rs @@ -340,7 +340,6 @@ mod test { for short_write_len in 1..last_changeset_bytes.len() - 1 { let file_path = temp_dir.path().join(format!("{}.dat", short_write_len)); - println!("Test file: {:?}", file_path); // simulate creating a file, writing data where the last write is incomplete { @@ -406,7 +405,6 @@ mod test { for read_count in 0..changesets.len() { let file_path = temp_dir.path().join(format!("{}.dat", read_count)); - println!("Test file: {:?}", file_path); // First, we create the file with all the changesets! let mut db = Store::::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap(); diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 4cd900378..8dc36b198 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -1579,7 +1579,6 @@ mod test { ]; for (i, t) in test_cases.into_iter().enumerate() { - println!("Case {}: {}", i, t.name); let (required, optional) = filter_duplicates(to_utxo_vec(t.required), to_utxo_vec(t.optional)); assert_eq!( From 295b9794caf142707c49666529268197694534e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 16 Aug 2024 04:21:33 +0000 Subject: [PATCH 12/77] fix(wallet)!: make `LoadParams` implicitly satisfy `Send` --- crates/wallet/src/wallet/mod.rs | 4 ++-- crates/wallet/src/wallet/params.rs | 12 ++++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index f98b16e91..d75c6524b 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -343,7 +343,7 @@ impl Wallet { /// [`reveal_next_address`]: Self::reveal_next_address pub fn create_single(descriptor: D) -> CreateParams where - D: IntoWalletDescriptor + Clone + 'static, + D: IntoWalletDescriptor + Send + Clone + 'static, { CreateParams::new_single(descriptor) } @@ -378,7 +378,7 @@ impl Wallet { /// ``` pub fn create(descriptor: D, change_descriptor: D) -> CreateParams where - D: IntoWalletDescriptor + Clone + 'static, + D: IntoWalletDescriptor + Send + Clone + 'static, { CreateParams::new(descriptor, change_descriptor) } diff --git a/crates/wallet/src/wallet/params.rs b/crates/wallet/src/wallet/params.rs index 9b0795395..8e3402973 100644 --- a/crates/wallet/src/wallet/params.rs +++ b/crates/wallet/src/wallet/params.rs @@ -17,12 +17,13 @@ use super::{ChangeSet, LoadError, PersistedWallet}; /// [object safety rules](https://doc.rust-lang.org/reference/items/traits.html#object-safety). type DescriptorToExtract = Box< dyn FnOnce(&SecpCtx, Network) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> + + Send + 'static, >; fn make_descriptor_to_extract(descriptor: D) -> DescriptorToExtract where - D: IntoWalletDescriptor + 'static, + D: IntoWalletDescriptor + Send + 'static, { Box::new(|secp, network| descriptor.into_wallet_descriptor(secp, network)) } @@ -50,7 +51,7 @@ impl CreateParams { /// /// Use this method only when building a wallet with a single descriptor. See /// also [`Wallet::create_single`]. - pub fn new_single(descriptor: D) -> Self { + pub fn new_single(descriptor: D) -> Self { Self { descriptor: make_descriptor_to_extract(descriptor), descriptor_keymap: KeyMap::default(), @@ -68,7 +69,10 @@ impl CreateParams { /// * `network` = [`Network::Bitcoin`] /// * `genesis_hash` = `None` /// * `lookahead` = [`DEFAULT_LOOKAHEAD`] - pub fn new(descriptor: D, change_descriptor: D) -> Self { + pub fn new( + descriptor: D, + change_descriptor: D, + ) -> Self { Self { descriptor: make_descriptor_to_extract(descriptor), descriptor_keymap: KeyMap::default(), @@ -184,7 +188,7 @@ impl LoadParams { /// for an expected descriptor containing secrets. pub fn descriptor(mut self, keychain: KeychainKind, expected_descriptor: Option) -> Self where - D: IntoWalletDescriptor + 'static, + D: IntoWalletDescriptor + Send + 'static, { let expected = expected_descriptor.map(|d| make_descriptor_to_extract(d)); match keychain { From 340808e820cc144c8b2756dd9c235fad1400ad76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 16 Aug 2024 04:11:20 +0000 Subject: [PATCH 13/77] docs(wallet): fixes/improvements for `persisted` and `params` types --- crates/wallet/src/wallet/params.rs | 8 ++++---- crates/wallet/src/wallet/persisted.rs | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/wallet/src/wallet/params.rs b/crates/wallet/src/wallet/params.rs index d90124724..f91034002 100644 --- a/crates/wallet/src/wallet/params.rs +++ b/crates/wallet/src/wallet/params.rs @@ -109,7 +109,7 @@ impl CreateParams { self } - /// Create [`PersistedWallet`] with the given `Db`. + /// Create [`PersistedWallet`] with the given [`WalletPersister`]. pub fn create_wallet

( self, persister: &mut P, @@ -120,7 +120,7 @@ impl CreateParams { PersistedWallet::create(persister, self) } - /// Create [`PersistedWallet`] with the given async `Db`. + /// Create [`PersistedWallet`] with the given [`AsyncWalletPersister`]. pub async fn create_wallet_async

( self, persister: &mut P, @@ -220,7 +220,7 @@ impl LoadParams { self } - /// Load [`PersistedWallet`] with the given `persister`. + /// Load [`PersistedWallet`] with the given [`WalletPersister`]. pub fn load_wallet

( self, persister: &mut P, @@ -231,7 +231,7 @@ impl LoadParams { PersistedWallet::load(persister, self) } - /// Load [`PersistedWallet`] with the given async `persister`. + /// Load [`PersistedWallet`] with the given [`AsyncWalletPersister`]. pub async fn load_wallet_async

( self, persister: &mut P, diff --git a/crates/wallet/src/wallet/persisted.rs b/crates/wallet/src/wallet/persisted.rs index 38d489e27..a8876e8e4 100644 --- a/crates/wallet/src/wallet/persisted.rs +++ b/crates/wallet/src/wallet/persisted.rs @@ -36,7 +36,10 @@ pub trait WalletPersister { /// data, return an empty changeset (using [`ChangeSet::default()`]). /// /// Error should only occur on database failure. Multiple calls to `initialize` should not - /// error. Calling [`persist`] before calling `initialize` should not error either. + /// error. Calling `initialize` inbetween calls to `persist` should not error. + /// + /// Calling [`persist`] before the `persister` is `initialize`d may error. However, some + /// persister implementations may NOT require initialization at all (and not error). /// /// [`persist`]: WalletPersister::persist fn initialize(persister: &mut Self) -> Result; @@ -56,7 +59,7 @@ type FutureResult<'a, T, E> = Pin> + Send + /// For a blocking version, use [`WalletPersister`]. /// /// Associated functions of this trait should not be called directly, and the trait is designed so -/// that associated functions are hard to find (since they are not methods!). [`WalletPersister`] is +/// that associated functions are hard to find (since they are not methods!). [`AsyncWalletPersister`] is /// used by [`PersistedWallet`] (a light wrapper around [`Wallet`]) which enforces some level of /// safety. Refer to [`PersistedWallet`] for more about the safety checks. pub trait AsyncWalletPersister { @@ -66,7 +69,7 @@ pub trait AsyncWalletPersister { /// Initialize the `persister` and load all data. /// /// This is called by [`PersistedWallet::create_async`] and [`PersistedWallet::load_async`] to - /// ensure the [`WalletPersister`] is initialized and returns all data in the `persister`. + /// ensure the [`AsyncWalletPersister`] is initialized and returns all data in the `persister`. /// /// # Implementation Details /// @@ -76,7 +79,10 @@ pub trait AsyncWalletPersister { /// data, return an empty changeset (using [`ChangeSet::default()`]). /// /// Error should only occur on database failure. Multiple calls to `initialize` should not - /// error. Calling [`persist`] before calling `initialize` should not error either. + /// error. Calling `initialize` inbetween calls to `persist` should not error. + /// + /// Calling [`persist`] before the `persister` is `initialize`d may error. However, some + /// persister implementations may NOT require initialization at all (and not error). /// /// [`persist`]: AsyncWalletPersister::persist fn initialize<'a>(persister: &'a mut Self) -> FutureResult<'a, ChangeSet, Self::Error> @@ -171,6 +177,8 @@ impl PersistedWallet

{ /// Persist staged changes of wallet into `persister`. /// + /// Returns whether any new changes were persisted. + /// /// If the `persister` errors, the staged changes will not be cleared. pub fn persist(&mut self, persister: &mut P) -> Result { match self.inner.staged_mut() { @@ -186,7 +194,7 @@ impl PersistedWallet

{ /// Methods when `P` is an [`AsyncWalletPersister`]. impl PersistedWallet

{ - /// Create a new [`PersistedWallet`] witht the given async `persister` and `params`. + /// Create a new [`PersistedWallet`] with the given async `persister` and `params`. pub async fn create_async( persister: &mut P, params: CreateParams, @@ -230,6 +238,8 @@ impl PersistedWallet

{ /// Persist staged changes of wallet into an async `persister`. /// + /// Returns whether any new changes were persisted. + /// /// If the `persister` errors, the staged changes will not be cleared. pub async fn persist_async<'a>(&'a mut self, persister: &mut P) -> Result { match self.inner.staged_mut() { From b84292787f3b7291b79a1a096a9fc6e5ba33045c Mon Sep 17 00:00:00 2001 From: Praveen Perera Date: Thu, 15 Aug 2024 11:22:37 -0500 Subject: [PATCH 14/77] feat(wallet): Derive Clone on AddressInfo --- crates/wallet/src/wallet/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index f98b16e91..f304e8a67 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -171,7 +171,7 @@ impl From for Update { /// A derived address and the index it was found at. /// For convenience this automatically derefs to `Address` -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct AddressInfo { /// Child index of this address pub index: u32, From 71a3e0e335f0d29249224558118be9edada82be6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 22 Aug 2024 08:39:08 +0000 Subject: [PATCH 15/77] feat(chain): get rid of `TxGraph::determine_changeset` Contain most of the insertion logic in `.insert_{}` methods, thus simplifying `.apply_{}` methods. We can also get rid of `.determine_changeset`. --- crates/chain/src/tx_graph.rs | 224 +++++++++++++--------------- crates/chain/tests/test_tx_graph.rs | 55 ++----- 2 files changed, 112 insertions(+), 167 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 0eab93867..9ab1268b3 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -512,28 +512,66 @@ impl TxGraph { /// /// [`apply_changeset`]: Self::apply_changeset pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { - let mut update = Self::default(); - update.txs.insert( - outpoint.txid, - ( - TxNodeInternal::Partial([(outpoint.vout, txout)].into()), - BTreeSet::new(), - ), - ); - self.apply_update(update) + let mut changeset = ChangeSet::::default(); + let (tx_node, _) = self.txs.entry(outpoint.txid).or_default(); + match tx_node { + TxNodeInternal::Whole(_) => { + // ignore this txout we have the full one already. + // NOTE: You might think putting a debug_assert! here to check the output being + // replaced was actually correct is a good idea but the tests have already been + // written assuming this never panics. + } + TxNodeInternal::Partial(partial_tx) => { + match partial_tx.insert(outpoint.vout, txout.clone()) { + Some(old_txout) => { + debug_assert_eq!( + txout, old_txout, + "txout of the same outpoint should never change" + ); + } + None => { + changeset.txouts.insert(outpoint, txout); + } + } + } + } + changeset } /// Inserts the given transaction into [`TxGraph`]. /// /// The [`ChangeSet`] returned will be empty if `tx` already exists. pub fn insert_tx>>(&mut self, tx: T) -> ChangeSet { - let tx = tx.into(); - let mut update = Self::default(); - update.txs.insert( - tx.compute_txid(), - (TxNodeInternal::Whole(tx), BTreeSet::new()), - ); - self.apply_update(update) + let tx: Arc = tx.into(); + let txid = tx.compute_txid(); + let mut changeset = ChangeSet::::default(); + + let (tx_node, _) = self.txs.entry(txid).or_default(); + match tx_node { + TxNodeInternal::Whole(existing_tx) => { + debug_assert_eq!( + existing_tx.as_ref(), + tx.as_ref(), + "tx of same txid should never change" + ); + } + partial_tx => { + for txin in &tx.input { + // this means the tx is coinbase so there is no previous output + if txin.previous_output.is_null() { + continue; + } + self.spends + .entry(txin.previous_output) + .or_default() + .insert(txid); + } + *partial_tx = TxNodeInternal::Whole(tx.clone()); + changeset.txs.insert(tx); + } + } + + changeset } /// Batch insert unconfirmed transactions. @@ -558,9 +596,17 @@ impl TxGraph { /// The [`ChangeSet`] returned will be empty if graph already knows that `txid` exists in /// `anchor`. pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet { - let mut update = Self::default(); - update.anchors.insert((anchor, txid)); - self.apply_update(update) + let mut changeset = ChangeSet::::default(); + if self.anchors.insert((anchor.clone(), txid)) { + let (_tx_node, anchors) = self.txs.entry(txid).or_default(); + let _inserted = anchors.insert(anchor.clone()); + debug_assert!( + _inserted, + "anchors in `.anchors` and `.txs` should be consistent" + ); + changeset.anchors.insert((anchor, txid)); + } + changeset } /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. @@ -571,9 +617,13 @@ impl TxGraph { /// /// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { - let mut update = Self::default(); - update.last_seen.insert(txid, seen_at); - self.apply_update(update) + let mut changeset = ChangeSet::::default(); + let last_seen = self.last_seen.entry(txid).or_default(); + if seen_at > *last_seen { + *last_seen = seen_at; + changeset.last_seen.insert(txid, seen_at); + } + changeset } /// Update the last seen time for all unconfirmed transactions. @@ -641,124 +691,50 @@ impl TxGraph { /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { - let changeset = self.determine_changeset(update); - self.apply_changeset(changeset.clone()); + let mut changeset = ChangeSet::::default(); + for tx_node in update.full_txs() { + changeset.merge(self.insert_tx(tx_node.tx)); + } + for (outpoint, txout) in update.floating_txouts() { + changeset.merge(self.insert_txout(outpoint, txout.clone())); + } + for (anchor, txid) in &update.anchors { + changeset.merge(self.insert_anchor(*txid, anchor.clone())); + } + for (&txid, &last_seen) in &update.last_seen { + changeset.merge(self.insert_seen_at(txid, last_seen)); + } changeset } /// Determines the [`ChangeSet`] between `self` and an empty [`TxGraph`]. pub fn initial_changeset(&self) -> ChangeSet { - Self::default().determine_changeset(self.clone()) + ChangeSet { + txs: self.full_txs().map(|tx_node| tx_node.tx).collect(), + txouts: self + .floating_txouts() + .map(|(op, txout)| (op, txout.clone())) + .collect(), + anchors: self.anchors.clone(), + last_seen: self.last_seen.iter().map(|(&k, &v)| (k, v)).collect(), + } } /// Applies [`ChangeSet`] to [`TxGraph`]. pub fn apply_changeset(&mut self, changeset: ChangeSet) { - for wrapped_tx in changeset.txs { - let tx = wrapped_tx.as_ref(); - let txid = tx.compute_txid(); - - tx.input - .iter() - .map(|txin| txin.previous_output) - // coinbase spends are not to be counted - .filter(|outpoint| !outpoint.is_null()) - // record spend as this tx has spent this outpoint - .for_each(|outpoint| { - self.spends.entry(outpoint).or_default().insert(txid); - }); - - match self.txs.get_mut(&txid) { - Some((tx_node @ TxNodeInternal::Partial(_), _)) => { - *tx_node = TxNodeInternal::Whole(wrapped_tx.clone()); - } - Some((TxNodeInternal::Whole(tx), _)) => { - debug_assert_eq!( - tx.as_ref().compute_txid(), - txid, - "tx should produce txid that is same as key" - ); - } - None => { - self.txs - .insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new())); - } - } + for tx in changeset.txs { + let _ = self.insert_tx(tx); } - for (outpoint, txout) in changeset.txouts { - let tx_entry = self.txs.entry(outpoint.txid).or_default(); - - match tx_entry { - (TxNodeInternal::Whole(_), _) => { /* do nothing since we already have full tx */ } - (TxNodeInternal::Partial(txouts), _) => { - txouts.insert(outpoint.vout, txout); - } - } + let _ = self.insert_txout(outpoint, txout); } - for (anchor, txid) in changeset.anchors { - if self.anchors.insert((anchor.clone(), txid)) { - let (_, anchors) = self.txs.entry(txid).or_default(); - anchors.insert(anchor); - } + let _ = self.insert_anchor(txid, anchor); } - - for (txid, new_last_seen) in changeset.last_seen { - let last_seen = self.last_seen.entry(txid).or_default(); - if new_last_seen > *last_seen { - *last_seen = new_last_seen; - } + for (txid, seen_at) in changeset.last_seen { + let _ = self.insert_seen_at(txid, seen_at); } } - - /// Previews the resultant [`ChangeSet`] when [`Self`] is updated against the `update` graph. - /// - /// The [`ChangeSet`] would be the set difference between `update` and `self` (transactions that - /// exist in `update` but not in `self`). - pub(crate) fn determine_changeset(&self, update: TxGraph) -> ChangeSet { - let mut changeset = ChangeSet::::default(); - - for (&txid, (update_tx_node, _)) in &update.txs { - match (self.txs.get(&txid), update_tx_node) { - (None, TxNodeInternal::Whole(update_tx)) => { - changeset.txs.insert(update_tx.clone()); - } - (None, TxNodeInternal::Partial(update_txos)) => { - changeset.txouts.extend( - update_txos - .iter() - .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), - ); - } - (Some((TxNodeInternal::Whole(_), _)), _) => {} - (Some((TxNodeInternal::Partial(_), _)), TxNodeInternal::Whole(update_tx)) => { - changeset.txs.insert(update_tx.clone()); - } - ( - Some((TxNodeInternal::Partial(txos), _)), - TxNodeInternal::Partial(update_txos), - ) => { - changeset.txouts.extend( - update_txos - .iter() - .filter(|(vout, _)| !txos.contains_key(*vout)) - .map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())), - ); - } - } - } - - for (txid, update_last_seen) in update.last_seen { - let prev_last_seen = self.last_seen.get(&txid).copied(); - if Some(update_last_seen) > prev_last_seen { - changeset.last_seen.insert(txid, update_last_seen); - } - } - - changeset.anchors = update.anchors.difference(&self.anchors).cloned().collect(); - - changeset - } } impl TxGraph { diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 8ddf7f30a..3ffa82439 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -301,59 +301,28 @@ fn insert_tx_can_retrieve_full_tx_from_graph() { #[test] fn insert_tx_displaces_txouts() { let mut tx_graph = TxGraph::<()>::default(); + let tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, input: vec![], output: vec![TxOut { value: Amount::from_sat(42_000), - script_pubkey: ScriptBuf::new(), + script_pubkey: ScriptBuf::default(), }], }; + let txid = tx.compute_txid(); + let outpoint = OutPoint::new(txid, 0); + let txout = tx.output.first().unwrap(); - let changeset = tx_graph.insert_txout( - OutPoint { - txid: tx.compute_txid(), - vout: 0, - }, - TxOut { - value: Amount::from_sat(1_337_000), - script_pubkey: ScriptBuf::default(), - }, - ); - + let changeset = tx_graph.insert_txout(outpoint, txout.clone()); assert!(!changeset.is_empty()); - let _ = tx_graph.insert_txout( - OutPoint { - txid: tx.compute_txid(), - vout: 0, - }, - TxOut { - value: Amount::from_sat(1_000_000_000), - script_pubkey: ScriptBuf::new(), - }, - ); - - let _changeset = tx_graph.insert_tx(tx.clone()); - - assert_eq!( - tx_graph - .get_txout(OutPoint { - txid: tx.compute_txid(), - vout: 0 - }) - .unwrap() - .value, - Amount::from_sat(42_000) - ); - assert_eq!( - tx_graph.get_txout(OutPoint { - txid: tx.compute_txid(), - vout: 1 - }), - None - ); + let changeset = tx_graph.insert_tx(tx.clone()); + assert_eq!(changeset.txs.len(), 1); + assert!(changeset.txouts.is_empty()); + assert!(tx_graph.get_tx(txid).is_some()); + assert_eq!(tx_graph.get_txout(outpoint), Some(txout)); } #[test] @@ -385,7 +354,7 @@ fn insert_txout_does_not_displace_tx() { let _ = tx_graph.insert_txout( OutPoint { txid: tx.compute_txid(), - vout: 0, + vout: 1, }, TxOut { value: Amount::from_sat(1_000_000_000), From b92f8c9ac1e000ccced89dc597a8ff278842d5ed Mon Sep 17 00:00:00 2001 From: valued mammal Date: Tue, 20 Aug 2024 23:04:07 -0400 Subject: [PATCH 16/77] ci: add cron-update-rust.yml --- .github/workflows/cont_integration.yml | 14 ++++++++- .github/workflows/cron-update-rust.yml | 42 ++++++++++++++++++++++++++ rust-version | 1 + 3 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/cron-update-rust.yml create mode 100644 rust-version diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index ef6fc8da1..b41406617 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -4,6 +4,17 @@ name: CI jobs: + prepare: + runs-on: ubuntu-latest + outputs: + rust_version: ${{ steps.read_toolchain.outputs.rust_version }} + steps: + - name: "Checkout repo" + uses: actions/checkout@v4 + - name: "Read rust version" + id: read_toolchain + run: echo "rust_version=$(cat rust-version)" >> $GITHUB_OUTPUT + build-test: name: Build and test runs-on: ubuntu-latest @@ -116,12 +127,13 @@ jobs: run: cargo fmt --all -- --config format_code_in_doc_comments=true --check clippy_check: + needs: prepare runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - uses: actions-rs/toolchain@v1 with: - toolchain: 1.78.0 + toolchain: ${{ needs.prepare.outputs.rust_version }} components: clippy override: true - name: Rust Cache diff --git a/.github/workflows/cron-update-rust.yml b/.github/workflows/cron-update-rust.yml new file mode 100644 index 000000000..3801f78a8 --- /dev/null +++ b/.github/workflows/cron-update-rust.yml @@ -0,0 +1,42 @@ +name: Update rust version +on: + schedule: + - cron: "0 0 15 * *" # At 00:00 on day-of-month 15. + workflow_dispatch: # allows manual triggering +jobs: + format: + name: Update rustc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Update rust-version to use latest stable + run: | + set -x + # Extract the version from whatever version of the compiler dtolnay/rust-toolchain gives us. + RUST_VERSION=$(rustc --verbose --version | sed -ne 's/^release: //p') + # Update the version in the reference file. + echo "${RUST_VERSION}" > rust-version + echo "rust_version=${RUST_VERSION}" >> $GITHUB_ENV + # In case of no new version don't make an empty PR. + if ! git diff --exit-code > /dev/null; then + echo "Updated rustc. Opening PR." + echo "changes_made=true" >> $GITHUB_ENV + else + echo "Attempted to update rustc but the latest stable date did not change. Not opening any PR." + echo "changes_made=false" >> $GITHUB_ENV + fi + - name: Create Pull Request + if: env.changes_made == 'true' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.GITHUB_TOKEN }} + author: Update Rustc Bot + committer: Update Rustc Bot + branch: create-pull-request/update-rust-version + title: | + ci: automated update to rustc ${{ env.rust_version }} + commit-message: | + ci: automated update to rustc ${{ env.rust_version }} + body: | + Automated update to Github CI workflow `cont_integration.yml` by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action diff --git a/rust-version b/rust-version new file mode 100644 index 000000000..b3a8c61e6 --- /dev/null +++ b/rust-version @@ -0,0 +1 @@ +1.79.0 From 5150801dc5a38cce1571f916499622db755c5ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 06:13:42 +0000 Subject: [PATCH 17/77] feat!: introduce `tx_graph::Update` Instead of updating a `TxGraph` with a `TxGraph`, we introduce a dedicated data object (`tx_graph::Update`). This brings us closer to completing #1543. Co-authored-by: Wei Chen --- crates/chain/src/indexed_tx_graph.rs | 11 +- crates/chain/src/spk_client.rs | 10 +- crates/chain/src/tx_graph.rs | 99 ++++++++-- crates/chain/tests/test_tx_graph.rs | 88 +++------ crates/electrum/src/bdk_electrum_client.rs | 57 +++--- crates/electrum/tests/test_electrum.rs | 61 ++++--- crates/esplora/src/async_ext.rs | 169 ++++++++++-------- crates/esplora/src/blocking_ext.rs | 130 +++++++------- crates/esplora/src/lib.rs | 10 +- crates/esplora/tests/async_ext.rs | 46 +++-- crates/esplora/tests/blocking_ext.rs | 47 +++-- crates/wallet/src/wallet/export.rs | 9 +- crates/wallet/src/wallet/mod.rs | 14 +- crates/wallet/tests/common.rs | 9 +- crates/wallet/tests/wallet.rs | 9 +- example-crates/example_electrum/src/main.rs | 2 +- example-crates/example_esplora/src/main.rs | 4 +- example-crates/wallet_electrum/src/main.rs | 2 +- .../wallet_esplora_async/src/main.rs | 2 +- .../wallet_esplora_blocking/src/main.rs | 2 +- 20 files changed, 439 insertions(+), 342 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 92a08a9ef..d24b1b307 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -91,13 +91,10 @@ where /// Apply an `update` directly. /// /// `update` is a [`TxGraph`] and the resultant changes is returned as [`ChangeSet`]. - pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { - let graph = self.graph.apply_update(update); - let indexer = self.index_tx_graph_changeset(&graph); - ChangeSet { - tx_graph: graph, - indexer, - } + pub fn apply_update(&mut self, update: tx_graph::Update) -> ChangeSet { + let tx_graph = self.graph.apply_update(update); + let indexer = self.index_tx_graph_changeset(&tx_graph); + ChangeSet { tx_graph, indexer } } /// Insert a floating `txout` of given `outpoint`. diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs index 567a8f0a9..e31b431dd 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/chain/src/spk_client.rs @@ -3,7 +3,7 @@ use crate::{ alloc::{boxed::Box, collections::VecDeque, vec::Vec}, collections::BTreeMap, local_chain::CheckPoint, - ConfirmationBlockTime, Indexed, TxGraph, + ConfirmationBlockTime, Indexed, }; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; @@ -345,8 +345,8 @@ impl SyncRequest { #[must_use] #[derive(Debug)] pub struct SyncResult { - /// The update to apply to the receiving [`TxGraph`]. - pub graph_update: TxGraph, + /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). + pub graph_update: crate::tx_graph::Update, /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). pub chain_update: Option, } @@ -497,8 +497,8 @@ impl FullScanRequest { #[derive(Debug)] pub struct FullScanResult { /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). - pub graph_update: TxGraph, - /// The update to apply to the receiving [`TxGraph`]. + pub graph_update: crate::tx_graph::Update, + /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). pub chain_update: Option, /// Last active indices for the corresponding keychains (`K`). pub last_active_indices: BTreeMap, diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 9ab1268b3..ba894fa93 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -70,13 +70,17 @@ //! //! ``` //! # use bdk_chain::{Merge, BlockId}; -//! # use bdk_chain::tx_graph::TxGraph; +//! # use bdk_chain::tx_graph::{self, TxGraph}; //! # use bdk_chain::example_utils::*; //! # use bitcoin::Transaction; +//! # use std::sync::Arc; //! # let tx_a = tx_from_hex(RAW_TX_1); //! # let tx_b = tx_from_hex(RAW_TX_2); //! let mut graph: TxGraph = TxGraph::default(); -//! let update = TxGraph::new(vec![tx_a, tx_b]); +//! +//! let mut update = tx_graph::Update::default(); +//! update.txs.push(Arc::new(tx_a)); +//! update.txs.push(Arc::new(tx_b)); //! //! // apply the update graph //! let changeset = graph.apply_update(update.clone()); @@ -101,6 +105,79 @@ use core::{ ops::{Deref, RangeInclusive}, }; +/// Data object used to update the [`TxGraph`] with. +#[derive(Debug, Clone)] +pub struct Update { + /// Full transactions. + pub txs: Vec>, + /// Floating txouts. + pub txouts: BTreeMap, + /// Transaction anchors. + pub anchors: BTreeSet<(A, Txid)>, + /// Seen at times for transactions. + pub seen_ats: HashMap, +} + +impl Default for Update { + fn default() -> Self { + Self { + txs: Default::default(), + txouts: Default::default(), + anchors: Default::default(), + seen_ats: Default::default(), + } + } +} + +impl From> for Update { + fn from(graph: TxGraph) -> Self { + Self { + txs: graph.full_txs().map(|tx_node| tx_node.tx).collect(), + txouts: graph + .floating_txouts() + .map(|(op, txo)| (op, txo.clone())) + .collect(), + anchors: graph.anchors, + seen_ats: graph.last_seen.into_iter().collect(), + } + } +} + +impl From> for TxGraph { + fn from(update: Update) -> Self { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(update); + graph + } +} + +impl Update { + /// Update the [`seen_ats`](Self::seen_ats) for all unanchored transactions. + pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) { + let seen_ats = &mut self.seen_ats; + let anchors = &self.anchors; + let unanchored_txids = self.txs.iter().map(|tx| tx.compute_txid()).filter(|txid| { + for (_, anchor_txid) in anchors { + if txid == anchor_txid { + return false; + } + } + true + }); + for txid in unanchored_txids { + seen_ats.insert(txid, seen_at); + } + } + + /// Extend this update with `other`. + pub fn extend(&mut self, other: Update) { + self.txs.extend(other.txs); + self.txouts.extend(other.txouts); + self.anchors.extend(other.anchors); + self.seen_ats.extend(other.seen_ats); + } +} + /// A graph of transactions and spends. /// /// See the [module-level documentation] for more. @@ -690,19 +767,19 @@ impl TxGraph { /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). - pub fn apply_update(&mut self, update: TxGraph) -> ChangeSet { + pub fn apply_update(&mut self, update: Update) -> ChangeSet { let mut changeset = ChangeSet::::default(); - for tx_node in update.full_txs() { - changeset.merge(self.insert_tx(tx_node.tx)); + for tx in update.txs { + changeset.merge(self.insert_tx(tx)); } - for (outpoint, txout) in update.floating_txouts() { - changeset.merge(self.insert_txout(outpoint, txout.clone())); + for (outpoint, txout) in update.txouts { + changeset.merge(self.insert_txout(outpoint, txout)); } - for (anchor, txid) in &update.anchors { - changeset.merge(self.insert_anchor(*txid, anchor.clone())); + for (anchor, txid) in update.anchors { + changeset.merge(self.insert_anchor(txid, anchor)); } - for (&txid, &last_seen) in &update.last_seen { - changeset.merge(self.insert_seen_at(txid, last_seen)); + for (txid, seen_at) in update.seen_ats { + changeset.merge(self.insert_seen_at(txid, seen_at)); } changeset } diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 3ffa82439..c6399f53b 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,7 +2,7 @@ #[macro_use] mod common; -use bdk_chain::tx_graph::CalculateFeeError; +use bdk_chain::tx_graph::{self, CalculateFeeError}; use bdk_chain::{ collections::*, local_chain::LocalChain, @@ -49,7 +49,7 @@ fn insert_txouts() { )]; // One full transaction to be included in the update - let update_txs = Transaction { + let update_tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, input: vec![TxIn { @@ -63,17 +63,17 @@ fn insert_txouts() { }; // Conf anchor used to mark the full transaction as confirmed. - let conf_anchor = ChainPosition::Confirmed(BlockId { + let conf_anchor = BlockId { height: 100, hash: h!("random blockhash"), - }); + }; - // Unconfirmed anchor to mark the partial transactions as unconfirmed - let unconf_anchor = ChainPosition::::Unconfirmed(1000000); + // Unconfirmed seen_at timestamp to mark the partial transactions as unconfirmed. + let unconf_seen_at = 1000000_u64; // Make the original graph let mut graph = { - let mut graph = TxGraph::>::default(); + let mut graph = TxGraph::::default(); for (outpoint, txout) in &original_ops { assert_eq!( graph.insert_txout(*outpoint, txout.clone()), @@ -88,57 +88,21 @@ fn insert_txouts() { // Make the update graph let update = { - let mut graph = TxGraph::default(); + let mut update = tx_graph::Update::default(); for (outpoint, txout) in &update_ops { - // Insert partials transactions - assert_eq!( - graph.insert_txout(*outpoint, txout.clone()), - ChangeSet { - txouts: [(*outpoint, txout.clone())].into(), - ..Default::default() - } - ); + // Insert partials transactions. + update.txouts.insert(*outpoint, txout.clone()); // Mark them unconfirmed. - assert_eq!( - graph.insert_anchor(outpoint.txid, unconf_anchor), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [(unconf_anchor, outpoint.txid)].into(), - last_seen: [].into() - } - ); - // Mark them last seen at. - assert_eq!( - graph.insert_seen_at(outpoint.txid, 1000000), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [].into(), - last_seen: [(outpoint.txid, 1000000)].into() - } - ); + update.seen_ats.insert(outpoint.txid, unconf_seen_at); } - // Insert the full transaction - assert_eq!( - graph.insert_tx(update_txs.clone()), - ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), - ..Default::default() - } - ); + // Insert the full transaction. + update.txs.push(update_tx.clone().into()); // Mark it as confirmed. - assert_eq!( - graph.insert_anchor(update_txs.compute_txid(), conf_anchor), - ChangeSet { - txs: [].into(), - txouts: [].into(), - anchors: [(conf_anchor, update_txs.compute_txid())].into(), - last_seen: [].into() - } - ); - graph + update + .anchors + .insert((conf_anchor, update_tx.compute_txid())); + update }; // Check the resulting addition. @@ -147,13 +111,9 @@ fn insert_txouts() { assert_eq!( changeset, ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), + txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.clone().into(), - anchors: [ - (conf_anchor, update_txs.compute_txid()), - (unconf_anchor, h!("tx2")) - ] - .into(), + anchors: [(conf_anchor, update_tx.compute_txid()),].into(), last_seen: [(h!("tx2"), 1000000)].into() } ); @@ -188,7 +148,7 @@ fn insert_txouts() { assert_eq!( graph - .tx_outputs(update_txs.compute_txid()) + .tx_outputs(update_tx.compute_txid()) .expect("should exists"), [( 0u32, @@ -204,13 +164,9 @@ fn insert_txouts() { assert_eq!( graph.initial_changeset(), ChangeSet { - txs: [Arc::new(update_txs.clone())].into(), + txs: [Arc::new(update_tx.clone())].into(), txouts: update_ops.into_iter().chain(original_ops).collect(), - anchors: [ - (conf_anchor, update_txs.compute_txid()), - (unconf_anchor, h!("tx2")) - ] - .into(), + anchors: [(conf_anchor, update_tx.compute_txid()),].into(), last_seen: [(h!("tx2"), 1000000)].into() } ); diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 1458e2bd9..571660756 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -3,12 +3,12 @@ use bdk_chain::{ collections::{BTreeMap, HashMap}, local_chain::CheckPoint, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, - tx_graph::TxGraph, + tx_graph::{self, TxGraph}, Anchor, BlockId, ConfirmationBlockTime, }; use electrum_client::{ElectrumApi, Error, HeaderNotification}; use std::{ - collections::BTreeSet, + collections::HashSet, sync::{Arc, Mutex}, }; @@ -138,7 +138,7 @@ impl BdkElectrumClient { None => None, }; - let mut graph_update = TxGraph::::default(); + let mut graph_update = tx_graph::Update::::default(); let mut last_active_indices = BTreeMap::::default(); for keychain in request.keychains() { let spks = request.iter_spks(keychain.clone()); @@ -158,7 +158,7 @@ impl BdkElectrumClient { Some((chain_tip, latest_blocks)) => Some(chain_update( chain_tip, &latest_blocks, - graph_update.all_anchors(), + graph_update.anchors.iter().cloned(), )?), _ => None, }; @@ -205,7 +205,7 @@ impl BdkElectrumClient { None => None, }; - let mut graph_update = TxGraph::::default(); + let mut graph_update = tx_graph::Update::::default(); self.populate_with_spks( &mut graph_update, request @@ -227,7 +227,7 @@ impl BdkElectrumClient { Some((chain_tip, latest_blocks)) => Some(chain_update( chain_tip, &latest_blocks, - graph_update.all_anchors(), + graph_update.anchors.iter().cloned(), )?), None => None, }; @@ -245,7 +245,7 @@ impl BdkElectrumClient { /// also included. fn populate_with_spks( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, mut spks: impl Iterator, stop_gap: usize, batch_size: usize, @@ -278,7 +278,7 @@ impl BdkElectrumClient { } for tx_res in spk_history { - let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?); + graph_update.txs.push(self.fetch_tx(tx_res.tx_hash)?); self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?; } } @@ -291,7 +291,7 @@ impl BdkElectrumClient { /// included. Anchors of the aforementioned transactions are included. fn populate_with_outpoints( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, outpoints: impl IntoIterator, ) -> Result<(), Error> { for outpoint in outpoints { @@ -314,7 +314,7 @@ impl BdkElectrumClient { if !has_residing && res.tx_hash == op_txid { has_residing = true; - let _ = graph_update.insert_tx(Arc::clone(&op_tx)); + graph_update.txs.push(Arc::clone(&op_tx)); self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; } @@ -328,7 +328,7 @@ impl BdkElectrumClient { if !has_spending { continue; } - let _ = graph_update.insert_tx(Arc::clone(&res_tx)); + graph_update.txs.push(Arc::clone(&res_tx)); self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; } } @@ -339,7 +339,7 @@ impl BdkElectrumClient { /// Populate the `graph_update` with transactions/anchors of the provided `txids`. fn populate_with_txids( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, txids: impl IntoIterator, ) -> Result<(), Error> { for txid in txids { @@ -366,7 +366,7 @@ impl BdkElectrumClient { self.validate_merkle_for_anchor(graph_update, txid, r.height)?; } - let _ = graph_update.insert_tx(tx); + graph_update.txs.push(tx); } Ok(()) } @@ -375,7 +375,7 @@ impl BdkElectrumClient { // An anchor is inserted if the transaction is validated to be in a confirmed block. fn validate_merkle_for_anchor( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, txid: Txid, confirmation_height: i32, ) -> Result<(), Error> { @@ -402,8 +402,7 @@ impl BdkElectrumClient { } if is_confirmed_tx { - let _ = graph_update.insert_anchor( - txid, + graph_update.anchors.insert(( ConfirmationBlockTime { confirmation_time: header.time as u64, block_id: BlockId { @@ -411,7 +410,8 @@ impl BdkElectrumClient { hash: header.block_hash(), }, }, - ); + txid, + )); } } Ok(()) @@ -421,17 +421,18 @@ impl BdkElectrumClient { // which we do not have by default. This data is needed to calculate the transaction fee. fn fetch_prev_txout( &self, - graph_update: &mut TxGraph, + graph_update: &mut tx_graph::Update, ) -> Result<(), Error> { - let full_txs: Vec> = - graph_update.full_txs().map(|tx_node| tx_node.tx).collect(); - for tx in full_txs { - for vin in &tx.input { - let outpoint = vin.previous_output; - let vout = outpoint.vout; - let prev_tx = self.fetch_tx(outpoint.txid)?; - let txout = prev_tx.output[vout as usize].clone(); - let _ = graph_update.insert_txout(outpoint, txout); + let mut no_dup = HashSet::::new(); + for tx in &graph_update.txs { + if no_dup.insert(tx.compute_txid()) { + for vin in &tx.input { + let outpoint = vin.previous_output; + let vout = outpoint.vout; + let prev_tx = self.fetch_tx(outpoint.txid)?; + let txout = prev_tx.output[vout as usize].clone(); + let _ = graph_update.txouts.insert(outpoint, txout); + } } } Ok(()) @@ -516,7 +517,7 @@ fn fetch_tip_and_latest_blocks( fn chain_update( mut tip: CheckPoint, latest_blocks: &BTreeMap, - anchors: &BTreeSet<(A, Txid)>, + anchors: impl Iterator, ) -> Result { for anchor in anchors { let height = anchor.0.anchor_block().height; diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index 63e91081b..e8b054d33 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -1,9 +1,9 @@ use bdk_chain::{ - bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash}, + bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash}, local_chain::LocalChain, spk_client::{FullScanRequest, SyncRequest, SyncResult}, spk_txout::SpkTxOutIndex, - Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, + Balance, ConfirmationBlockTime, IndexedTxGraph, Indexer, Merge, TxGraph, }; use bdk_electrum::BdkElectrumClient; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; @@ -49,7 +49,7 @@ where .elapsed() .expect("must get time") .as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); if let Some(chain_update) = update.chain_update.clone() { let _ = chain @@ -128,18 +128,23 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -151,12 +156,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); - + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions", + ); Ok(()) } @@ -216,7 +224,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1, false)? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -227,10 +235,10 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -259,8 +267,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -273,8 +282,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); @@ -475,13 +485,12 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { )?; // Retain a snapshot of all anchors before reorg process. - let initial_anchors = update.graph_update.all_anchors(); - let anchors: Vec<_> = initial_anchors.iter().cloned().collect(); - assert_eq!(anchors.len(), REORG_COUNT); + let initial_anchors = update.graph_update.anchors.clone(); + assert_eq!(initial_anchors.len(), REORG_COUNT); for i in 0..REORG_COUNT { - let (anchor, txid) = anchors[i]; + let (anchor, txid) = initial_anchors.iter().nth(i).unwrap(); assert_eq!(anchor.block_id.hash, hashes[i]); - assert_eq!(txid, txids[i]); + assert_eq!(*txid, txids[i]); } // Check if initial balance is correct. @@ -507,7 +516,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { )?; // Check that no new anchors are added during current reorg. - assert!(initial_anchors.is_superset(update.graph_update.all_anchors())); + assert!(initial_anchors.is_superset(&update.graph_update.anchors)); assert_eq!( get_balance(&recv_chain, &recv_graph)?, diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 066b91e17..f3c8e966a 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashSet}; use async_trait::async_trait; use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; @@ -6,10 +6,9 @@ use bdk_chain::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, collections::BTreeMap, local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, TxGraph, + BlockId, ConfirmationBlockTime, }; -use bdk_chain::{Anchor, Indexed}; -use esplora_client::{Tx, TxStatus}; +use bdk_chain::{tx_graph, Anchor, Indexed}; use futures::{stream::FuturesOrdered, TryStreamExt}; use crate::{insert_anchor_from_status, insert_prevouts}; @@ -72,23 +71,29 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { None }; - let mut graph_update = TxGraph::default(); + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in keychains { let keychain_spks = request.iter_spks(keychain.clone()); - let (tx_graph, last_active_index) = - fetch_txs_with_keychain_spks(self, keychain_spks, stop_gap, parallel_requests) - .await?; - let _ = graph_update.apply_update(tx_graph); + let (update, last_active_index) = fetch_txs_with_keychain_spks( + self, + &mut inserted_txs, + keychain_spks, + stop_gap, + parallel_requests, + ) + .await?; + graph_update.extend(update); if let Some(last_active_index) = last_active_index { last_active_indices.insert(keychain, last_active_index); } } let chain_update = match (chain_tip, latest_blocks) { - (Some(chain_tip), Some(latest_blocks)) => Some( - chain_update(self, &latest_blocks, &chain_tip, graph_update.all_anchors()).await?, - ), + (Some(chain_tip), Some(latest_blocks)) => { + Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?) + } _ => None, }; @@ -113,20 +118,40 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { None }; - let mut graph_update = TxGraph::::default(); - let _ = graph_update - .apply_update(fetch_txs_with_spks(self, request.iter_spks(), parallel_requests).await?); - let _ = graph_update.apply_update( - fetch_txs_with_txids(self, request.iter_txids(), parallel_requests).await?, + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); + graph_update.extend( + fetch_txs_with_spks( + self, + &mut inserted_txs, + request.iter_spks(), + parallel_requests, + ) + .await?, + ); + graph_update.extend( + fetch_txs_with_txids( + self, + &mut inserted_txs, + request.iter_txids(), + parallel_requests, + ) + .await?, ); - let _ = graph_update.apply_update( - fetch_txs_with_outpoints(self, request.iter_outpoints(), parallel_requests).await?, + graph_update.extend( + fetch_txs_with_outpoints( + self, + &mut inserted_txs, + request.iter_outpoints(), + parallel_requests, + ) + .await?, ); let chain_update = match (chain_tip, latest_blocks) { - (Some(chain_tip), Some(latest_blocks)) => Some( - chain_update(self, &latest_blocks, &chain_tip, graph_update.all_anchors()).await?, - ), + (Some(chain_tip), Some(latest_blocks)) => { + Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?) + } _ => None, }; @@ -252,13 +277,14 @@ async fn chain_update( /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_keychain_spks> + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, mut keychain_spks: I, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, Option), Error> { +) -> Result<(tx_graph::Update, Option), Error> { type TxsOfSpkIndex = (u32, Vec); - let mut tx_graph = TxGraph::default(); + let mut update = tx_graph::Update::::default(); let mut last_index = Option::::None; let mut last_active_index = Option::::None; @@ -294,9 +320,11 @@ async fn fetch_txs_with_keychain_spks> + S last_active_index = Some(index); } for tx in txs { - let _ = tx_graph.insert_tx(tx.to_tx()); - insert_anchor_from_status(&mut tx_graph, tx.txid, tx.status); - insert_prevouts(&mut tx_graph, tx.vin); + if inserted_txs.insert(tx.txid) { + update.txs.push(tx.to_tx().into()); + } + insert_anchor_from_status(&mut update, tx.txid, tx.status); + insert_prevouts(&mut update, tx.vin); } } @@ -311,7 +339,7 @@ async fn fetch_txs_with_keychain_spks> + S } } - Ok((tx_graph, last_active_index)) + Ok((update, last_active_index)) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `spks` @@ -324,20 +352,22 @@ async fn fetch_txs_with_keychain_spks> + S /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_spks + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, spks: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { fetch_txs_with_keychain_spks( client, + inserted_txs, spks.into_iter().enumerate().map(|(i, spk)| (i as u32, spk)), usize::MAX, parallel_requests, ) .await - .map(|(tx_graph, _)| tx_graph) + .map(|(update, _)| update) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `txids` @@ -348,39 +378,27 @@ where /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_txids + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, txids: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { - enum EsploraResp { - TxStatus(TxStatus), - Tx(Option), - } - - let mut tx_graph = TxGraph::default(); - let mut txids = txids.into_iter(); + let mut update = tx_graph::Update::::default(); + // Only fetch for non-inserted txs. + let mut txids = txids + .into_iter() + .filter(|txid| !inserted_txs.contains(txid)) + .collect::>() + .into_iter(); loop { let handles = txids .by_ref() .take(parallel_requests) .map(|txid| { let client = client.clone(); - let tx_already_exists = tx_graph.get_tx(txid).is_some(); - async move { - if tx_already_exists { - client - .get_tx_status(&txid) - .await - .map(|s| (txid, EsploraResp::TxStatus(s))) - } else { - client - .get_tx_info(&txid) - .await - .map(|t| (txid, EsploraResp::Tx(t))) - } - } + async move { client.get_tx_info(&txid).await.map(|t| (txid, t)) } }) .collect::>(); @@ -388,21 +406,17 @@ where break; } - for (txid, resp) in handles.try_collect::>().await? { - match resp { - EsploraResp::TxStatus(status) => { - insert_anchor_from_status(&mut tx_graph, txid, status); - } - EsploraResp::Tx(Some(tx_info)) => { - let _ = tx_graph.insert_tx(tx_info.to_tx()); - insert_anchor_from_status(&mut tx_graph, txid, tx_info.status); - insert_prevouts(&mut tx_graph, tx_info.vin); + for (txid, tx_info) in handles.try_collect::>().await? { + if let Some(tx_info) = tx_info { + if inserted_txs.insert(txid) { + update.txs.push(tx_info.to_tx().into()); } - _ => continue, + insert_anchor_from_status(&mut update, txid, tx_info.status); + insert_prevouts(&mut update, tx_info.vin); } } } - Ok(tx_graph) + Ok(update) } /// Fetch transactions and [`ConfirmationBlockTime`]s that contain and spend the provided @@ -413,22 +427,27 @@ where /// Refer to [crate-level docs](crate) for more. async fn fetch_txs_with_outpoints + Send>( client: &esplora_client::AsyncClient, + inserted_txs: &mut HashSet, outpoints: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { let outpoints = outpoints.into_iter().collect::>(); + let mut update = tx_graph::Update::::default(); // make sure txs exists in graph and tx statuses are updated // TODO: We should maintain a tx cache (like we do with Electrum). - let mut tx_graph = fetch_txs_with_txids( - client, - outpoints.iter().copied().map(|op| op.txid), - parallel_requests, - ) - .await?; + update.extend( + fetch_txs_with_txids( + client, + inserted_txs, + outpoints.iter().copied().map(|op| op.txid), + parallel_requests, + ) + .await?, + ); // get outpoint spend-statuses let mut outpoints = outpoints.into_iter(); @@ -452,18 +471,18 @@ where Some(txid) => txid, None => continue, }; - if tx_graph.get_tx(spend_txid).is_none() { + if !inserted_txs.contains(&spend_txid) { missing_txs.push(spend_txid); } if let Some(spend_status) = op_status.status { - insert_anchor_from_status(&mut tx_graph, spend_txid, spend_status); + insert_anchor_from_status(&mut update, spend_txid, spend_status); } } } - let _ = - tx_graph.apply_update(fetch_txs_with_txids(client, missing_txs, parallel_requests).await?); - Ok(tx_graph) + update + .extend(fetch_txs_with_txids(client, inserted_txs, missing_txs, parallel_requests).await?); + Ok(update) } #[cfg(test)] diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 6e3e25afe..62f0d351e 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashSet}; use std::thread::JoinHandle; use bdk_chain::collections::BTreeMap; @@ -6,10 +6,10 @@ use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncRe use bdk_chain::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, TxGraph, + BlockId, ConfirmationBlockTime, }; -use bdk_chain::{Anchor, Indexed}; -use esplora_client::{OutputStatus, Tx, TxStatus}; +use bdk_chain::{tx_graph, Anchor, Indexed}; +use esplora_client::{OutputStatus, Tx}; use crate::{insert_anchor_from_status, insert_prevouts}; @@ -66,13 +66,19 @@ impl EsploraExt for esplora_client::BlockingClient { None }; - let mut graph_update = TxGraph::default(); + let mut graph_update = tx_graph::Update::default(); + let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in request.keychains() { let keychain_spks = request.iter_spks(keychain.clone()); - let (tx_graph, last_active_index) = - fetch_txs_with_keychain_spks(self, keychain_spks, stop_gap, parallel_requests)?; - let _ = graph_update.apply_update(tx_graph); + let (update, last_active_index) = fetch_txs_with_keychain_spks( + self, + &mut inserted_txs, + keychain_spks, + stop_gap, + parallel_requests, + )?; + graph_update.extend(update); if let Some(last_active_index) = last_active_index { last_active_indices.insert(keychain, last_active_index); } @@ -83,7 +89,7 @@ impl EsploraExt for esplora_client::BlockingClient { self, &latest_blocks, &chain_tip, - graph_update.all_anchors(), + &graph_update.anchors, )?), _ => None, }; @@ -109,19 +115,23 @@ impl EsploraExt for esplora_client::BlockingClient { None }; - let mut graph_update = TxGraph::default(); - let _ = graph_update.apply_update(fetch_txs_with_spks( + let mut graph_update = tx_graph::Update::::default(); + let mut inserted_txs = HashSet::::new(); + graph_update.extend(fetch_txs_with_spks( self, + &mut inserted_txs, request.iter_spks(), parallel_requests, )?); - let _ = graph_update.apply_update(fetch_txs_with_txids( + graph_update.extend(fetch_txs_with_txids( self, + &mut inserted_txs, request.iter_txids(), parallel_requests, )?); - let _ = graph_update.apply_update(fetch_txs_with_outpoints( + graph_update.extend(fetch_txs_with_outpoints( self, + &mut inserted_txs, request.iter_outpoints(), parallel_requests, )?); @@ -131,7 +141,7 @@ impl EsploraExt for esplora_client::BlockingClient { self, &latest_blocks, &chain_tip, - graph_update.all_anchors(), + &graph_update.anchors, )?), _ => None, }; @@ -244,13 +254,14 @@ fn chain_update( fn fetch_txs_with_keychain_spks>>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, mut keychain_spks: I, stop_gap: usize, parallel_requests: usize, -) -> Result<(TxGraph, Option), Error> { +) -> Result<(tx_graph::Update, Option), Error> { type TxsOfSpkIndex = (u32, Vec); - let mut tx_graph = TxGraph::default(); + let mut update = tx_graph::Update::::default(); let mut last_index = Option::::None; let mut last_active_index = Option::::None; @@ -289,9 +300,11 @@ fn fetch_txs_with_keychain_spks>>( last_active_index = Some(index); } for tx in txs { - let _ = tx_graph.insert_tx(tx.to_tx()); - insert_anchor_from_status(&mut tx_graph, tx.txid, tx.status); - insert_prevouts(&mut tx_graph, tx.vin); + if inserted_txs.insert(tx.txid) { + update.txs.push(tx.to_tx().into()); + } + insert_anchor_from_status(&mut update, tx.txid, tx.status); + insert_prevouts(&mut update, tx.vin); } } @@ -306,7 +319,7 @@ fn fetch_txs_with_keychain_spks>>( } } - Ok((tx_graph, last_active_index)) + Ok((update, last_active_index)) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `spks` @@ -319,16 +332,18 @@ fn fetch_txs_with_keychain_spks>>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_spks>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, spks: I, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { fetch_txs_with_keychain_spks( client, + inserted_txs, spks.into_iter().enumerate().map(|(i, spk)| (i as u32, spk)), usize::MAX, parallel_requests, ) - .map(|(tx_graph, _)| tx_graph) + .map(|(update, _)| update) } /// Fetch transactions and associated [`ConfirmationBlockTime`]s by scanning `txids` @@ -339,59 +354,48 @@ fn fetch_txs_with_spks>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_txids>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, txids: I, parallel_requests: usize, -) -> Result, Error> { - enum EsploraResp { - TxStatus(TxStatus), - Tx(Option), - } - - let mut tx_graph = TxGraph::default(); - let mut txids = txids.into_iter(); +) -> Result, Error> { + let mut update = tx_graph::Update::::default(); + // Only fetch for non-inserted txs. + let mut txids = txids + .into_iter() + .filter(|txid| !inserted_txs.contains(txid)) + .collect::>() + .into_iter(); loop { let handles = txids .by_ref() .take(parallel_requests) .map(|txid| { let client = client.clone(); - let tx_already_exists = tx_graph.get_tx(txid).is_some(); std::thread::spawn(move || { - if tx_already_exists { - client - .get_tx_status(&txid) - .map_err(Box::new) - .map(|s| (txid, EsploraResp::TxStatus(s))) - } else { - client - .get_tx_info(&txid) - .map_err(Box::new) - .map(|t| (txid, EsploraResp::Tx(t))) - } + client + .get_tx_info(&txid) + .map_err(Box::new) + .map(|t| (txid, t)) }) }) - .collect::>>>(); + .collect::), Error>>>>(); if handles.is_empty() { break; } for handle in handles { - let (txid, resp) = handle.join().expect("thread must not panic")?; - match resp { - EsploraResp::TxStatus(status) => { - insert_anchor_from_status(&mut tx_graph, txid, status); + let (txid, tx_info) = handle.join().expect("thread must not panic")?; + if let Some(tx_info) = tx_info { + if inserted_txs.insert(txid) { + update.txs.push(tx_info.to_tx().into()); } - EsploraResp::Tx(Some(tx_info)) => { - let _ = tx_graph.insert_tx(tx_info.to_tx()); - insert_anchor_from_status(&mut tx_graph, txid, tx_info.status); - insert_prevouts(&mut tx_graph, tx_info.vin); - } - _ => continue, + insert_anchor_from_status(&mut update, txid, tx_info.status); + insert_prevouts(&mut update, tx_info.vin); } } } - Ok(tx_graph) + Ok(update) } /// Fetch transactions and [`ConfirmationBlockTime`]s that contain and spend the provided @@ -402,18 +406,21 @@ fn fetch_txs_with_txids>( /// Refer to [crate-level docs](crate) for more. fn fetch_txs_with_outpoints>( client: &esplora_client::BlockingClient, + inserted_txs: &mut HashSet, outpoints: I, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { let outpoints = outpoints.into_iter().collect::>(); + let mut update = tx_graph::Update::::default(); // make sure txs exists in graph and tx statuses are updated // TODO: We should maintain a tx cache (like we do with Electrum). - let mut tx_graph = fetch_txs_with_txids( + update.extend(fetch_txs_with_txids( client, + inserted_txs, outpoints.iter().map(|op| op.txid), parallel_requests, - )?; + )?); // get outpoint spend-statuses let mut outpoints = outpoints.into_iter(); @@ -442,22 +449,23 @@ fn fetch_txs_with_outpoints>( Some(txid) => txid, None => continue, }; - if tx_graph.get_tx(spend_txid).is_none() { + if !inserted_txs.contains(&spend_txid) { missing_txs.push(spend_txid); } if let Some(spend_status) = op_status.status { - insert_anchor_from_status(&mut tx_graph, spend_txid, spend_status); + insert_anchor_from_status(&mut update, spend_txid, spend_status); } } } } - let _ = tx_graph.apply_update(fetch_txs_with_txids( + update.extend(fetch_txs_with_txids( client, + inserted_txs, missing_txs, parallel_requests, )?); - Ok(tx_graph) + Ok(update) } #[cfg(test)] diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs index 7db6967b6..9a6e8f1df 100644 --- a/crates/esplora/src/lib.rs +++ b/crates/esplora/src/lib.rs @@ -26,7 +26,7 @@ //! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora use bdk_chain::bitcoin::{Amount, OutPoint, TxOut, Txid}; -use bdk_chain::{BlockId, ConfirmationBlockTime, TxGraph}; +use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime}; use esplora_client::TxStatus; pub use esplora_client; @@ -42,7 +42,7 @@ mod async_ext; pub use async_ext::*; fn insert_anchor_from_status( - tx_graph: &mut TxGraph, + update: &mut tx_graph::Update, txid: Txid, status: TxStatus, ) { @@ -57,21 +57,21 @@ fn insert_anchor_from_status( block_id: BlockId { height, hash }, confirmation_time: time, }; - let _ = tx_graph.insert_anchor(txid, anchor); + update.anchors.insert((anchor, txid)); } } /// Inserts floating txouts into `tx_graph` using [`Vin`](esplora_client::api::Vin)s returned by /// Esplora. fn insert_prevouts( - tx_graph: &mut TxGraph, + update: &mut tx_graph::Update, esplora_inputs: impl IntoIterator, ) { let prevouts = esplora_inputs .into_iter() .filter_map(|vin| Some((vin.txid, vin.vout, vin.prevout?))); for (prev_txid, prev_vout, prev_txout) in prevouts { - let _ = tx_graph.insert_txout( + update.txouts.insert( OutPoint::new(prev_txid, prev_vout), TxOut { script_pubkey: prev_txout.scriptpubkey, diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index 70d464194..7b0ef7fa6 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -1,4 +1,5 @@ use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; +use bdk_chain::{ConfirmationBlockTime, TxGraph}; use bdk_esplora::EsploraAsyncExt; use esplora_client::{self, Builder}; use std::collections::{BTreeSet, HashSet}; @@ -6,7 +7,7 @@ use std::str::FromStr; use std::thread::sleep; use std::time::Duration; -use bdk_chain::bitcoin::{Address, Amount, Txid}; +use bdk_chain::bitcoin::{Address, Amount}; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; #[tokio::test] @@ -78,18 +79,23 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -101,11 +107,15 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions" + ); Ok(()) } @@ -167,7 +177,7 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1).await? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -178,10 +188,10 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -212,8 +222,9 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -226,8 +237,9 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index 818f1f5fb..b3833b899 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -1,4 +1,5 @@ use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; +use bdk_chain::{ConfirmationBlockTime, TxGraph}; use bdk_esplora::EsploraExt; use esplora_client::{self, Builder}; use std::collections::{BTreeSet, HashSet}; @@ -6,7 +7,7 @@ use std::str::FromStr; use std::thread::sleep; use std::time::Duration; -use bdk_chain::bitcoin::{Address, Amount, Txid}; +use bdk_chain::bitcoin::{Address, Amount}; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; #[test] @@ -78,18 +79,23 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { ); let graph_update = sync_update.graph_update; + let updated_graph = { + let mut graph = TxGraph::::default(); + let _ = graph.apply_update(graph_update.clone()); + graph + }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in graph_update.full_txs() { + for tx in &graph_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. - let fee = graph_update.calculate_fee(&tx.tx).expect("Fee must exist"); + let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); // Retrieve the fee in the transaction data from `bitcoind`. let tx_fee = env .bitcoind .client - .get_transaction(&tx.txid, None) + .get_transaction(&tx.compute_txid(), None) .expect("Tx must exist") .fee .expect("Fee must exist") @@ -101,12 +107,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { assert_eq!(fee, tx_fee); } - let mut graph_update_txids: Vec = graph_update.full_txs().map(|tx| tx.txid).collect(); - graph_update_txids.sort(); - let mut expected_txids = vec![txid1, txid2]; - expected_txids.sort(); - assert_eq!(graph_update_txids, expected_txids); - + assert_eq!( + graph_update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + [txid1, txid2].into(), + "update must include all expected transactions" + ); Ok(()) } @@ -168,7 +177,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1)? }; - assert!(full_scan_update.graph_update.full_txs().next().is_none()); + assert!(full_scan_update.graph_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -179,10 +188,10 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { assert_eq!( full_scan_update .graph_update - .full_txs() - .next() + .txs + .first() .unwrap() - .txid, + .compute_txid(), txid_4th_addr ); assert_eq!(full_scan_update.last_active_indices[&0], 3); @@ -213,8 +222,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 1); assert!(txs.contains(&txid_4th_addr)); @@ -227,8 +237,9 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; let txs: HashSet<_> = full_scan_update .graph_update - .full_txs() - .map(|tx| tx.txid) + .txs + .iter() + .map(|tx| tx.compute_txid()) .collect(); assert_eq!(txs.len(), 2); assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr)); diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 6dce7503b..386d9d4e3 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -219,13 +219,13 @@ mod test { use bdk_chain::{BlockId, ConfirmationBlockTime}; use bitcoin::hashes::Hash; use bitcoin::{transaction, BlockHash, Network, Transaction}; + use chain::tx_graph; use super::*; use crate::Wallet; fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet { use crate::wallet::Update; - use bdk_chain::TxGraph; let mut wallet = Wallet::create(descriptor.to_string(), change_descriptor.to_string()) .network(network) .create_wallet_no_persist() @@ -253,11 +253,12 @@ mod test { confirmation_time: 0, block_id, }; - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + anchors: [(anchor, txid)].into_iter().collect(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 47e440c71..0d6cdf184 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -132,7 +132,7 @@ pub struct Update { pub last_active_indices: BTreeMap, /// Update for the wallet's internal [`TxGraph`]. - pub graph: TxGraph, + pub graph: chain::tx_graph::Update, /// Update for the wallet's internal [`LocalChain`]. /// @@ -2562,7 +2562,7 @@ macro_rules! floating_rate { macro_rules! doctest_wallet { () => {{ use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash}; - use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph}; + use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph, tx_graph}; use $crate::{Update, KeychainKind, Wallet}; let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)"; let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)"; @@ -2590,9 +2590,13 @@ macro_rules! doctest_wallet { confirmation_time: 50_000, block_id, }; - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); - let update = Update { graph, ..Default::default() }; + let update = Update { + graph: tx_graph::Update { + anchors: [(anchor, txid)].into_iter().collect(), + ..Default::default() + }, + ..Default::default() + }; wallet.apply_update(update).unwrap(); wallet }} diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 288560b0a..561a9a5fb 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -1,5 +1,5 @@ #![allow(unused)] -use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; +use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph}; use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet}; use bitcoin::{ hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, @@ -218,11 +218,12 @@ pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: Confir }) .expect("confirmation height cannot be greater than tip"); - let mut graph = TxGraph::default(); - let _ = graph.insert_anchor(txid, anchor); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + anchors: [(anchor, txid)].into(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index c530e779c..243161658 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -5,7 +5,7 @@ use std::str::FromStr; use anyhow::Context; use assert_matches::assert_matches; -use bdk_chain::COINBASE_MATURITY; +use bdk_chain::{tx_graph, COINBASE_MATURITY}; use bdk_chain::{BlockId, ConfirmationTime}; use bdk_wallet::coin_selection::{self, LargestFirstCoinSelection}; use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor}; @@ -81,11 +81,12 @@ fn receive_output_in_latest_block(wallet: &mut Wallet, value: u64) -> OutPoint { fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) { use bdk_wallet::Update; - let mut graph = bdk_chain::TxGraph::default(); - let _ = graph.insert_seen_at(txid, seen_at); wallet .apply_update(Update { - graph, + graph: tx_graph::Update { + seen_ats: [(txid, seen_at)].into_iter().collect(), + ..Default::default() + }, ..Default::default() }) .unwrap(); diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 49608fbf1..7212547d6 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -252,7 +252,7 @@ fn main() -> anyhow::Result<()> { .elapsed() .expect("must get time") .as_secs(); - let _ = graph_update.update_last_seen_unconfirmed(now); + graph_update.update_last_seen_unconfirmed(now); let db_changeset = { let mut chain = chain.lock().unwrap(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index b07a6697d..d188eab76 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -172,7 +172,7 @@ fn main() -> anyhow::Result<()> { // We want to keep track of the latest time a transaction was seen unconfirmed. let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); let mut graph = graph.lock().expect("mutex must not be poisoned"); let mut chain = chain.lock().expect("mutex must not be poisoned"); @@ -269,7 +269,7 @@ fn main() -> anyhow::Result<()> { // Update last seen unconfirmed let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); ( chain diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index f4596ce18..4cc698a00 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -67,7 +67,7 @@ fn main() -> Result<(), anyhow::Error> { let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); println!(); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index f81f8101c..d4dae1f3f 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -61,7 +61,7 @@ async fn main() -> Result<(), anyhow::Error> { .full_scan(request, STOP_GAP, PARALLEL_REQUESTS) .await?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; wallet.persist(&mut conn)?; diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index bec395611..9f79d6bf6 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -61,7 +61,7 @@ fn main() -> Result<(), anyhow::Error> { let mut update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - let _ = update.graph_update.update_last_seen_unconfirmed(now); + update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; if let Some(changeset) = wallet.take_staged() { From 9d47ad14ef671d83235f374e2a78f2b60e230bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 12:14:42 +0000 Subject: [PATCH 18/77] docs(chain): use `doc_cfg` feature --- crates/chain/src/lib.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 3fb8c0eda..029eedc28 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -17,6 +17,12 @@ //! //! [Bitcoin Dev Kit]: https://bitcoindevkit.org/ +// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr( + docsrs, + doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png") +)] #![no_std] #![warn(missing_docs)] From 1adcb62ff0a7dd62e2c644e69365008e7f8a4389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 09:53:42 +0000 Subject: [PATCH 19/77] feat(chain)!: `TxGraph::apply_update` auto-adds `seen_at` for unanchored Change `apply_update` to use the current timestamp as `seen_at` for unanchored transactions of the update. This makes `apply_update` only avaliable with the "std" feature. Introduce `apply_update_at` which includes an optional `seen_at` input. This is the no-std version of `apply_update`. Also update docs. --- crates/chain/src/indexed_tx_graph.rs | 27 ++++++++- crates/chain/src/tx_graph.rs | 56 ++++++++++++------- crates/electrum/tests/test_electrum.rs | 9 +-- crates/wallet/src/wallet/mod.rs | 33 ++++++++++- example-crates/example_electrum/src/main.rs | 8 +-- example-crates/example_esplora/src/main.rs | 12 +--- example-crates/wallet_electrum/src/main.rs | 5 +- .../wallet_esplora_async/src/main.rs | 4 +- .../wallet_esplora_blocking/src/main.rs | 4 +- 9 files changed, 100 insertions(+), 58 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index d24b1b307..73ae458ff 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -90,13 +90,38 @@ where /// Apply an `update` directly. /// - /// `update` is a [`TxGraph`] and the resultant changes is returned as [`ChangeSet`]. + /// `update` is a [`tx_graph::Update`] and the resultant changes is returned as [`ChangeSet`]. + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: tx_graph::Update) -> ChangeSet { let tx_graph = self.graph.apply_update(update); let indexer = self.index_tx_graph_changeset(&tx_graph); ChangeSet { tx_graph, indexer } } + /// Apply the given `update` with an optional `seen_at` timestamp. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](IndexedTxGraph::apply_update) to have the `seen_at` value automatically + /// set to the current time. + pub fn apply_update_at( + &mut self, + update: tx_graph::Update, + seen_at: Option, + ) -> ChangeSet { + let tx_graph = self.graph.apply_update_at(update, seen_at); + let indexer = self.index_tx_graph_changeset(&tx_graph); + ChangeSet { tx_graph, indexer } + } + /// Insert a floating `txout` of given `outpoint`. pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet { let graph = self.graph.insert_txout(outpoint, txout); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index ba894fa93..6da4d8afd 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -146,29 +146,12 @@ impl From> for Update { impl From> for TxGraph { fn from(update: Update) -> Self { let mut graph = TxGraph::::default(); - let _ = graph.apply_update(update); + let _ = graph.apply_update_at(update, None); graph } } impl Update { - /// Update the [`seen_ats`](Self::seen_ats) for all unanchored transactions. - pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) { - let seen_ats = &mut self.seen_ats; - let anchors = &self.anchors; - let unanchored_txids = self.txs.iter().map(|tx| tx.compute_txid()).filter(|txid| { - for (_, anchor_txid) in anchors { - if txid == anchor_txid { - return false; - } - } - true - }); - for txid in unanchored_txids { - seen_ats.insert(txid, seen_at); - } - } - /// Extend this update with `other`. pub fn extend(&mut self, other: Update) { self.txs.extend(other.txs); @@ -762,25 +745,56 @@ impl TxGraph { changeset } - /// Extends this graph with another so that `self` becomes the union of the two sets of - /// transactions. + /// Extends this graph with the given `update`. /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that /// exist in `update` but not in `self`). + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: Update) -> ChangeSet { + use std::time::*; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("current time must be greater than epoch anchor"); + self.apply_update_at(update, Some(now.as_secs())) + } + + /// Extends this graph with the given `update` alongside an optional `seen_at` timestamp. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](TxGraph::apply_update) to have the `seen_at` value automatically set + /// to the current time. + pub fn apply_update_at(&mut self, update: Update, seen_at: Option) -> ChangeSet { let mut changeset = ChangeSet::::default(); + let mut unanchored_txs = HashSet::::new(); for tx in update.txs { - changeset.merge(self.insert_tx(tx)); + if unanchored_txs.insert(tx.compute_txid()) { + changeset.merge(self.insert_tx(tx)); + } } for (outpoint, txout) in update.txouts { changeset.merge(self.insert_txout(outpoint, txout)); } for (anchor, txid) in update.anchors { + unanchored_txs.remove(&txid); changeset.merge(self.insert_anchor(txid, anchor)); } for (txid, seen_at) in update.seen_ats { changeset.merge(self.insert_seen_at(txid, seen_at)); } + if let Some(seen_at) = seen_at { + for txid in unanchored_txs { + changeset.merge(self.insert_seen_at(txid, seen_at)); + } + } changeset } diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index e8b054d33..d5e4a1596 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -38,19 +38,12 @@ where Spks: IntoIterator, Spks::IntoIter: ExactSizeIterator + Send + 'static, { - let mut update = client.sync( + let update = client.sync( SyncRequest::builder().chain_tip(chain.tip()).spks(spks), BATCH_SIZE, true, )?; - // Update `last_seen` to be able to calculate balance for unconfirmed transactions. - let now = std::time::UNIX_EPOCH - .elapsed() - .expect("must get time") - .as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); - if let Some(chain_update) = update.chain_update.clone() { let _ = chain .apply_update(chain_update) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 0d6cdf184..638bb5757 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2277,7 +2277,34 @@ impl Wallet { /// to persist staged wallet changes see [`Wallet::reveal_next_address`]. ` /// /// [`commit`]: Self::commit + #[cfg(feature = "std")] + #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { + use std::time::*; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("time now must surpass epoch anchor"); + self.apply_update_at(update, Some(now.as_secs())) + } + + /// Applies an `update` alongside an optional `seen_at` timestamp and stages the changes. + /// + /// `seen_at` represents when the update is seen (in unix seconds). It is used to determine the + /// `last_seen`s for all transactions in the update which have no corresponding anchor(s). The + /// `last_seen` value is used internally to determine precedence of conflicting unconfirmed + /// transactions (where the transaction with the lower `last_seen` value is omitted from the + /// canonical history). + /// + /// Not setting a `seen_at` value means unconfirmed transactions introduced by this update will + /// not be part of the canonical history of transactions. + /// + /// Use [`apply_update`](Wallet::apply_update) to have the `seen_at` value automatically set to + /// the current time. + pub fn apply_update_at( + &mut self, + update: impl Into, + seen_at: Option, + ) -> Result<(), CannotConnectError> { let update = update.into(); let mut changeset = match update.chain { Some(chain_update) => ChangeSet::from(self.chain.apply_update(chain_update)?), @@ -2289,7 +2316,11 @@ impl Wallet { .index .reveal_to_target_multi(&update.last_active_indices); changeset.merge(index_changeset.into()); - changeset.merge(self.indexed_graph.apply_update(update.graph).into()); + changeset.merge( + self.indexed_graph + .apply_update_at(update.graph, seen_at) + .into(), + ); self.stage.merge(changeset); Ok(()) } diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 7212547d6..662bc4237 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -129,7 +129,7 @@ fn main() -> anyhow::Result<()> { // Tell the electrum client about the txs we've already got locally so it doesn't re-download them client.populate_tx_cache(&*graph.lock().unwrap()); - let (chain_update, mut graph_update, keychain_update) = match electrum_cmd.clone() { + let (chain_update, graph_update, keychain_update) = match electrum_cmd.clone() { ElectrumCommands::Scan { stop_gap, scan_options, @@ -248,12 +248,6 @@ fn main() -> anyhow::Result<()> { } }; - let now = std::time::UNIX_EPOCH - .elapsed() - .expect("must get time") - .as_secs(); - graph_update.update_last_seen_unconfirmed(now); - let db_changeset = { let mut chain = chain.lock().unwrap(); let mut graph = graph.lock().unwrap(); diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index d188eab76..d4692e35c 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -166,14 +166,10 @@ fn main() -> anyhow::Result<()> { // is reached. It returns a `TxGraph` update (`graph_update`) and a structure that // represents the last active spk derivation indices of keychains // (`keychain_indices_update`). - let mut update = client + let update = client .full_scan(request, *stop_gap, scan_options.parallel_requests) .context("scanning for transactions")?; - // We want to keep track of the latest time a transaction was seen unconfirmed. - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); - let mut graph = graph.lock().expect("mutex must not be poisoned"); let mut chain = chain.lock().expect("mutex must not be poisoned"); // Because we did a stop gap based scan we are likely to have some updates to our @@ -265,11 +261,7 @@ fn main() -> anyhow::Result<()> { } } - let mut update = client.sync(request, scan_options.parallel_requests)?; - - // Update last seen unconfirmed - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.sync(request, scan_options.parallel_requests)?; ( chain diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 4cc698a00..c05184052 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -64,10 +64,7 @@ fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; - - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?; println!(); diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index d4dae1f3f..6fd215dff 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -57,11 +57,9 @@ async fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client + let update = client .full_scan(request, STOP_GAP, PARALLEL_REQUESTS) .await?; - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); wallet.apply_update(update)?; wallet.persist(&mut conn)?; diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 9f79d6bf6..45e4685b7 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -59,9 +59,7 @@ fn main() -> Result<(), anyhow::Error> { } }); - let mut update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; - let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs(); - update.graph_update.update_last_seen_unconfirmed(now); + let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; wallet.apply_update(update)?; if let Some(changeset) = wallet.take_staged() { From 60b14f025404d7282e6d4cef5e1151f42ac37a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 11:50:09 +0000 Subject: [PATCH 20/77] revert(chain)!: rm `TxGraph::update_last_seen_unconfirmed` This is no longer needed as `TxGraph::apply_update` now automatically adds `seen_at` timestamps for unanchored transactions. --- crates/chain/src/tx_graph.rs | 65 +---------------------------- crates/chain/tests/test_tx_graph.rs | 36 ---------------- 2 files changed, 1 insertion(+), 100 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 6da4d8afd..e953580ce 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -671,11 +671,7 @@ impl TxGraph { /// Inserts the given `seen_at` for `txid` into [`TxGraph`]. /// - /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. To batch - /// update all unconfirmed transactions with the latest `seen_at`, see - /// [`update_last_seen_unconfirmed`]. - /// - /// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed + /// Note that [`TxGraph`] only keeps track of the latest `seen_at`. pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet { let mut changeset = ChangeSet::::default(); let last_seen = self.last_seen.entry(txid).or_default(); @@ -686,65 +682,6 @@ impl TxGraph { changeset } - /// Update the last seen time for all unconfirmed transactions. - /// - /// This method updates the last seen unconfirmed time for this [`TxGraph`] by inserting - /// the given `seen_at` for every transaction not yet anchored to a confirmed block, - /// and returns the [`ChangeSet`] after applying all updates to `self`. - /// - /// This is useful for keeping track of the latest time a transaction was seen - /// unconfirmed, which is important for evaluating transaction conflicts in the same - /// [`TxGraph`]. For details of how [`TxGraph`] resolves conflicts, see the docs for - /// [`try_get_chain_position`]. - /// - /// A normal use of this method is to call it with the current system time. Although - /// block headers contain a timestamp, using the header time would be less effective - /// at tracking mempool transactions, because it can drift from actual clock time, plus - /// we may want to update a transaction's last seen time repeatedly between blocks. - /// - /// # Example - /// - /// ```rust - /// # use bdk_chain::example_utils::*; - /// # use std::time::UNIX_EPOCH; - /// # let tx = tx_from_hex(RAW_TX_1); - /// # let mut tx_graph = bdk_chain::TxGraph::<()>::new([tx]); - /// let now = std::time::SystemTime::now() - /// .duration_since(UNIX_EPOCH) - /// .expect("valid duration") - /// .as_secs(); - /// let changeset = tx_graph.update_last_seen_unconfirmed(now); - /// assert!(!changeset.last_seen.is_empty()); - /// ``` - /// - /// Note that [`TxGraph`] only keeps track of the latest `seen_at`, so the given time must - /// by strictly greater than what is currently stored for a transaction to have an effect. - /// To insert a last seen time for a single txid, see [`insert_seen_at`]. - /// - /// [`insert_seen_at`]: Self::insert_seen_at - /// [`try_get_chain_position`]: Self::try_get_chain_position - pub fn update_last_seen_unconfirmed(&mut self, seen_at: u64) -> ChangeSet { - let mut changeset = ChangeSet::default(); - let unanchored_txs: Vec = self - .txs - .iter() - .filter_map( - |(&txid, (_, anchors))| { - if anchors.is_empty() { - Some(txid) - } else { - None - } - }, - ) - .collect(); - - for txid in unanchored_txs { - changeset.merge(self.insert_seen_at(txid, seen_at)); - } - changeset - } - /// Extends this graph with the given `update`. /// /// The returned [`ChangeSet`] is the set difference between `update` and `self` (transactions that diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index c6399f53b..ba8a00031 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1013,42 +1013,6 @@ fn test_changeset_last_seen_merge() { } } -#[test] -fn update_last_seen_unconfirmed() { - let mut graph = TxGraph::<()>::default(); - let tx = new_tx(0); - let txid = tx.compute_txid(); - - // insert a new tx - // initially we have a last_seen of None and no anchors - let _ = graph.insert_tx(tx); - let tx = graph.full_txs().next().unwrap(); - assert_eq!(tx.last_seen_unconfirmed, None); - assert!(tx.anchors.is_empty()); - - // higher timestamp should update last seen - let changeset = graph.update_last_seen_unconfirmed(2); - assert_eq!(changeset.last_seen.get(&txid).unwrap(), &2); - - // lower timestamp has no effect - let changeset = graph.update_last_seen_unconfirmed(1); - assert!(changeset.last_seen.is_empty()); - - // once anchored, last seen is not updated - let _ = graph.insert_anchor(txid, ()); - let changeset = graph.update_last_seen_unconfirmed(4); - assert!(changeset.is_empty()); - assert_eq!( - graph - .full_txs() - .next() - .unwrap() - .last_seen_unconfirmed - .unwrap(), - 2 - ); -} - #[test] fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anchor_in_best_chain() { let txs = vec![new_tx(0), new_tx(1)]; From ccb8c796b259ae348cb8b8ef35b8c60e5621ccb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 13:36:54 +0000 Subject: [PATCH 21/77] test(chain): `TxGraph` and `tx_graph::Update` conversion tests --- crates/chain/tests/test_tx_graph.rs | 127 ++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index ba8a00031..df5e6a621 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -1136,3 +1136,130 @@ fn call_map_anchors_with_non_deterministic_anchor() { ] ); } + +/// Tests `From` impls for conversion between [`TxGraph`] and [`tx_graph::Update`]. +#[test] +fn tx_graph_update_conversion() { + use tx_graph::Update; + + type TestCase = (&'static str, Update); + + fn make_tx(v: i32) -> Transaction { + Transaction { + version: transaction::Version(v), + lock_time: absolute::LockTime::ZERO, + input: vec![], + output: vec![], + } + } + + fn make_txout(a: u64) -> TxOut { + TxOut { + value: Amount::from_sat(a), + script_pubkey: ScriptBuf::default(), + } + } + + let test_cases: &[TestCase] = &[ + ("empty_update", Update::default()), + ( + "single_tx", + Update { + txs: vec![make_tx(0).into()], + ..Default::default() + }, + ), + ( + "two_txs", + Update { + txs: vec![make_tx(0).into(), make_tx(1).into()], + ..Default::default() + }, + ), + ( + "with_floating_txouts", + Update { + txs: vec![make_tx(0).into(), make_tx(1).into()], + txouts: [ + (OutPoint::new(h!("a"), 0), make_txout(0)), + (OutPoint::new(h!("a"), 1), make_txout(1)), + (OutPoint::new(h!("b"), 0), make_txout(2)), + ] + .into(), + ..Default::default() + }, + ), + ( + "with_anchors", + Update { + txs: vec![make_tx(0).into(), make_tx(1).into()], + txouts: [ + (OutPoint::new(h!("a"), 0), make_txout(0)), + (OutPoint::new(h!("a"), 1), make_txout(1)), + (OutPoint::new(h!("b"), 0), make_txout(2)), + ] + .into(), + anchors: [ + (ConfirmationBlockTime::default(), h!("a")), + (ConfirmationBlockTime::default(), h!("b")), + ] + .into(), + ..Default::default() + }, + ), + ( + "with_seen_ats", + Update { + txs: vec![make_tx(0).into(), make_tx(1).into()], + txouts: [ + (OutPoint::new(h!("a"), 0), make_txout(0)), + (OutPoint::new(h!("a"), 1), make_txout(1)), + (OutPoint::new(h!("d"), 0), make_txout(2)), + ] + .into(), + anchors: [ + (ConfirmationBlockTime::default(), h!("a")), + (ConfirmationBlockTime::default(), h!("b")), + ] + .into(), + seen_ats: [(h!("c"), 12346)].into_iter().collect(), + }, + ), + ]; + + for (test_name, update) in test_cases { + let mut tx_graph = TxGraph::::default(); + let _ = tx_graph.apply_update_at(update.clone(), None); + let update_from_tx_graph: Update = tx_graph.into(); + + assert_eq!( + update + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + update_from_tx_graph + .txs + .iter() + .map(|tx| tx.compute_txid()) + .collect::>(), + "{}: txs do not match", + test_name + ); + assert_eq!( + update.txouts, update_from_tx_graph.txouts, + "{}: txouts do not match", + test_name + ); + assert_eq!( + update.anchors, update_from_tx_graph.anchors, + "{}: anchors do not match", + test_name + ); + assert_eq!( + update.seen_ats, update_from_tx_graph.seen_ats, + "{}: seen_ats do not match", + test_name + ); + } +} From 6f7626ad0bb6ca7e45428af11dbf930d746d9cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 15:08:55 +0000 Subject: [PATCH 22/77] feat!: introduce `bdk_core` This is an initial version with `chain_data` types ported over. Types ported over include `BlockId`, `ConfirmationBlockTime`. The impls for `Anchor` and `AnchorFromBlockPosition` of these types are moved to where the traits are defined. --- Cargo.toml | 1 + crates/chain/Cargo.toml | 9 ++- crates/chain/src/chain_data.rs | 93 ++------------------------- crates/chain/src/example_utils.rs | 3 +- crates/chain/src/lib.rs | 60 ++++++++--------- crates/chain/src/rusqlite_impl.rs | 12 ++-- crates/chain/src/spk_client.rs | 3 +- crates/chain/src/tx_data_traits.rs | 36 ++++++++++- crates/chain/src/tx_graph.rs | 6 +- crates/chain/tests/test_tx_graph.rs | 6 +- crates/core/Cargo.toml | 21 ++++++ crates/core/src/chain_data.rs | 51 +++++++++++++++ crates/core/src/lib.rs | 58 +++++++++++++++++ example-crates/example_cli/src/lib.rs | 2 +- 14 files changed, 217 insertions(+), 144 deletions(-) create mode 100644 crates/core/Cargo.toml create mode 100644 crates/core/src/chain_data.rs create mode 100644 crates/core/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 1c29bbaf5..ef6d55e92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "crates/wallet", "crates/chain", + "crates/core", "crates/file_store", "crates/electrum", "crates/esplora", diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 7261bdfa2..42a77441f 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -14,10 +14,8 @@ readme = "README.md" [dependencies] bitcoin = { version = "0.32.0", default-features = false } +bdk_core = { path = "../core", version = "0.1", default-features = false } serde = { version = "1", optional = true, features = ["derive", "rc"] } - -# Use hashbrown as a feature flag to have HashSet and HashMap from it. -hashbrown = { version = "0.9.1", optional = true, features = ["serde"] } miniscript = { version = "12.0.0", optional = true, default-features = false } # Feature dependencies @@ -30,6 +28,7 @@ proptest = "1.2.0" [features] default = ["std", "miniscript"] -std = ["bitcoin/std", "miniscript?/std"] -serde = ["dep:serde", "bitcoin/serde", "miniscript?/serde"] +std = ["bitcoin/std", "miniscript?/std", "bdk_core/std"] +serde = ["dep:serde", "bitcoin/serde", "miniscript?/serde", "bdk_core/serde"] +hashbrown = ["bdk_core/hashbrown"] rusqlite = ["std", "dep:rusqlite", "serde", "serde_json"] diff --git a/crates/chain/src/chain_data.rs b/crates/chain/src/chain_data.rs index 8ce6e31a4..ce6076c51 100644 --- a/crates/chain/src/chain_data.rs +++ b/crates/chain/src/chain_data.rs @@ -1,6 +1,7 @@ -use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid}; +use crate::ConfirmationBlockTime; +use bitcoin::{OutPoint, TxOut, Txid}; -use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY}; +use crate::{Anchor, COINBASE_MATURITY}; /// Represents the observed position of some chain data. /// @@ -82,92 +83,6 @@ impl From> for ConfirmationTime { } } -/// A reference to a block in the canonical chain. -/// -/// `BlockId` implements [`Anchor`]. When a transaction is anchored to `BlockId`, the confirmation -/// block and anchor block are the same block. -#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct BlockId { - /// The height of the block. - pub height: u32, - /// The hash of the block. - pub hash: BlockHash, -} - -impl Anchor for BlockId { - fn anchor_block(&self) -> Self { - *self - } -} - -impl AnchorFromBlockPosition for BlockId { - fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { - block_id - } -} - -impl Default for BlockId { - fn default() -> Self { - Self { - height: Default::default(), - hash: BlockHash::all_zeros(), - } - } -} - -impl From<(u32, BlockHash)> for BlockId { - fn from((height, hash): (u32, BlockHash)) -> Self { - Self { height, hash } - } -} - -impl From for (u32, BlockHash) { - fn from(block_id: BlockId) -> Self { - (block_id.height, block_id.hash) - } -} - -impl From<(&u32, &BlockHash)> for BlockId { - fn from((height, hash): (&u32, &BlockHash)) -> Self { - Self { - height: *height, - hash: *hash, - } - } -} - -/// An [`Anchor`] implementation that also records the exact confirmation time of the transaction. -/// -/// Refer to [`Anchor`] for more details. -#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -pub struct ConfirmationBlockTime { - /// The anchor block. - pub block_id: BlockId, - /// The confirmation time of the transaction being anchored. - pub confirmation_time: u64, -} - -impl Anchor for ConfirmationBlockTime { - fn anchor_block(&self) -> BlockId { - self.block_id - } - - fn confirmation_height_upper_bound(&self) -> u32 { - self.block_id.height - } -} - -impl AnchorFromBlockPosition for ConfirmationBlockTime { - fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { - Self { - block_id, - confirmation_time: block.header.time as _, - } - } -} - /// A `TxOut` with as much data as we can retrieve about it #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct FullTxOut { @@ -244,6 +159,8 @@ impl FullTxOut { #[cfg(test)] mod test { + use crate::BlockId; + use super::*; #[test] diff --git a/crates/chain/src/example_utils.rs b/crates/chain/src/example_utils.rs index 8077e2118..c71b6cfef 100644 --- a/crates/chain/src/example_utils.rs +++ b/crates/chain/src/example_utils.rs @@ -1,4 +1,5 @@ #![allow(unused)] +use crate::BlockId; use alloc::vec::Vec; use bitcoin::{ consensus, @@ -6,8 +7,6 @@ use bitcoin::{ Transaction, }; -use crate::BlockId; - pub const RAW_TX_1: &str = "0200000000010116d6174da7183d70d0a7d4dc314d517a7d135db79ad63515028b293a76f4f9d10000000000feffffff023a21fc8350060000160014531c405e1881ef192294b8813631e258bf98ea7a1027000000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c024730440220591b1a172a122da49ba79a3e79f98aaa03fd7a372f9760da18890b6a327e6010022013e82319231da6c99abf8123d7c07e13cf9bd8d76e113e18dc452e5024db156d012102318a2d558b2936c52e320decd6d92a88d7f530be91b6fe0af5caf41661e77da3ef2e0100"; pub const RAW_TX_2: &str = "02000000000101a688607020cfae91a61e7c516b5ef1264d5d77f17200c3866826c6c808ebf1620000000000feffffff021027000000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c20fd48ff530600001600146886c525e41d4522042bd0b159dfbade2504a6bb024730440220740ff7e665cd20565d4296b549df8d26b941be3f1e3af89a0b60e50c0dbeb69a02206213ab7030cf6edc6c90d4ccf33010644261e029950a688dc0b1a9ebe6ddcc5a012102f2ac6b396a97853cb6cd62242c8ae4842024742074475023532a51e9c53194253e760100"; pub const RAW_TX_3: &str = "0200000000010135d67ee47b557e68b8c6223958f597381965ed719f1207ee2b9e20432a24a5dc0100000000feffffff021027000000000000225120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb62215a5055060000160014070df7671dea67a50c4799a744b5c9be8f4bac690247304402207ebf8d29f71fd03e7e6977b3ea78ca5fcc5c49a42ae822348fc401862fdd766c02201d7e4ff0684ecb008b6142f36ead1b0b4d615524c4f58c261113d361f4427e25012103e6a75e2fab85e5ecad641afc4ffba7222f998649d9f18cac92f0fcc8618883b3ee760100"; diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index 029eedc28..c1c555961 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -38,8 +38,8 @@ pub use indexer::spk_txout; pub use indexer::Indexer; pub mod local_chain; mod tx_data_traits; -pub mod tx_graph; pub use tx_data_traits::*; +pub mod tx_graph; pub use tx_graph::TxGraph; mod chain_oracle; pub use chain_oracle::*; @@ -63,6 +63,9 @@ pub use spk_iter::*; pub mod rusqlite_impl; pub mod spk_client; +pub extern crate bdk_core; +pub use bdk_core::*; + #[allow(unused_imports)] #[macro_use] extern crate alloc; @@ -75,37 +78,6 @@ pub extern crate serde; #[macro_use] extern crate std; -#[cfg(all(not(feature = "std"), feature = "hashbrown"))] -extern crate hashbrown; - -// When no-std use `alloc`'s Hash collections. This is activated by default -#[cfg(all(not(feature = "std"), not(feature = "hashbrown")))] -#[doc(hidden)] -pub mod collections { - #![allow(dead_code)] - pub type HashSet = alloc::collections::BTreeSet; - pub type HashMap = alloc::collections::BTreeMap; - pub use alloc::collections::{btree_map as hash_map, *}; -} - -// When we have std, use `std`'s all collections -#[cfg(all(feature = "std", not(feature = "hashbrown")))] -#[doc(hidden)] -pub mod collections { - pub use std::collections::{hash_map, *}; -} - -// With this special feature `hashbrown`, use `hashbrown`'s hash collections, and else from `alloc`. -#[cfg(feature = "hashbrown")] -#[doc(hidden)] -pub mod collections { - #![allow(dead_code)] - pub type HashSet = hashbrown::HashSet; - pub type HashMap = hashbrown::HashMap; - pub use alloc::collections::*; - pub use hashbrown::hash_map; -} - /// How many confirmations are needed f or a coinbase output to be spent. pub const COINBASE_MATURITY: u32 = 100; @@ -137,3 +109,27 @@ impl core::ops::Deref for Impl { &self.0 } } + +/// A wrapper that we use to impl remote traits for types in our crate or dependency crates that impl [`Anchor`]. +pub struct AnchorImpl(pub T); + +impl AnchorImpl { + /// Returns the inner `T`. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl From for AnchorImpl { + fn from(value: T) -> Self { + Self(value) + } +} + +impl core::ops::Deref for AnchorImpl { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/crates/chain/src/rusqlite_impl.rs b/crates/chain/src/rusqlite_impl.rs index d8ef65c42..2cc4481c9 100644 --- a/crates/chain/src/rusqlite_impl.rs +++ b/crates/chain/src/rusqlite_impl.rs @@ -157,15 +157,15 @@ impl ToSql for Impl { } } -impl FromSql for Impl { +impl FromSql for AnchorImpl { fn column_result(value: ValueRef<'_>) -> FromSqlResult { serde_json::from_str(value.as_str()?) - .map(Impl) + .map(AnchorImpl) .map_err(from_sql_error) } } -impl ToSql for Impl { +impl ToSql for AnchorImpl { fn to_sql(&self) -> rusqlite::Result> { serde_json::to_string(&self.0) .map(Into::into) @@ -319,12 +319,12 @@ where ))?; let row_iter = statement.query_map([], |row| { Ok(( - row.get::<_, Impl>("json(anchor)")?, + row.get::<_, AnchorImpl>("json(anchor)")?, row.get::<_, Impl>("txid")?, )) })?; for row in row_iter { - let (Impl(anchor), Impl(txid)) = row?; + let (AnchorImpl(anchor), Impl(txid)) = row?; changeset.anchors.insert((anchor, txid)); } @@ -381,7 +381,7 @@ where ":txid": Impl(*txid), ":block_height": anchor_block.height, ":block_hash": Impl(anchor_block.hash), - ":anchor": Impl(anchor.clone()), + ":anchor": AnchorImpl(anchor.clone()), })?; } diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs index e31b431dd..75fb66984 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/chain/src/spk_client.rs @@ -3,8 +3,9 @@ use crate::{ alloc::{boxed::Box, collections::VecDeque, vec::Vec}, collections::BTreeMap, local_chain::CheckPoint, - ConfirmationBlockTime, Indexed, + Indexed, }; +use bdk_core::ConfirmationBlockTime; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; type InspectSync = dyn FnMut(SyncItem, SyncProgress) + Send + 'static; diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index 8a324f6a5..d3d562bf3 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,6 +1,5 @@ -use crate::collections::BTreeMap; -use crate::collections::BTreeSet; -use crate::BlockId; +use crate::collections::{BTreeMap, BTreeSet}; +use crate::{BlockId, ConfirmationBlockTime}; use alloc::vec::Vec; /// Trait that "anchors" blockchain data to a specific block of height and hash. @@ -85,6 +84,22 @@ impl<'a, A: Anchor> Anchor for &'a A { } } +impl Anchor for BlockId { + fn anchor_block(&self) -> Self { + *self + } +} + +impl Anchor for ConfirmationBlockTime { + fn anchor_block(&self) -> BlockId { + self.block_id + } + + fn confirmation_height_upper_bound(&self) -> u32 { + self.block_id.height + } +} + /// An [`Anchor`] that can be constructed from a given block, block height and transaction position /// within the block. pub trait AnchorFromBlockPosition: Anchor { @@ -92,6 +107,21 @@ pub trait AnchorFromBlockPosition: Anchor { fn from_block_position(block: &bitcoin::Block, block_id: BlockId, tx_pos: usize) -> Self; } +impl AnchorFromBlockPosition for BlockId { + fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { + block_id + } +} + +impl AnchorFromBlockPosition for ConfirmationBlockTime { + fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { + Self { + block_id, + confirmation_time: block.header.time as _, + } + } +} + /// Trait that makes an object mergeable. pub trait Merge: Default { /// Merge another object of the same type onto `self`. diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index e953580ce..206aaf11e 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -92,9 +92,9 @@ //! [`try_get_chain_position`]: TxGraph::try_get_chain_position //! [`insert_txout`]: TxGraph::insert_txout -use crate::{ - collections::*, Anchor, Balance, BlockId, ChainOracle, ChainPosition, FullTxOut, Merge, -}; +use crate::collections::*; +use crate::BlockId; +use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge}; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; use alloc::vec::Vec; diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index df5e6a621..4ce6772bb 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -2,12 +2,12 @@ #[macro_use] mod common; -use bdk_chain::tx_graph::{self, CalculateFeeError}; +use bdk_chain::{collections::*, BlockId, ConfirmationBlockTime}; use bdk_chain::{ - collections::*, local_chain::LocalChain, + tx_graph::{self, CalculateFeeError}, tx_graph::{ChangeSet, TxGraph}, - Anchor, BlockId, ChainOracle, ChainPosition, ConfirmationBlockTime, Merge, + Anchor, ChainOracle, ChainPosition, Merge, }; use bitcoin::{ absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount, diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml new file mode 100644 index 000000000..d74cf906c --- /dev/null +++ b/crates/core/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "bdk_core" +version = "0.1.0" +edition = "2021" +rust-version = "1.63" +homepage = "https://bitcoindevkit.org" +repository = "https://github.com/bitcoindevkit/bdk" +documentation = "https://docs.rs/bdk_core" +description = "Collection of core structures for Bitcoin Dev Kit." +license = "MIT OR Apache-2.0" +readme = "README.md" + +[dependencies] +bitcoin = { version = "0.32", default-features = false } +serde = { version = "1", optional = true, features = ["derive", "rc"] } +hashbrown = { version = "0.9.1", optional = true } + +[features] +default = ["std"] +std = ["bitcoin/std"] +serde = ["dep:serde", "bitcoin/serde", "hashbrown?/serde"] diff --git a/crates/core/src/chain_data.rs b/crates/core/src/chain_data.rs new file mode 100644 index 000000000..2e64c9cb2 --- /dev/null +++ b/crates/core/src/chain_data.rs @@ -0,0 +1,51 @@ +use bitcoin::{hashes::Hash, BlockHash}; + +/// A reference to a block in the canonical chain. +#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct BlockId { + /// The height of the block. + pub height: u32, + /// The hash of the block. + pub hash: BlockHash, +} + +impl Default for BlockId { + fn default() -> Self { + Self { + height: Default::default(), + hash: BlockHash::all_zeros(), + } + } +} + +impl From<(u32, BlockHash)> for BlockId { + fn from((height, hash): (u32, BlockHash)) -> Self { + Self { height, hash } + } +} + +impl From for (u32, BlockHash) { + fn from(block_id: BlockId) -> Self { + (block_id.height, block_id.hash) + } +} + +impl From<(&u32, &BlockHash)> for BlockId { + fn from((height, hash): (&u32, &BlockHash)) -> Self { + Self { + height: *height, + hash: *hash, + } + } +} + +/// Represents the confirmation block and time of a transaction. +#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +pub struct ConfirmationBlockTime { + /// The anchor block. + pub block_id: BlockId, + /// The confirmation time of the transaction being anchored. + pub confirmation_time: u64, +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs new file mode 100644 index 000000000..1c5358401 --- /dev/null +++ b/crates/core/src/lib.rs @@ -0,0 +1,58 @@ +//! This crate is a collection of core structures for [Bitcoin Dev Kit]. + +// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined +#![cfg_attr(docsrs, feature(doc_cfg))] +#![cfg_attr( + docsrs, + doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png") +)] +#![no_std] +#![warn(missing_docs)] + +pub use bitcoin; + +#[allow(unused_imports)] +#[macro_use] +extern crate alloc; + +#[allow(unused_imports)] +#[cfg(feature = "std")] +#[macro_use] +extern crate std; + +#[cfg(feature = "serde")] +pub extern crate serde; + +#[cfg(all(not(feature = "std"), feature = "hashbrown"))] +extern crate hashbrown; + +// When no-std use `alloc`'s Hash collections. This is activated by default +#[cfg(all(not(feature = "std"), not(feature = "hashbrown")))] +#[doc(hidden)] +pub mod collections { + #![allow(dead_code)] + pub type HashSet = alloc::collections::BTreeSet; + pub type HashMap = alloc::collections::BTreeMap; + pub use alloc::collections::{btree_map as hash_map, *}; +} + +// When we have std, use `std`'s all collections +#[cfg(all(feature = "std", not(feature = "hashbrown")))] +#[doc(hidden)] +pub mod collections { + pub use std::collections::{hash_map, *}; +} + +// With this special feature `hashbrown`, use `hashbrown`'s hash collections, and else from `alloc`. +#[cfg(feature = "hashbrown")] +#[doc(hidden)] +pub mod collections { + #![allow(dead_code)] + pub type HashSet = hashbrown::HashSet; + pub type HashMap = hashbrown::HashMap; + pub use alloc::collections::*; + pub use hashbrown::hash_map; +} + +mod chain_data; +pub use chain_data::*; diff --git a/example-crates/example_cli/src/lib.rs b/example-crates/example_cli/src/lib.rs index 393f9d3fb..6a97252fc 100644 --- a/example-crates/example_cli/src/lib.rs +++ b/example-crates/example_cli/src/lib.rs @@ -1,4 +1,3 @@ -use bdk_chain::ConfirmationBlockTime; use serde_json::json; use std::cmp; use std::collections::HashMap; @@ -20,6 +19,7 @@ use bdk_chain::miniscript::{ psbt::PsbtExt, Descriptor, DescriptorPublicKey, }; +use bdk_chain::ConfirmationBlockTime; use bdk_chain::{ indexed_tx_graph, indexer::keychain_txout::{self, KeychainTxOutIndex}, From a86c878a60f40ce30ab00c0b47fa6cddf30cfe30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 15:18:25 +0000 Subject: [PATCH 23/77] refactor(chain): change `CheckPoint::apply_changeset` to be a separate function: `apply_changeset_to_checkpoint`. --- crates/chain/src/local_chain.rs | 103 ++++++++++++++++---------------- 1 file changed, 52 insertions(+), 51 deletions(-) diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 3a822ab0b..4a2505f70 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -207,46 +207,6 @@ impl CheckPoint { base.extend(core::iter::once(block_id).chain(tail.into_iter().rev())) .expect("tail is in order") } - - /// Apply `changeset` to the checkpoint. - fn apply_changeset(mut self, changeset: &ChangeSet) -> Result { - if let Some(start_height) = changeset.blocks.keys().next().cloned() { - // changes after point of agreement - let mut extension = BTreeMap::default(); - // point of agreement - let mut base: Option = None; - - for cp in self.iter() { - if cp.height() >= start_height { - extension.insert(cp.height(), cp.hash()); - } else { - base = Some(cp); - break; - } - } - - for (&height, &hash) in &changeset.blocks { - match hash { - Some(hash) => { - extension.insert(height, hash); - } - None => { - extension.remove(&height); - } - }; - } - - let new_tip = match base { - Some(base) => base - .extend(extension.into_iter().map(BlockId::from)) - .expect("extension is strictly greater than base"), - None => LocalChain::from_blocks(extension)?.tip(), - }; - self = new_tip; - } - - Ok(self) - } } /// Iterates over checkpoints backwards. @@ -275,6 +235,49 @@ impl IntoIterator for CheckPoint { } } +/// Apply `changeset` to the checkpoint. +fn apply_changeset_to_checkpoint( + mut init_cp: CheckPoint, + changeset: &ChangeSet, +) -> Result { + if let Some(start_height) = changeset.blocks.keys().next().cloned() { + // changes after point of agreement + let mut extension = BTreeMap::default(); + // point of agreement + let mut base: Option = None; + + for cp in init_cp.iter() { + if cp.height() >= start_height { + extension.insert(cp.height(), cp.hash()); + } else { + base = Some(cp); + break; + } + } + + for (&height, &hash) in &changeset.blocks { + match hash { + Some(hash) => { + extension.insert(height, hash); + } + None => { + extension.remove(&height); + } + }; + } + + let new_tip = match base { + Some(base) => base + .extend(extension.into_iter().map(BlockId::from)) + .expect("extension is strictly greater than base"), + None => LocalChain::from_blocks(extension)?.tip(), + }; + init_cp = new_tip; + } + + Ok(init_cp) +} + /// This is a local implementation of [`ChainOracle`]. #[derive(Debug, Clone, PartialEq)] pub struct LocalChain { @@ -490,7 +493,7 @@ impl LocalChain { /// Apply the given `changeset`. pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> { let old_tip = self.tip.clone(); - let new_tip = old_tip.apply_changeset(changeset)?; + let new_tip = apply_changeset_to_checkpoint(old_tip, changeset)?; self.tip = new_tip; debug_assert!(self._check_changeset_is_applied(changeset)); Ok(()) @@ -848,12 +851,10 @@ fn merge_chains( if is_update_height_superset_of_original { return Ok((update_tip, changeset)); } else { - let new_tip = - original_tip.apply_changeset(&changeset).map_err(|_| { - CannotConnectError { - try_include_height: 0, - } - })?; + let new_tip = apply_changeset_to_checkpoint(original_tip, &changeset) + .map_err(|_| CannotConnectError { + try_include_height: 0, + })?; return Ok((new_tip, changeset)); } } @@ -889,10 +890,10 @@ fn merge_chains( } } - let new_tip = original_tip - .apply_changeset(&changeset) - .map_err(|_| CannotConnectError { + let new_tip = apply_changeset_to_checkpoint(original_tip, &changeset).map_err(|_| { + CannotConnectError { try_include_height: 0, - })?; + } + })?; Ok((new_tip, changeset)) } From 77e31c71a5edc011df42daece1920903bc2b866f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 15:38:10 +0000 Subject: [PATCH 24/77] feat!: move `CheckPoint` to `bdk_core` Also add `CheckPoint::eq_ptr` which compares the internal pointers of two `CheckPoint`s. This is used as an optimisation when comparing two chains. --- crates/chain/src/local_chain.rs | 234 +------------------------------- crates/chain/src/spk_client.rs | 3 +- crates/core/src/checkpoint.rs | 233 +++++++++++++++++++++++++++++++ crates/core/src/lib.rs | 3 + crates/testenv/src/lib.rs | 3 +- 5 files changed, 241 insertions(+), 235 deletions(-) create mode 100644 crates/core/src/checkpoint.rs diff --git a/crates/chain/src/local_chain.rs b/crates/chain/src/local_chain.rs index 4a2505f70..e16131db3 100644 --- a/crates/chain/src/local_chain.rs +++ b/crates/chain/src/local_chain.rs @@ -5,236 +5,10 @@ use core::ops::RangeBounds; use crate::collections::BTreeMap; use crate::{BlockId, ChainOracle, Merge}; -use alloc::sync::Arc; +pub use bdk_core::{CheckPoint, CheckPointIter}; use bitcoin::block::Header; use bitcoin::BlockHash; -/// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a -/// transaction anchor. -/// -/// Each checkpoint contains the height and hash of a block ([`BlockId`]). -/// -/// Internally, checkpoints are nodes of a reference-counted linked-list. This allows the caller to -/// cheaply clone a [`CheckPoint`] without copying the whole list and to view the entire chain -/// without holding a lock on [`LocalChain`]. -#[derive(Debug, Clone)] -pub struct CheckPoint(Arc); - -/// The internal contents of [`CheckPoint`]. -#[derive(Debug, Clone)] -struct CPInner { - /// Block id (hash and height). - block: BlockId, - /// Previous checkpoint (if any). - prev: Option>, -} - -impl PartialEq for CheckPoint { - fn eq(&self, other: &Self) -> bool { - let self_cps = self.iter().map(|cp| cp.block_id()); - let other_cps = other.iter().map(|cp| cp.block_id()); - self_cps.eq(other_cps) - } -} - -impl CheckPoint { - /// Construct a new base block at the front of a linked list. - pub fn new(block: BlockId) -> Self { - Self(Arc::new(CPInner { block, prev: None })) - } - - /// Construct a checkpoint from a list of [`BlockId`]s in ascending height order. - /// - /// # Errors - /// - /// This method will error if any of the follow occurs: - /// - /// - The `blocks` iterator is empty, in which case, the error will be `None`. - /// - The `blocks` iterator is not in ascending height order. - /// - The `blocks` iterator contains multiple [`BlockId`]s of the same height. - /// - /// The error type is the last successful checkpoint constructed (if any). - pub fn from_block_ids( - block_ids: impl IntoIterator, - ) -> Result> { - let mut blocks = block_ids.into_iter(); - let mut acc = CheckPoint::new(blocks.next().ok_or(None)?); - for id in blocks { - acc = acc.push(id).map_err(Some)?; - } - Ok(acc) - } - - /// Construct a checkpoint from the given `header` and block `height`. - /// - /// If `header` is of the genesis block, the checkpoint won't have a [`prev`] node. Otherwise, - /// we return a checkpoint linked with the previous block. - /// - /// [`prev`]: CheckPoint::prev - pub fn from_header(header: &bitcoin::block::Header, height: u32) -> Self { - let hash = header.block_hash(); - let this_block_id = BlockId { height, hash }; - - let prev_height = match height.checked_sub(1) { - Some(h) => h, - None => return Self::new(this_block_id), - }; - - let prev_block_id = BlockId { - height: prev_height, - hash: header.prev_blockhash, - }; - - CheckPoint::new(prev_block_id) - .push(this_block_id) - .expect("must construct checkpoint") - } - - /// Puts another checkpoint onto the linked list representing the blockchain. - /// - /// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you - /// are pushing on to. - pub fn push(self, block: BlockId) -> Result { - if self.height() < block.height { - Ok(Self(Arc::new(CPInner { - block, - prev: Some(self.0), - }))) - } else { - Err(self) - } - } - - /// Extends the checkpoint linked list by a iterator of block ids. - /// - /// Returns an `Err(self)` if there is block which does not have a greater height than the - /// previous one. - pub fn extend(self, blocks: impl IntoIterator) -> Result { - let mut curr = self.clone(); - for block in blocks { - curr = curr.push(block).map_err(|_| self.clone())?; - } - Ok(curr) - } - - /// Get the [`BlockId`] of the checkpoint. - pub fn block_id(&self) -> BlockId { - self.0.block - } - - /// Get the height of the checkpoint. - pub fn height(&self) -> u32 { - self.0.block.height - } - - /// Get the block hash of the checkpoint. - pub fn hash(&self) -> BlockHash { - self.0.block.hash - } - - /// Get the previous checkpoint in the chain - pub fn prev(&self) -> Option { - self.0.prev.clone().map(CheckPoint) - } - - /// Iterate from this checkpoint in descending height. - pub fn iter(&self) -> CheckPointIter { - self.clone().into_iter() - } - - /// Get checkpoint at `height`. - /// - /// Returns `None` if checkpoint at `height` does not exist`. - pub fn get(&self, height: u32) -> Option { - self.range(height..=height).next() - } - - /// Iterate checkpoints over a height range. - /// - /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip - /// height). - pub fn range(&self, range: R) -> impl Iterator - where - R: RangeBounds, - { - let start_bound = range.start_bound().cloned(); - let end_bound = range.end_bound().cloned(); - self.iter() - .skip_while(move |cp| match end_bound { - core::ops::Bound::Included(inc_bound) => cp.height() > inc_bound, - core::ops::Bound::Excluded(exc_bound) => cp.height() >= exc_bound, - core::ops::Bound::Unbounded => false, - }) - .take_while(move |cp| match start_bound { - core::ops::Bound::Included(inc_bound) => cp.height() >= inc_bound, - core::ops::Bound::Excluded(exc_bound) => cp.height() > exc_bound, - core::ops::Bound::Unbounded => true, - }) - } - - /// Inserts `block_id` at its height within the chain. - /// - /// The effect of `insert` depends on whether a height already exists. If it doesn't the - /// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after - /// it. If the height already existed and has a conflicting block hash then it will be purged - /// along with all block followin it. The returned chain will have a tip of the `block_id` - /// passed in. Of course, if the `block_id` was already present then this just returns `self`. - #[must_use] - pub fn insert(self, block_id: BlockId) -> Self { - assert_ne!(block_id.height, 0, "cannot insert the genesis block"); - - let mut cp = self.clone(); - let mut tail = vec![]; - let base = loop { - if cp.height() == block_id.height { - if cp.hash() == block_id.hash { - return self; - } - // if we have a conflict we just return the inserted block because the tail is by - // implication invalid. - tail = vec![]; - break cp.prev().expect("can't be called on genesis block"); - } - - if cp.height() < block_id.height { - break cp; - } - - tail.push(cp.block_id()); - cp = cp.prev().expect("will break before genesis block"); - }; - - base.extend(core::iter::once(block_id).chain(tail.into_iter().rev())) - .expect("tail is in order") - } -} - -/// Iterates over checkpoints backwards. -pub struct CheckPointIter { - current: Option>, -} - -impl Iterator for CheckPointIter { - type Item = CheckPoint; - - fn next(&mut self) -> Option { - let current = self.current.clone()?; - self.current.clone_from(¤t.prev); - Some(CheckPoint(current)) - } -} - -impl IntoIterator for CheckPoint { - type Item = CheckPoint; - type IntoIter = CheckPointIter; - - fn into_iter(self) -> Self::IntoIter { - CheckPointIter { - current: Some(self.0), - } - } -} - /// Apply `changeset` to the checkpoint. fn apply_changeset_to_checkpoint( mut init_cp: CheckPoint, @@ -582,9 +356,7 @@ impl LocalChain { /// Iterate over checkpoints in descending height order. pub fn iter_checkpoints(&self) -> CheckPointIter { - CheckPointIter { - current: Some(self.tip.0.clone()), - } + self.tip.iter() } fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool { @@ -847,7 +619,7 @@ fn merge_chains( prev_orig_was_invalidated = false; // OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we // can guarantee that no older blocks are introduced. - if Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) { + if o.eq_ptr(u) { if is_update_height_superset_of_original { return Ok((update_tip, changeset)); } else { diff --git a/crates/chain/src/spk_client.rs b/crates/chain/src/spk_client.rs index 75fb66984..e31b431dd 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/chain/src/spk_client.rs @@ -3,9 +3,8 @@ use crate::{ alloc::{boxed::Box, collections::VecDeque, vec::Vec}, collections::BTreeMap, local_chain::CheckPoint, - Indexed, + ConfirmationBlockTime, Indexed, }; -use bdk_core::ConfirmationBlockTime; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; type InspectSync = dyn FnMut(SyncItem, SyncProgress) + Send + 'static; diff --git a/crates/core/src/checkpoint.rs b/crates/core/src/checkpoint.rs new file mode 100644 index 000000000..0abadda1d --- /dev/null +++ b/crates/core/src/checkpoint.rs @@ -0,0 +1,233 @@ +use core::ops::RangeBounds; + +use alloc::sync::Arc; +use bitcoin::BlockHash; + +use crate::BlockId; + +/// A checkpoint is a node of a reference-counted linked list of [`BlockId`]s. +/// +/// Checkpoints are cheaply cloneable and are useful to find the agreement point between two sparse +/// block chains. +#[derive(Debug, Clone)] +pub struct CheckPoint(Arc); + +/// The internal contents of [`CheckPoint`]. +#[derive(Debug, Clone)] +struct CPInner { + /// Block id (hash and height). + block: BlockId, + /// Previous checkpoint (if any). + prev: Option>, +} + +impl PartialEq for CheckPoint { + fn eq(&self, other: &Self) -> bool { + let self_cps = self.iter().map(|cp| cp.block_id()); + let other_cps = other.iter().map(|cp| cp.block_id()); + self_cps.eq(other_cps) + } +} + +impl CheckPoint { + /// Construct a new base block at the front of a linked list. + pub fn new(block: BlockId) -> Self { + Self(Arc::new(CPInner { block, prev: None })) + } + + /// Construct a checkpoint from a list of [`BlockId`]s in ascending height order. + /// + /// # Errors + /// + /// This method will error if any of the follow occurs: + /// + /// - The `blocks` iterator is empty, in which case, the error will be `None`. + /// - The `blocks` iterator is not in ascending height order. + /// - The `blocks` iterator contains multiple [`BlockId`]s of the same height. + /// + /// The error type is the last successful checkpoint constructed (if any). + pub fn from_block_ids( + block_ids: impl IntoIterator, + ) -> Result> { + let mut blocks = block_ids.into_iter(); + let mut acc = CheckPoint::new(blocks.next().ok_or(None)?); + for id in blocks { + acc = acc.push(id).map_err(Some)?; + } + Ok(acc) + } + + /// Construct a checkpoint from the given `header` and block `height`. + /// + /// If `header` is of the genesis block, the checkpoint won't have a [`prev`] node. Otherwise, + /// we return a checkpoint linked with the previous block. + /// + /// [`prev`]: CheckPoint::prev + pub fn from_header(header: &bitcoin::block::Header, height: u32) -> Self { + let hash = header.block_hash(); + let this_block_id = BlockId { height, hash }; + + let prev_height = match height.checked_sub(1) { + Some(h) => h, + None => return Self::new(this_block_id), + }; + + let prev_block_id = BlockId { + height: prev_height, + hash: header.prev_blockhash, + }; + + CheckPoint::new(prev_block_id) + .push(this_block_id) + .expect("must construct checkpoint") + } + + /// Puts another checkpoint onto the linked list representing the blockchain. + /// + /// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you + /// are pushing on to. + pub fn push(self, block: BlockId) -> Result { + if self.height() < block.height { + Ok(Self(Arc::new(CPInner { + block, + prev: Some(self.0), + }))) + } else { + Err(self) + } + } + + /// Extends the checkpoint linked list by a iterator of block ids. + /// + /// Returns an `Err(self)` if there is block which does not have a greater height than the + /// previous one. + pub fn extend(self, blocks: impl IntoIterator) -> Result { + let mut curr = self.clone(); + for block in blocks { + curr = curr.push(block).map_err(|_| self.clone())?; + } + Ok(curr) + } + + /// Get the [`BlockId`] of the checkpoint. + pub fn block_id(&self) -> BlockId { + self.0.block + } + + /// Get the height of the checkpoint. + pub fn height(&self) -> u32 { + self.0.block.height + } + + /// Get the block hash of the checkpoint. + pub fn hash(&self) -> BlockHash { + self.0.block.hash + } + + /// Get the previous checkpoint in the chain + pub fn prev(&self) -> Option { + self.0.prev.clone().map(CheckPoint) + } + + /// Iterate from this checkpoint in descending height. + pub fn iter(&self) -> CheckPointIter { + self.clone().into_iter() + } + + /// Get checkpoint at `height`. + /// + /// Returns `None` if checkpoint at `height` does not exist`. + pub fn get(&self, height: u32) -> Option { + self.range(height..=height).next() + } + + /// Iterate checkpoints over a height range. + /// + /// Note that we always iterate checkpoints in reverse height order (iteration starts at tip + /// height). + pub fn range(&self, range: R) -> impl Iterator + where + R: RangeBounds, + { + let start_bound = range.start_bound().cloned(); + let end_bound = range.end_bound().cloned(); + self.iter() + .skip_while(move |cp| match end_bound { + core::ops::Bound::Included(inc_bound) => cp.height() > inc_bound, + core::ops::Bound::Excluded(exc_bound) => cp.height() >= exc_bound, + core::ops::Bound::Unbounded => false, + }) + .take_while(move |cp| match start_bound { + core::ops::Bound::Included(inc_bound) => cp.height() >= inc_bound, + core::ops::Bound::Excluded(exc_bound) => cp.height() > exc_bound, + core::ops::Bound::Unbounded => true, + }) + } + + /// Inserts `block_id` at its height within the chain. + /// + /// The effect of `insert` depends on whether a height already exists. If it doesn't the + /// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after + /// it. If the height already existed and has a conflicting block hash then it will be purged + /// along with all block followin it. The returned chain will have a tip of the `block_id` + /// passed in. Of course, if the `block_id` was already present then this just returns `self`. + #[must_use] + pub fn insert(self, block_id: BlockId) -> Self { + assert_ne!(block_id.height, 0, "cannot insert the genesis block"); + + let mut cp = self.clone(); + let mut tail = vec![]; + let base = loop { + if cp.height() == block_id.height { + if cp.hash() == block_id.hash { + return self; + } + // if we have a conflict we just return the inserted block because the tail is by + // implication invalid. + tail = vec![]; + break cp.prev().expect("can't be called on genesis block"); + } + + if cp.height() < block_id.height { + break cp; + } + + tail.push(cp.block_id()); + cp = cp.prev().expect("will break before genesis block"); + }; + + base.extend(core::iter::once(block_id).chain(tail.into_iter().rev())) + .expect("tail is in order") + } + + /// This method tests for `self` and `other` to have equal internal pointers. + pub fn eq_ptr(&self, other: &Self) -> bool { + Arc::as_ptr(&self.0) == Arc::as_ptr(&other.0) + } +} + +/// Iterates over checkpoints backwards. +pub struct CheckPointIter { + current: Option>, +} + +impl Iterator for CheckPointIter { + type Item = CheckPoint; + + fn next(&mut self) -> Option { + let current = self.current.clone()?; + self.current.clone_from(¤t.prev); + Some(CheckPoint(current)) + } +} + +impl IntoIterator for CheckPoint { + type Item = CheckPoint; + type IntoIter = CheckPointIter; + + fn into_iter(self) -> Self::IntoIter { + CheckPointIter { + current: Some(self.0), + } + } +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 1c5358401..f8ac5e328 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -56,3 +56,6 @@ pub mod collections { mod chain_data; pub use chain_data::*; + +mod checkpoint; +pub use checkpoint::*; diff --git a/crates/testenv/src/lib.rs b/crates/testenv/src/lib.rs index 6d169bdce..747acc448 100644 --- a/crates/testenv/src/lib.rs +++ b/crates/testenv/src/lib.rs @@ -4,8 +4,7 @@ use bdk_chain::{ secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget, ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid, }, - local_chain::CheckPoint, - BlockId, + BlockId, local_chain::CheckPoint, }; use bitcoincore_rpc::{ bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules}, From bdea871a0d0d337772f226fdcf5cd734f3d857ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 16:57:59 +0000 Subject: [PATCH 25/77] feat!: move `tx_graph::Update` to `bdk_core` --- crates/chain/src/tx_graph.rs | 35 +--------------------------- crates/core/src/lib.rs | 45 ++++++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 206aaf11e..085418fc3 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -98,6 +98,7 @@ use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge}; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; use alloc::vec::Vec; +pub use bdk_core::tx_graph::Update; use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; use core::fmt::{self, Formatter}; use core::{ @@ -105,30 +106,6 @@ use core::{ ops::{Deref, RangeInclusive}, }; -/// Data object used to update the [`TxGraph`] with. -#[derive(Debug, Clone)] -pub struct Update { - /// Full transactions. - pub txs: Vec>, - /// Floating txouts. - pub txouts: BTreeMap, - /// Transaction anchors. - pub anchors: BTreeSet<(A, Txid)>, - /// Seen at times for transactions. - pub seen_ats: HashMap, -} - -impl Default for Update { - fn default() -> Self { - Self { - txs: Default::default(), - txouts: Default::default(), - anchors: Default::default(), - seen_ats: Default::default(), - } - } -} - impl From> for Update { fn from(graph: TxGraph) -> Self { Self { @@ -151,16 +128,6 @@ impl From> for TxGraph { } } -impl Update { - /// Extend this update with `other`. - pub fn extend(&mut self, other: Update) { - self.txs.extend(other.txs); - self.txouts.extend(other.txouts); - self.anchors.extend(other.anchors); - self.seen_ats.extend(other.seen_ats); - } -} - /// A graph of transactions and spends. /// /// See the [module-level documentation] for more. diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index f8ac5e328..bed9da4a1 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -59,3 +59,48 @@ pub use chain_data::*; mod checkpoint; pub use checkpoint::*; + +/// Core structures for [`TxGraph`]. +/// +/// [`TxGraph`]: https://docs.rs/bdk_chain/latest/bdk_chain/tx_graph/struct.TxGraph.html +pub mod tx_graph { + use crate::collections::{BTreeMap, BTreeSet, HashMap}; + use alloc::{sync::Arc, vec::Vec}; + use bitcoin::{OutPoint, Transaction, TxOut, Txid}; + + /// Data object used to update a [`TxGraph`]. + /// + /// [`TxGraph`]: https://docs.rs/bdk_chain/latest/bdk_chain/tx_graph/struct.TxGraph.html + #[derive(Debug, Clone)] + pub struct Update { + /// Full transactions. + pub txs: Vec>, + /// Floating txouts. + pub txouts: BTreeMap, + /// Transaction anchors. + pub anchors: BTreeSet<(A, Txid)>, + /// Seen at times for transactions. + pub seen_ats: HashMap, + } + + impl Default for Update { + fn default() -> Self { + Self { + txs: Default::default(), + txouts: Default::default(), + anchors: Default::default(), + seen_ats: Default::default(), + } + } + } + + impl Update { + /// Extend this update with `other`. + pub fn extend(&mut self, other: Update) { + self.txs.extend(other.txs); + self.txouts.extend(other.txouts); + self.anchors.extend(other.anchors); + self.seen_ats.extend(other.seen_ats); + } + } +} From ab0315d14fa741a691ee0deef4567ea66cb44a60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 18:12:46 +0000 Subject: [PATCH 26/77] feat!: move `spk_client`s to `bdk_core` Also introduced extension trait for builder methods that take in a `KeychainTxOutIndex`. `Indexed` and `KeychainIndexed` are also moved to `bdk_core`. --- crates/chain/src/indexer/keychain_txout.rs | 41 ++++++++++++++++ crates/chain/src/lib.rs | 6 --- crates/core/Cargo.toml | 3 ++ crates/core/src/lib.rs | 7 +++ crates/{chain => core}/src/spk_client.rs | 57 +++++----------------- crates/testenv/src/lib.rs | 3 +- crates/wallet/src/wallet/mod.rs | 2 + example-crates/example_esplora/src/main.rs | 1 + 8 files changed, 68 insertions(+), 52 deletions(-) rename crates/{chain => core}/src/spk_client.rs (90%) diff --git a/crates/chain/src/indexer/keychain_txout.rs b/crates/chain/src/indexer/keychain_txout.rs index ce9707c7b..c43208095 100644 --- a/crates/chain/src/indexer/keychain_txout.rs +++ b/crates/chain/src/indexer/keychain_txout.rs @@ -4,6 +4,7 @@ use crate::{ collections::*, miniscript::{Descriptor, DescriptorPublicKey}, + spk_client::{FullScanRequestBuilder, SyncRequestBuilder}, spk_iter::BIP32_MAX_INDEX, spk_txout::SpkTxOutIndex, DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator, @@ -875,3 +876,43 @@ impl Merge for ChangeSet { self.last_revealed.is_empty() } } + +/// Trait to extend [`SyncRequestBuilder`]. +pub trait SyncRequestBuilderExt { + /// Add [`Script`](bitcoin::Script)s that are revealed by the `indexer` of the given `spk_range` + /// that will be synced against. + fn revealed_spks_from_indexer(self, indexer: &KeychainTxOutIndex, spk_range: R) -> Self + where + R: core::ops::RangeBounds; + + /// Add [`Script`](bitcoin::Script)s that are revealed by the `indexer` but currently unused. + fn unused_spks_from_indexer(self, indexer: &KeychainTxOutIndex) -> Self; +} + +impl SyncRequestBuilderExt for SyncRequestBuilder<(K, u32)> { + fn revealed_spks_from_indexer(self, indexer: &KeychainTxOutIndex, spk_range: R) -> Self + where + R: core::ops::RangeBounds, + { + self.spks_with_indexes(indexer.revealed_spks(spk_range)) + } + + fn unused_spks_from_indexer(self, indexer: &KeychainTxOutIndex) -> Self { + self.spks_with_indexes(indexer.unused_spks()) + } +} + +/// Trait to extend [`FullScanRequestBuilder`]. +pub trait FullScanRequestBuilderExt { + /// Add spk iterators for each keychain tracked in `indexer`. + fn spks_from_indexer(self, indexer: &KeychainTxOutIndex) -> Self; +} + +impl FullScanRequestBuilderExt for FullScanRequestBuilder { + fn spks_from_indexer(mut self, indexer: &KeychainTxOutIndex) -> Self { + for (keychain, spks) in indexer.all_unbounded_spk_iters() { + self = self.spks_for_keychain(keychain, spks); + } + self + } +} diff --git a/crates/chain/src/lib.rs b/crates/chain/src/lib.rs index c1c555961..9667bb549 100644 --- a/crates/chain/src/lib.rs +++ b/crates/chain/src/lib.rs @@ -61,7 +61,6 @@ pub use indexer::keychain_txout; pub use spk_iter::*; #[cfg(feature = "rusqlite")] pub mod rusqlite_impl; -pub mod spk_client; pub extern crate bdk_core; pub use bdk_core::*; @@ -81,11 +80,6 @@ extern crate std; /// How many confirmations are needed f or a coinbase output to be spent. pub const COINBASE_MATURITY: u32 = 100; -/// A tuple of keychain index and `T` representing the indexed value. -pub type Indexed = (u32, T); -/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them. -pub type KeychainIndexed = ((K, u32), T); - /// A wrapper that we use to impl remote traits for types in our crate or dependency crates. pub struct Impl(pub T); diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d74cf906c..80741b074 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -19,3 +19,6 @@ hashbrown = { version = "0.9.1", optional = true } default = ["std"] std = ["bitcoin/std"] serde = ["dep:serde", "bitcoin/serde", "hashbrown?/serde"] + +[dev-dependencies] +bdk_chain = { version = "0.17.0", path = "../chain" } diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index bed9da4a1..038bee621 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -54,12 +54,19 @@ pub mod collections { pub use hashbrown::hash_map; } +/// A tuple of keychain index and `T` representing the indexed value. +pub type Indexed = (u32, T); +/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them. +pub type KeychainIndexed = ((K, u32), T); + mod chain_data; pub use chain_data::*; mod checkpoint; pub use checkpoint::*; +pub mod spk_client; + /// Core structures for [`TxGraph`]. /// /// [`TxGraph`]: https://docs.rs/bdk_chain/latest/bdk_chain/tx_graph/struct.TxGraph.html diff --git a/crates/chain/src/spk_client.rs b/crates/core/src/spk_client.rs similarity index 90% rename from crates/chain/src/spk_client.rs rename to crates/core/src/spk_client.rs index e31b431dd..e54849fbb 100644 --- a/crates/chain/src/spk_client.rs +++ b/crates/core/src/spk_client.rs @@ -2,8 +2,7 @@ use crate::{ alloc::{boxed::Box, collections::VecDeque, vec::Vec}, collections::BTreeMap, - local_chain::CheckPoint, - ConfirmationBlockTime, Indexed, + CheckPoint, ConfirmationBlockTime, Indexed, }; use bitcoin::{OutPoint, Script, ScriptBuf, Txid}; @@ -101,27 +100,6 @@ impl Default for SyncRequestBuilder { } } -#[cfg(feature = "miniscript")] -impl SyncRequestBuilder<(K, u32)> { - /// Add [`Script`]s that are revealed by the `indexer` of the given `spk_range` that will be - /// synced against. - pub fn revealed_spks_from_indexer( - self, - indexer: &crate::indexer::keychain_txout::KeychainTxOutIndex, - spk_range: impl core::ops::RangeBounds, - ) -> Self { - self.spks_with_indexes(indexer.revealed_spks(spk_range)) - } - - /// Add [`Script`]s that are revealed by the `indexer` but currently unused. - pub fn unused_spks_from_indexer( - self, - indexer: &crate::indexer::keychain_txout::KeychainTxOutIndex, - ) -> Self { - self.spks_with_indexes(indexer.unused_spks()) - } -} - impl SyncRequestBuilder<()> { /// Add [`Script`]s that will be synced against. pub fn spks(self, spks: impl IntoIterator) -> Self { @@ -132,7 +110,7 @@ impl SyncRequestBuilder<()> { impl SyncRequestBuilder { /// Set the initial chain tip for the sync request. /// - /// This is used to update [`LocalChain`](crate::local_chain::LocalChain). + /// This is used to update [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). pub fn chain_tip(mut self, cp: CheckPoint) -> Self { self.inner.chain_tip = Some(cp); self @@ -143,7 +121,7 @@ impl SyncRequestBuilder { /// # Example /// /// Sync revealed script pubkeys obtained from a - /// [`KeychainTxOutIndex`](crate::keychain_txout::KeychainTxOutIndex). + /// [`KeychainTxOutIndex`](../../bdk_chain/indexer/keychain_txout/struct.KeychainTxOutIndex.html). /// /// ```rust /// # use bdk_chain::spk_client::SyncRequest; @@ -216,9 +194,9 @@ impl SyncRequestBuilder { /// /// ```rust /// # use bdk_chain::{bitcoin::{hashes::Hash, ScriptBuf}, local_chain::LocalChain}; +/// # use bdk_chain::spk_client::SyncRequest; /// # let (local_chain, _) = LocalChain::from_genesis_hash(Hash::all_zeros()); /// # let scripts = [ScriptBuf::default(), ScriptBuf::default()]; -/// # use bdk_chain::spk_client::SyncRequest; /// // Construct a sync request. /// let sync_request = SyncRequest::builder() /// // Provide chain tip of the local wallet. @@ -345,9 +323,11 @@ impl SyncRequest { #[must_use] #[derive(Debug)] pub struct SyncResult { - /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). + /// The update to apply to the receiving + /// [`TxGraph`](../../bdk_chain/tx_graph/struct.TxGraph.html). pub graph_update: crate::tx_graph::Update, - /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). + /// The update to apply to the receiving + /// [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). pub chain_update: Option, } @@ -374,24 +354,10 @@ impl Default for FullScanRequestBuilder { } } -#[cfg(feature = "miniscript")] -impl FullScanRequestBuilder { - /// Add spk iterators for each keychain tracked in `indexer`. - pub fn spks_from_indexer( - mut self, - indexer: &crate::indexer::keychain_txout::KeychainTxOutIndex, - ) -> Self { - for (keychain, spks) in indexer.all_unbounded_spk_iters() { - self = self.spks_for_keychain(keychain, spks); - } - self - } -} - impl FullScanRequestBuilder { /// Set the initial chain tip for the full scan request. /// - /// This is used to update [`LocalChain`](crate::local_chain::LocalChain). + /// This is used to update [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). pub fn chain_tip(mut self, tip: CheckPoint) -> Self { self.inner.chain_tip = Some(tip); self @@ -496,9 +462,10 @@ impl FullScanRequest { #[must_use] #[derive(Debug)] pub struct FullScanResult { - /// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain). + /// The update to apply to the receiving + /// [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). pub graph_update: crate::tx_graph::Update, - /// The update to apply to the receiving [`TxGraph`](crate::tx_graph::TxGraph). + /// The update to apply to the receiving [`TxGraph`](../../bdk_chain/tx_graph/struct.TxGraph.html). pub chain_update: Option, /// Last active indices for the corresponding keychains (`K`). pub last_active_indices: BTreeMap, diff --git a/crates/testenv/src/lib.rs b/crates/testenv/src/lib.rs index 747acc448..6d169bdce 100644 --- a/crates/testenv/src/lib.rs +++ b/crates/testenv/src/lib.rs @@ -4,7 +4,8 @@ use bdk_chain::{ secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget, ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid, }, - BlockId, local_chain::CheckPoint, + local_chain::CheckPoint, + BlockId, }; use bitcoincore_rpc::{ bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules}, diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 638bb5757..72c42ddfe 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2472,6 +2472,7 @@ impl Wallet { /// [`SyncRequest`] collects all revealed script pubkeys from the wallet keychain needed to /// start a blockchain sync with a spk based blockchain client. pub fn start_sync_with_revealed_spks(&self) -> SyncRequestBuilder<(KeychainKind, u32)> { + use bdk_chain::keychain_txout::SyncRequestBuilderExt; SyncRequest::builder() .chain_tip(self.chain.tip()) .revealed_spks_from_indexer(&self.indexed_graph.index, ..) @@ -2486,6 +2487,7 @@ impl Wallet { /// This operation is generally only used when importing or restoring a previously used wallet /// in which the list of used scripts is not known. pub fn start_full_scan(&self) -> FullScanRequestBuilder { + use bdk_chain::keychain_txout::FullScanRequestBuilderExt; FullScanRequest::builder() .chain_tip(self.chain.tip()) .spks_from_indexer(&self.indexed_graph.index) diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index d4692e35c..7a4005878 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -6,6 +6,7 @@ use std::{ use bdk_chain::{ bitcoin::Network, + keychain_txout::FullScanRequestBuilderExt, spk_client::{FullScanRequest, SyncRequest}, Merge, }; From 0d302f5f204eeac8902a4b5943b9b820c6b575ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 18:26:28 +0000 Subject: [PATCH 27/77] feat(electrum)!: depend on `bdk_core` instead of `bdk_chain` `.populate_tx_cache` has been changed to take in a collection of `Arc`. --- crates/electrum/Cargo.toml | 3 +- crates/electrum/src/bdk_electrum_client.rs | 41 ++++++++++----------- crates/electrum/src/lib.rs | 24 +++++++----- example-crates/example_electrum/src/main.rs | 9 ++++- example-crates/wallet_electrum/src/main.rs | 2 +- 5 files changed, 44 insertions(+), 35 deletions(-) diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index eff11daac..b91c13662 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -10,11 +10,12 @@ license = "MIT OR Apache-2.0" readme = "README.md" [dependencies] -bdk_chain = { path = "../chain", version = "0.17.0" } +bdk_core = { path = "../core", version = "0.1" } electrum-client = { version = "0.21", features = ["proxy"], default-features = false } [dev-dependencies] bdk_testenv = { path = "../testenv", default-features = false } +bdk_chain = { path = "../chain", version = "0.17.0" } [features] default = ["use-rustls"] diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index 571660756..d78d7f64c 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -1,10 +1,8 @@ -use bdk_chain::{ +use bdk_core::{ bitcoin::{block::Header, BlockHash, OutPoint, ScriptBuf, Transaction, Txid}, collections::{BTreeMap, HashMap}, - local_chain::CheckPoint, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, - tx_graph::{self, TxGraph}, - Anchor, BlockId, ConfirmationBlockTime, + tx_graph, BlockId, CheckPoint, ConfirmationBlockTime, }; use electrum_client::{ElectrumApi, Error, HeaderNotification}; use std::{ @@ -39,14 +37,11 @@ impl BdkElectrumClient { /// Inserts transactions into the transaction cache so that the client will not fetch these /// transactions. - pub fn populate_tx_cache(&self, tx_graph: impl AsRef>) { - let txs = tx_graph - .as_ref() - .full_txs() - .map(|tx_node| (tx_node.txid, tx_node.tx)); - + pub fn populate_tx_cache(&self, txs: impl IntoIterator>>) { let mut tx_cache = self.tx_cache.lock().unwrap(); - for (txid, tx) in txs { + for tx in txs { + let tx = tx.into(); + let txid = tx.compute_txid(); tx_cache.insert(txid, tx); } } @@ -121,9 +116,10 @@ impl BdkElectrumClient { /// [`CalculateFeeError::MissingTxOut`] error if those `TxOut`s are not /// present in the transaction graph. /// - /// [`CalculateFeeError::MissingTxOut`]: bdk_chain::tx_graph::CalculateFeeError::MissingTxOut - /// [`Wallet.calculate_fee`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/struct.Wallet.html#method.calculate_fee - /// [`Wallet.calculate_fee_rate`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/struct.Wallet.html#method.calculate_fee_rate + /// [`bdk_chain`]: ../bdk_chain/index.html + /// [`CalculateFeeError::MissingTxOut`]: ../bdk_chain/tx_graph/enum.CalculateFeeError.html#variant.MissingTxOut + /// [`Wallet.calculate_fee`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee + /// [`Wallet.calculate_fee_rate`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee_rate pub fn full_scan( &self, request: impl Into>, @@ -189,9 +185,10 @@ impl BdkElectrumClient { /// may include scripts that have been used, use [`full_scan`] with the keychain. /// /// [`full_scan`]: Self::full_scan - /// [`CalculateFeeError::MissingTxOut`]: bdk_chain::tx_graph::CalculateFeeError::MissingTxOut - /// [`Wallet.calculate_fee`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/struct.Wallet.html#method.calculate_fee - /// [`Wallet.calculate_fee_rate`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/struct.Wallet.html#method.calculate_fee_rate + /// [`bdk_chain`]: ../bdk_chain/index.html + /// [`CalculateFeeError::MissingTxOut`]: ../bdk_chain/tx_graph/enum.CalculateFeeError.html#variant.MissingTxOut + /// [`Wallet.calculate_fee`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee + /// [`Wallet.calculate_fee_rate`]: ../bdk_wallet/struct.Wallet.html#method.calculate_fee_rate pub fn sync( &self, request: impl Into>, @@ -514,20 +511,20 @@ fn fetch_tip_and_latest_blocks( // Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not // surpass `latest_blocks`. -fn chain_update( +fn chain_update( mut tip: CheckPoint, latest_blocks: &BTreeMap, - anchors: impl Iterator, + anchors: impl Iterator, ) -> Result { - for anchor in anchors { - let height = anchor.0.anchor_block().height; + for (anchor, _txid) in anchors { + let height = anchor.block_id.height; // Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent // in case of a re-org. if tip.get(height).is_none() && height <= tip.height() { let hash = match latest_blocks.get(&height) { Some(&hash) => hash, - None => anchor.0.anchor_block().hash, + None => anchor.block_id.hash, }; tip = tip.insert(BlockId { hash, height }); } diff --git a/crates/electrum/src/lib.rs b/crates/electrum/src/lib.rs index d303ee403..914b4f19a 100644 --- a/crates/electrum/src/lib.rs +++ b/crates/electrum/src/lib.rs @@ -1,22 +1,26 @@ -//! This crate is used for updating structures of [`bdk_chain`] with data from an Electrum server. +//! This crate is used for returning updates from Electrum servers. //! -//! The two primary methods are [`BdkElectrumClient::sync`] and [`BdkElectrumClient::full_scan`]. In most cases -//! [`BdkElectrumClient::sync`] is used to sync the transaction histories of scripts that the application -//! cares about, for example the scripts for all the receive addresses of a Wallet's keychain that it -//! has shown a user. [`BdkElectrumClient::full_scan`] is meant to be used when importing or restoring a -//! keychain where the range of possibly used scripts is not known. In this case it is necessary to -//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a -//! sync or full scan the user receives relevant blockchain data and output updates for -//! [`bdk_chain`]. +//! Updates are returned as either a [`SyncResult`] (if [`BdkElectrumClient::sync()`] is called), +//! or a [`FullScanResult`] (if [`BdkElectrumClient::full_scan()`] is called). +//! +//! In most cases [`BdkElectrumClient::sync()`] is used to sync the transaction histories of scripts +//! that the application cares about, for example the scripts for all the receive addresses of a +//! Wallet's keychain that it has shown a user. +//! +//! [`BdkElectrumClient::full_scan`] is meant to be used when importing or restoring a keychain +//! where the range of possibly used scripts is not known. In this case it is necessary to scan all +//! keychain scripts until a number (the "stop gap") of unused scripts is discovered. //! //! Refer to [`example_electrum`] for a complete example. //! //! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum +//! [`SyncResult`]: bdk_core::spk_client::SyncResult +//! [`FullScanResult`]: bdk_core::spk_client::FullScanResult #![warn(missing_docs)] mod bdk_electrum_client; pub use bdk_electrum_client::*; -pub use bdk_chain; +pub use bdk_core; pub use electrum_client; diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 662bc4237..21910cf66 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -127,7 +127,14 @@ fn main() -> anyhow::Result<()> { let client = BdkElectrumClient::new(electrum_cmd.electrum_args().client(network)?); // Tell the electrum client about the txs we've already got locally so it doesn't re-download them - client.populate_tx_cache(&*graph.lock().unwrap()); + client.populate_tx_cache( + graph + .lock() + .unwrap() + .graph() + .full_txs() + .map(|tx_node| tx_node.tx), + ); let (chain_update, graph_update, keychain_update) = match electrum_cmd.clone() { ElectrumCommands::Scan { diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index c05184052..47cbfa15d 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -50,7 +50,7 @@ fn main() -> Result<(), anyhow::Error> { // Populate the electrum client's transaction cache so it doesn't redownload transaction we // already have. - client.populate_tx_cache(wallet.tx_graph()); + client.populate_tx_cache(wallet.tx_graph().full_txs().map(|tx_node| tx_node.tx)); let request = wallet.start_full_scan().inspect({ let mut stdout = std::io::stdout(); From fea8eede760130db32c2cfaecc272f6c1ed979db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 18:36:07 +0000 Subject: [PATCH 28/77] feat(esplora)!: depend on `bdk_core` instead of `bdk_chain` Helper methods have been changed to use concrete types (instead of `A: Anchor` generic) - since `Anchor` is still part of `bdk_chain`. Also fix esplora docs. --- crates/esplora/Cargo.toml | 3 +- crates/esplora/README.md | 4 +-- crates/esplora/src/async_ext.rs | 39 ++++++++++++----------- crates/esplora/src/blocking_ext.rs | 50 +++++++++++++++++------------- crates/esplora/src/lib.rs | 9 ++---- 5 files changed, 55 insertions(+), 50 deletions(-) diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 9148a0f86..8e0cf0464 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -12,13 +12,14 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bdk_chain = { path = "../chain", version = "0.17.0", default-features = false } +bdk_core = { path = "../core", version = "0.1", default-features = false } esplora-client = { version = "0.9.0", default-features = false } async-trait = { version = "0.1.66", optional = true } futures = { version = "0.3.26", optional = true } miniscript = { version = "12.0.0", optional = true, default-features = false } [dev-dependencies] +bdk_chain = { path = "../chain", version = "0.17.0" } bdk_testenv = { path = "../testenv", default-features = false } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/crates/esplora/README.md b/crates/esplora/README.md index ef2e65176..0535b9a38 100644 --- a/crates/esplora/README.md +++ b/crates/esplora/README.md @@ -37,7 +37,7 @@ For full examples, refer to [`example-crates/wallet_esplora_blocking`](https://g [`bdk_chain`]: https://docs.rs/bdk-chain/ [`EsploraExt`]: crate::EsploraExt [`EsploraAsyncExt`]: crate::EsploraAsyncExt -[`SyncRequest`]: bdk_chain::spk_client::SyncRequest -[`FullScanRequest`]: bdk_chain::spk_client::FullScanRequest +[`SyncRequest`]: bdk_core::spk_client::SyncRequest +[`FullScanRequest`]: bdk_core::spk_client::FullScanRequest [`sync`]: crate::EsploraExt::sync [`full_scan`]: crate::EsploraExt::full_scan diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index f3c8e966a..041205c5e 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -1,14 +1,10 @@ -use std::collections::{BTreeSet, HashSet}; - use async_trait::async_trait; -use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; -use bdk_chain::{ +use bdk_core::collections::{BTreeMap, BTreeSet, HashSet}; +use bdk_core::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; +use bdk_core::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, - collections::BTreeMap, - local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, + tx_graph, BlockId, CheckPoint, ConfirmationBlockTime, Indexed, }; -use bdk_chain::{tx_graph, Anchor, Indexed}; use futures::{stream::FuturesOrdered, TryStreamExt}; use crate::{insert_anchor_from_status, insert_prevouts}; @@ -209,11 +205,11 @@ async fn fetch_block( /// /// We want to have a corresponding checkpoint per anchor height. However, checkpoints fetched /// should not surpass `latest_blocks`. -async fn chain_update( +async fn chain_update( client: &esplora_client::AsyncClient, latest_blocks: &BTreeMap, local_tip: &CheckPoint, - anchors: &BTreeSet<(A, Txid)>, + anchors: &BTreeSet<(ConfirmationBlockTime, Txid)>, ) -> Result { let mut point_of_agreement = None; let mut conflicts = vec![]; @@ -242,8 +238,8 @@ async fn chain_update( .extend(conflicts.into_iter().rev()) .expect("evicted are in order"); - for anchor in anchors { - let height = anchor.0.anchor_block().height; + for (anchor, _txid) in anchors { + let height = anchor.block_id.height; if tip.get(height).is_none() { let hash = match fetch_block(client, latest_blocks, height).await? { Some(hash) => hash, @@ -494,6 +490,7 @@ mod test { local_chain::LocalChain, BlockId, }; + use bdk_core::ConfirmationBlockTime; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; use esplora_client::Builder; @@ -572,9 +569,12 @@ mod test { .iter() .map(|&height| -> anyhow::Result<_> { Ok(( - BlockId { - height, - hash: env.bitcoind.client.get_block_hash(height as _)?, + ConfirmationBlockTime { + block_id: BlockId { + height, + hash: env.bitcoind.client.get_block_hash(height as _)?, + }, + confirmation_time: height as _, }, Txid::all_zeros(), )) @@ -610,9 +610,12 @@ mod test { .iter() .map(|&(height, txid)| -> anyhow::Result<_> { Ok(( - BlockId { - height, - hash: env.bitcoind.client.get_block_hash(height as _)?, + ConfirmationBlockTime { + block_id: BlockId { + height, + hash: env.bitcoind.client.get_block_hash(height as _)?, + }, + confirmation_time: height as _, }, txid, )) diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index 62f0d351e..d0744b4ed 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -1,15 +1,11 @@ -use std::collections::{BTreeSet, HashSet}; -use std::thread::JoinHandle; - -use bdk_chain::collections::BTreeMap; -use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; -use bdk_chain::{ +use bdk_core::collections::{BTreeMap, BTreeSet, HashSet}; +use bdk_core::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; +use bdk_core::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, - local_chain::CheckPoint, - BlockId, ConfirmationBlockTime, + tx_graph, BlockId, CheckPoint, ConfirmationBlockTime, Indexed, }; -use bdk_chain::{tx_graph, Anchor, Indexed}; use esplora_client::{OutputStatus, Tx}; +use std::thread::JoinHandle; use crate::{insert_anchor_from_status, insert_prevouts}; @@ -199,11 +195,11 @@ fn fetch_block( /// /// We want to have a corresponding checkpoint per anchor height. However, checkpoints fetched /// should not surpass `latest_blocks`. -fn chain_update( +fn chain_update( client: &esplora_client::BlockingClient, latest_blocks: &BTreeMap, local_tip: &CheckPoint, - anchors: &BTreeSet<(A, Txid)>, + anchors: &BTreeSet<(ConfirmationBlockTime, Txid)>, ) -> Result { let mut point_of_agreement = None; let mut conflicts = vec![]; @@ -232,8 +228,8 @@ fn chain_update( .extend(conflicts.into_iter().rev()) .expect("evicted are in order"); - for anchor in anchors { - let height = anchor.0.anchor_block().height; + for (anchor, _) in anchors { + let height = anchor.block_id.height; if tip.get(height).is_none() { let hash = match fetch_block(client, latest_blocks, height)? { Some(hash) => hash, @@ -475,6 +471,7 @@ mod test { use bdk_chain::bitcoin::Txid; use bdk_chain::local_chain::LocalChain; use bdk_chain::BlockId; + use bdk_core::ConfirmationBlockTime; use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv}; use esplora_client::{BlockHash, Builder}; use std::collections::{BTreeMap, BTreeSet}; @@ -561,9 +558,12 @@ mod test { .iter() .map(|&height| -> anyhow::Result<_> { Ok(( - BlockId { - height, - hash: env.bitcoind.client.get_block_hash(height as _)?, + ConfirmationBlockTime { + block_id: BlockId { + height, + hash: env.bitcoind.client.get_block_hash(height as _)?, + }, + confirmation_time: height as _, }, Txid::all_zeros(), )) @@ -598,9 +598,12 @@ mod test { .iter() .map(|&(height, txid)| -> anyhow::Result<_> { Ok(( - BlockId { - height, - hash: env.bitcoind.client.get_block_hash(height as _)?, + ConfirmationBlockTime { + block_id: BlockId { + height, + hash: env.bitcoind.client.get_block_hash(height as _)?, + }, + confirmation_time: height as _, }, txid, )) @@ -794,9 +797,12 @@ mod test { let txid: Txid = bdk_chain::bitcoin::hashes::Hash::hash( &format!("txid_at_height_{}", h).into_bytes(), ); - let anchor = BlockId { - height: h, - hash: anchor_blockhash, + let anchor = ConfirmationBlockTime { + block_id: BlockId { + height: h, + hash: anchor_blockhash, + }, + confirmation_time: h as _, }; (anchor, txid) }) diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs index 9a6e8f1df..7aad133b4 100644 --- a/crates/esplora/src/lib.rs +++ b/crates/esplora/src/lib.rs @@ -19,14 +19,9 @@ //! Just like how [`EsploraExt`] extends the functionality of an //! [`esplora_client::BlockingClient`], [`EsploraAsyncExt`] is the async version which extends //! [`esplora_client::AsyncClient`]. -//! -//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph -//! [`LocalChain`]: bdk_chain::local_chain::LocalChain -//! [`ChainOracle`]: bdk_chain::ChainOracle -//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora -use bdk_chain::bitcoin::{Amount, OutPoint, TxOut, Txid}; -use bdk_chain::{tx_graph, BlockId, ConfirmationBlockTime}; +use bdk_core::bitcoin::{Amount, OutPoint, TxOut, Txid}; +use bdk_core::{tx_graph, BlockId, ConfirmationBlockTime}; use esplora_client::TxStatus; pub use esplora_client; From dafb9aaef7d8d4bc83c8ba0845323b92b82fbb3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Fri, 23 Aug 2024 18:38:55 +0000 Subject: [PATCH 29/77] feat(bitcoind_rpc)!: depend on `bdk_core` instead of `bdk_chain` --- crates/bitcoind_rpc/Cargo.toml | 7 ++++--- crates/bitcoind_rpc/src/lib.rs | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index bee58efa1..e601d6fe2 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -15,12 +15,13 @@ readme = "README.md" [dependencies] bitcoin = { version = "0.32.0", default-features = false } bitcoincore-rpc = { version = "0.19.0" } -bdk_chain = { path = "../chain", version = "0.17", default-features = false } +bdk_core = { path = "../core", version = "0.1", default-features = false } [dev-dependencies] bdk_testenv = { path = "../testenv", default-features = false } +bdk_chain = { path = "../chain", version = "0.17" } [features] default = ["std"] -std = ["bitcoin/std", "bdk_chain/std"] -serde = ["bitcoin/serde", "bdk_chain/serde"] +std = ["bitcoin/std", "bdk_core/std"] +serde = ["bitcoin/serde", "bdk_core/serde"] diff --git a/crates/bitcoind_rpc/src/lib.rs b/crates/bitcoind_rpc/src/lib.rs index ce5e863bb..49121cead 100644 --- a/crates/bitcoind_rpc/src/lib.rs +++ b/crates/bitcoind_rpc/src/lib.rs @@ -9,7 +9,7 @@ //! mempool. #![warn(missing_docs)] -use bdk_chain::{local_chain::CheckPoint, BlockId}; +use bdk_core::{BlockId, CheckPoint}; use bitcoin::{block::Header, Block, BlockHash, Transaction}; pub use bitcoincore_rpc; use bitcoincore_rpc::bitcoincore_rpc_json; From a5d076f215cd91173f55bda0d1cc59b9dde75511 Mon Sep 17 00:00:00 2001 From: LLFourn Date: Sun, 25 Aug 2024 15:35:18 +1000 Subject: [PATCH 30/77] chore(core)!: s/tx_graph::Update/TxUpdate/ We shouldn't refer to `tx_graph` in `bdk_core`. TxGraph happens to consume `Update` but it doesn't conceptually own the definition of an update to transactions. I tried to also remove a lot of references to "graph" where they shouldn't be. "graph" is a very general kind of data structure so we should avoid referring to it where it's not relevant. `tx_update` is much better than `graph_update`. --- crates/chain/src/indexed_tx_graph.rs | 6 +- crates/chain/src/tx_graph.rs | 14 ++-- crates/chain/tests/test_tx_graph.rs | 22 +++---- .../core/src/{chain_data.rs => block_id.rs} | 0 crates/core/src/lib.rs | 52 ++------------- crates/core/src/spk_client.rs | 24 ++++--- crates/core/src/tx_update.rs | 43 ++++++++++++ crates/electrum/src/bdk_electrum_client.rs | 66 +++++++++---------- crates/electrum/tests/test_electrum.rs | 22 +++---- crates/esplora/src/async_ext.rs | 36 +++++----- crates/esplora/src/blocking_ext.rs | 36 +++++----- crates/esplora/src/lib.rs | 6 +- crates/esplora/tests/async_ext.rs | 16 ++--- crates/esplora/tests/blocking_ext.rs | 16 ++--- crates/wallet/src/wallet/export.rs | 2 +- crates/wallet/src/wallet/mod.rs | 12 ++-- crates/wallet/tests/common.rs | 2 +- crates/wallet/tests/wallet.rs | 2 +- example-crates/example_electrum/src/main.rs | 8 +-- example-crates/example_esplora/src/main.rs | 6 +- 20 files changed, 195 insertions(+), 196 deletions(-) rename crates/core/src/{chain_data.rs => block_id.rs} (100%) create mode 100644 crates/core/src/tx_update.rs diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index 73ae458ff..ed2a1f0ce 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -90,10 +90,10 @@ where /// Apply an `update` directly. /// - /// `update` is a [`tx_graph::Update`] and the resultant changes is returned as [`ChangeSet`]. + /// `update` is a [`tx_graph::TxUpdate`] and the resultant changes is returned as [`ChangeSet`]. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - pub fn apply_update(&mut self, update: tx_graph::Update) -> ChangeSet { + pub fn apply_update(&mut self, update: tx_graph::TxUpdate) -> ChangeSet { let tx_graph = self.graph.apply_update(update); let indexer = self.index_tx_graph_changeset(&tx_graph); ChangeSet { tx_graph, indexer } @@ -114,7 +114,7 @@ where /// set to the current time. pub fn apply_update_at( &mut self, - update: tx_graph::Update, + update: tx_graph::TxUpdate, seen_at: Option, ) -> ChangeSet { let tx_graph = self.graph.apply_update_at(update, seen_at); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 085418fc3..127b47cf2 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -78,7 +78,7 @@ //! # let tx_b = tx_from_hex(RAW_TX_2); //! let mut graph: TxGraph = TxGraph::default(); //! -//! let mut update = tx_graph::Update::default(); +//! let mut update = tx_graph::TxUpdate::default(); //! update.txs.push(Arc::new(tx_a)); //! update.txs.push(Arc::new(tx_b)); //! @@ -98,7 +98,7 @@ use crate::{Anchor, Balance, ChainOracle, ChainPosition, FullTxOut, Merge}; use alloc::collections::vec_deque::VecDeque; use alloc::sync::Arc; use alloc::vec::Vec; -pub use bdk_core::tx_graph::Update; +pub use bdk_core::TxUpdate; use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid}; use core::fmt::{self, Formatter}; use core::{ @@ -106,7 +106,7 @@ use core::{ ops::{Deref, RangeInclusive}, }; -impl From> for Update { +impl From> for TxUpdate { fn from(graph: TxGraph) -> Self { Self { txs: graph.full_txs().map(|tx_node| tx_node.tx).collect(), @@ -120,8 +120,8 @@ impl From> for Update { } } -impl From> for TxGraph { - fn from(update: Update) -> Self { +impl From> for TxGraph { + fn from(update: TxUpdate) -> Self { let mut graph = TxGraph::::default(); let _ = graph.apply_update_at(update, None); graph @@ -655,7 +655,7 @@ impl TxGraph { /// exist in `update` but not in `self`). #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] - pub fn apply_update(&mut self, update: Update) -> ChangeSet { + pub fn apply_update(&mut self, update: TxUpdate) -> ChangeSet { use std::time::*; let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -676,7 +676,7 @@ impl TxGraph { /// /// Use [`apply_update`](TxGraph::apply_update) to have the `seen_at` value automatically set /// to the current time. - pub fn apply_update_at(&mut self, update: Update, seen_at: Option) -> ChangeSet { + pub fn apply_update_at(&mut self, update: TxUpdate, seen_at: Option) -> ChangeSet { let mut changeset = ChangeSet::::default(); let mut unanchored_txs = HashSet::::new(); for tx in update.txs { diff --git a/crates/chain/tests/test_tx_graph.rs b/crates/chain/tests/test_tx_graph.rs index 4ce6772bb..a49c9e5f5 100644 --- a/crates/chain/tests/test_tx_graph.rs +++ b/crates/chain/tests/test_tx_graph.rs @@ -88,7 +88,7 @@ fn insert_txouts() { // Make the update graph let update = { - let mut update = tx_graph::Update::default(); + let mut update = tx_graph::TxUpdate::default(); for (outpoint, txout) in &update_ops { // Insert partials transactions. update.txouts.insert(*outpoint, txout.clone()); @@ -1137,12 +1137,12 @@ fn call_map_anchors_with_non_deterministic_anchor() { ); } -/// Tests `From` impls for conversion between [`TxGraph`] and [`tx_graph::Update`]. +/// Tests `From` impls for conversion between [`TxGraph`] and [`tx_graph::TxUpdate`]. #[test] fn tx_graph_update_conversion() { - use tx_graph::Update; + use tx_graph::TxUpdate; - type TestCase = (&'static str, Update); + type TestCase = (&'static str, TxUpdate); fn make_tx(v: i32) -> Transaction { Transaction { @@ -1161,24 +1161,24 @@ fn tx_graph_update_conversion() { } let test_cases: &[TestCase] = &[ - ("empty_update", Update::default()), + ("empty_update", TxUpdate::default()), ( "single_tx", - Update { + TxUpdate { txs: vec![make_tx(0).into()], ..Default::default() }, ), ( "two_txs", - Update { + TxUpdate { txs: vec![make_tx(0).into(), make_tx(1).into()], ..Default::default() }, ), ( "with_floating_txouts", - Update { + TxUpdate { txs: vec![make_tx(0).into(), make_tx(1).into()], txouts: [ (OutPoint::new(h!("a"), 0), make_txout(0)), @@ -1191,7 +1191,7 @@ fn tx_graph_update_conversion() { ), ( "with_anchors", - Update { + TxUpdate { txs: vec![make_tx(0).into(), make_tx(1).into()], txouts: [ (OutPoint::new(h!("a"), 0), make_txout(0)), @@ -1209,7 +1209,7 @@ fn tx_graph_update_conversion() { ), ( "with_seen_ats", - Update { + TxUpdate { txs: vec![make_tx(0).into(), make_tx(1).into()], txouts: [ (OutPoint::new(h!("a"), 0), make_txout(0)), @@ -1230,7 +1230,7 @@ fn tx_graph_update_conversion() { for (test_name, update) in test_cases { let mut tx_graph = TxGraph::::default(); let _ = tx_graph.apply_update_at(update.clone(), None); - let update_from_tx_graph: Update = tx_graph.into(); + let update_from_tx_graph: TxUpdate = tx_graph.into(); assert_eq!( update diff --git a/crates/core/src/chain_data.rs b/crates/core/src/block_id.rs similarity index 100% rename from crates/core/src/chain_data.rs rename to crates/core/src/block_id.rs diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 038bee621..aeb34dca3 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -59,55 +59,13 @@ pub type Indexed = (u32, T); /// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them. pub type KeychainIndexed = ((K, u32), T); -mod chain_data; -pub use chain_data::*; +mod block_id; +pub use block_id::*; mod checkpoint; pub use checkpoint::*; -pub mod spk_client; - -/// Core structures for [`TxGraph`]. -/// -/// [`TxGraph`]: https://docs.rs/bdk_chain/latest/bdk_chain/tx_graph/struct.TxGraph.html -pub mod tx_graph { - use crate::collections::{BTreeMap, BTreeSet, HashMap}; - use alloc::{sync::Arc, vec::Vec}; - use bitcoin::{OutPoint, Transaction, TxOut, Txid}; - - /// Data object used to update a [`TxGraph`]. - /// - /// [`TxGraph`]: https://docs.rs/bdk_chain/latest/bdk_chain/tx_graph/struct.TxGraph.html - #[derive(Debug, Clone)] - pub struct Update { - /// Full transactions. - pub txs: Vec>, - /// Floating txouts. - pub txouts: BTreeMap, - /// Transaction anchors. - pub anchors: BTreeSet<(A, Txid)>, - /// Seen at times for transactions. - pub seen_ats: HashMap, - } +mod tx_update; +pub use tx_update::*; - impl Default for Update { - fn default() -> Self { - Self { - txs: Default::default(), - txouts: Default::default(), - anchors: Default::default(), - seen_ats: Default::default(), - } - } - } - - impl Update { - /// Extend this update with `other`. - pub fn extend(&mut self, other: Update) { - self.txs.extend(other.txs); - self.txouts.extend(other.txouts); - self.anchors.extend(other.anchors); - self.seen_ats.extend(other.seen_ats); - } - } -} +pub mod spk_client; diff --git a/crates/core/src/spk_client.rs b/crates/core/src/spk_client.rs index e54849fbb..1aceae5bb 100644 --- a/crates/core/src/spk_client.rs +++ b/crates/core/src/spk_client.rs @@ -323,18 +323,16 @@ impl SyncRequest { #[must_use] #[derive(Debug)] pub struct SyncResult { - /// The update to apply to the receiving - /// [`TxGraph`](../../bdk_chain/tx_graph/struct.TxGraph.html). - pub graph_update: crate::tx_graph::Update, - /// The update to apply to the receiving - /// [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). + /// Relevant transaction data discovered during the scan. + pub tx_update: crate::TxUpdate, + /// Changes to the chain discovered during the scan. pub chain_update: Option, } impl Default for SyncResult { fn default() -> Self { Self { - graph_update: Default::default(), + tx_update: Default::default(), chain_update: Default::default(), } } @@ -462,19 +460,19 @@ impl FullScanRequest { #[must_use] #[derive(Debug)] pub struct FullScanResult { - /// The update to apply to the receiving - /// [`LocalChain`](../../bdk_chain/local_chain/struct.LocalChain.html). - pub graph_update: crate::tx_graph::Update, - /// The update to apply to the receiving [`TxGraph`](../../bdk_chain/tx_graph/struct.TxGraph.html). - pub chain_update: Option, - /// Last active indices for the corresponding keychains (`K`). + /// Relevant transaction data discovered during the scan. + pub tx_update: crate::TxUpdate, + /// Last active indices for the corresponding keychains (`K`). An index is active if it had a + /// transaction associated with the script pubkey at that index. pub last_active_indices: BTreeMap, + /// Changes to the chain discovered during the scan. + pub chain_update: Option, } impl Default for FullScanResult { fn default() -> Self { Self { - graph_update: Default::default(), + tx_update: Default::default(), chain_update: Default::default(), last_active_indices: Default::default(), } diff --git a/crates/core/src/tx_update.rs b/crates/core/src/tx_update.rs new file mode 100644 index 000000000..a1ff75de7 --- /dev/null +++ b/crates/core/src/tx_update.rs @@ -0,0 +1,43 @@ +use crate::collections::{BTreeMap, BTreeSet, HashMap}; +use alloc::{sync::Arc, vec::Vec}; +use bitcoin::{OutPoint, Transaction, TxOut, Txid}; + +/// Data object used to communicate updates about relevant transactions from some chain data soruce +/// to the core model (usually a `bdk_chain::TxGraph`). +#[derive(Debug, Clone)] +pub struct TxUpdate { + /// Full transactions. These are transactions that were determined to be relevant to the wallet + /// given the request. + pub txs: Vec>, + /// Floating txouts. These are `TxOut`s that exist but the whole transaction wasn't included in + /// `txs` since only knowing about the output is important. These are often used to help determine + /// the fee of a wallet transaction. + pub txouts: BTreeMap, + /// Transaction anchors. Anchors tells us a position in the chain where a transaction was + /// confirmed. + pub anchors: BTreeSet<(A, Txid)>, + /// Seen at times for transactions. This records when a transaction was most recently seen in + /// the user's mempool for the sake of tie-breaking other conflicting transactions. + pub seen_ats: HashMap, +} + +impl Default for TxUpdate { + fn default() -> Self { + Self { + txs: Default::default(), + txouts: Default::default(), + anchors: Default::default(), + seen_ats: Default::default(), + } + } +} + +impl TxUpdate { + /// Extend this update with `other`. + pub fn extend(&mut self, other: TxUpdate) { + self.txs.extend(other.txs); + self.txouts.extend(other.txouts); + self.anchors.extend(other.anchors); + self.seen_ats.extend(other.seen_ats); + } +} diff --git a/crates/electrum/src/bdk_electrum_client.rs b/crates/electrum/src/bdk_electrum_client.rs index d78d7f64c..a98063336 100644 --- a/crates/electrum/src/bdk_electrum_client.rs +++ b/crates/electrum/src/bdk_electrum_client.rs @@ -2,7 +2,7 @@ use bdk_core::{ bitcoin::{block::Header, BlockHash, OutPoint, ScriptBuf, Transaction, Txid}, collections::{BTreeMap, HashMap}, spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}, - tx_graph, BlockId, CheckPoint, ConfirmationBlockTime, + BlockId, CheckPoint, ConfirmationBlockTime, TxUpdate, }; use electrum_client::{ElectrumApi, Error, HeaderNotification}; use std::{ @@ -134,12 +134,12 @@ impl BdkElectrumClient { None => None, }; - let mut graph_update = tx_graph::Update::::default(); + let mut tx_update = TxUpdate::::default(); let mut last_active_indices = BTreeMap::::default(); for keychain in request.keychains() { let spks = request.iter_spks(keychain.clone()); if let Some(last_active_index) = - self.populate_with_spks(&mut graph_update, spks, stop_gap, batch_size)? + self.populate_with_spks(&mut tx_update, spks, stop_gap, batch_size)? { last_active_indices.insert(keychain, last_active_index); } @@ -147,20 +147,20 @@ impl BdkElectrumClient { // Fetch previous `TxOut`s for fee calculation if flag is enabled. if fetch_prev_txouts { - self.fetch_prev_txout(&mut graph_update)?; + self.fetch_prev_txout(&mut tx_update)?; } let chain_update = match tip_and_latest_blocks { Some((chain_tip, latest_blocks)) => Some(chain_update( chain_tip, &latest_blocks, - graph_update.anchors.iter().cloned(), + tx_update.anchors.iter().cloned(), )?), _ => None, }; Ok(FullScanResult { - graph_update, + tx_update, chain_update, last_active_indices, }) @@ -202,9 +202,9 @@ impl BdkElectrumClient { None => None, }; - let mut graph_update = tx_graph::Update::::default(); + let mut tx_update = TxUpdate::::default(); self.populate_with_spks( - &mut graph_update, + &mut tx_update, request .iter_spks() .enumerate() @@ -212,37 +212,37 @@ impl BdkElectrumClient { usize::MAX, batch_size, )?; - self.populate_with_txids(&mut graph_update, request.iter_txids())?; - self.populate_with_outpoints(&mut graph_update, request.iter_outpoints())?; + self.populate_with_txids(&mut tx_update, request.iter_txids())?; + self.populate_with_outpoints(&mut tx_update, request.iter_outpoints())?; // Fetch previous `TxOut`s for fee calculation if flag is enabled. if fetch_prev_txouts { - self.fetch_prev_txout(&mut graph_update)?; + self.fetch_prev_txout(&mut tx_update)?; } let chain_update = match tip_and_latest_blocks { Some((chain_tip, latest_blocks)) => Some(chain_update( chain_tip, &latest_blocks, - graph_update.anchors.iter().cloned(), + tx_update.anchors.iter().cloned(), )?), None => None, }; Ok(SyncResult { - graph_update, + tx_update, chain_update, }) } - /// Populate the `graph_update` with transactions/anchors associated with the given `spks`. + /// Populate the `tx_update` with transactions/anchors associated with the given `spks`. /// /// Transactions that contains an output with requested spk, or spends form an output with - /// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are + /// requested spk will be added to `tx_update`. Anchors of the aforementioned transactions are /// also included. fn populate_with_spks( &self, - graph_update: &mut tx_graph::Update, + tx_update: &mut TxUpdate, mut spks: impl Iterator, stop_gap: usize, batch_size: usize, @@ -275,20 +275,20 @@ impl BdkElectrumClient { } for tx_res in spk_history { - graph_update.txs.push(self.fetch_tx(tx_res.tx_hash)?); - self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?; + tx_update.txs.push(self.fetch_tx(tx_res.tx_hash)?); + self.validate_merkle_for_anchor(tx_update, tx_res.tx_hash, tx_res.height)?; } } } } - /// Populate the `graph_update` with associated transactions/anchors of `outpoints`. + /// Populate the `tx_update` with associated transactions/anchors of `outpoints`. /// /// Transactions in which the outpoint resides, and transactions that spend from the outpoint are /// included. Anchors of the aforementioned transactions are included. fn populate_with_outpoints( &self, - graph_update: &mut tx_graph::Update, + tx_update: &mut TxUpdate, outpoints: impl IntoIterator, ) -> Result<(), Error> { for outpoint in outpoints { @@ -311,8 +311,8 @@ impl BdkElectrumClient { if !has_residing && res.tx_hash == op_txid { has_residing = true; - graph_update.txs.push(Arc::clone(&op_tx)); - self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; + tx_update.txs.push(Arc::clone(&op_tx)); + self.validate_merkle_for_anchor(tx_update, res.tx_hash, res.height)?; } if !has_spending && res.tx_hash != op_txid { @@ -325,18 +325,18 @@ impl BdkElectrumClient { if !has_spending { continue; } - graph_update.txs.push(Arc::clone(&res_tx)); - self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?; + tx_update.txs.push(Arc::clone(&res_tx)); + self.validate_merkle_for_anchor(tx_update, res.tx_hash, res.height)?; } } } Ok(()) } - /// Populate the `graph_update` with transactions/anchors of the provided `txids`. + /// Populate the `tx_update` with transactions/anchors of the provided `txids`. fn populate_with_txids( &self, - graph_update: &mut tx_graph::Update, + tx_update: &mut TxUpdate, txids: impl IntoIterator, ) -> Result<(), Error> { for txid in txids { @@ -360,10 +360,10 @@ impl BdkElectrumClient { .into_iter() .find(|r| r.tx_hash == txid) { - self.validate_merkle_for_anchor(graph_update, txid, r.height)?; + self.validate_merkle_for_anchor(tx_update, txid, r.height)?; } - graph_update.txs.push(tx); + tx_update.txs.push(tx); } Ok(()) } @@ -372,7 +372,7 @@ impl BdkElectrumClient { // An anchor is inserted if the transaction is validated to be in a confirmed block. fn validate_merkle_for_anchor( &self, - graph_update: &mut tx_graph::Update, + tx_update: &mut TxUpdate, txid: Txid, confirmation_height: i32, ) -> Result<(), Error> { @@ -399,7 +399,7 @@ impl BdkElectrumClient { } if is_confirmed_tx { - graph_update.anchors.insert(( + tx_update.anchors.insert(( ConfirmationBlockTime { confirmation_time: header.time as u64, block_id: BlockId { @@ -418,17 +418,17 @@ impl BdkElectrumClient { // which we do not have by default. This data is needed to calculate the transaction fee. fn fetch_prev_txout( &self, - graph_update: &mut tx_graph::Update, + tx_update: &mut TxUpdate, ) -> Result<(), Error> { let mut no_dup = HashSet::::new(); - for tx in &graph_update.txs { + for tx in &tx_update.txs { if no_dup.insert(tx.compute_txid()) { for vin in &tx.input { let outpoint = vin.previous_output; let vout = outpoint.vout; let prev_tx = self.fetch_tx(outpoint.txid)?; let txout = prev_tx.output[vout as usize].clone(); - let _ = graph_update.txouts.insert(outpoint, txout); + let _ = tx_update.txouts.insert(outpoint, txout); } } } diff --git a/crates/electrum/tests/test_electrum.rs b/crates/electrum/tests/test_electrum.rs index d5e4a1596..5f032ba6c 100644 --- a/crates/electrum/tests/test_electrum.rs +++ b/crates/electrum/tests/test_electrum.rs @@ -49,7 +49,7 @@ where .apply_update(chain_update) .map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?; } - let _ = graph.apply_update(update.graph_update.clone()); + let _ = graph.apply_update(update.tx_update.clone()); Ok(update) } @@ -120,15 +120,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { "update should not alter original checkpoint tip since we already started with all checkpoints", ); - let graph_update = sync_update.graph_update; + let tx_update = sync_update.tx_update; let updated_graph = { let mut graph = TxGraph::::default(); - let _ = graph.apply_update(graph_update.clone()); + let _ = graph.apply_update(tx_update.clone()); graph }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in &graph_update.txs { + for tx in &tx_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); @@ -150,7 +150,7 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { } assert_eq!( - graph_update + tx_update .txs .iter() .map(|tx| tx.compute_txid()) @@ -217,7 +217,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1, false)? }; - assert!(full_scan_update.graph_update.txs.is_empty()); + assert!(full_scan_update.tx_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -227,7 +227,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; assert_eq!( full_scan_update - .graph_update + .tx_update .txs .first() .unwrap() @@ -259,7 +259,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { client.full_scan(request, 5, 1, false)? }; let txs: HashSet<_> = full_scan_update - .graph_update + .tx_update .txs .iter() .map(|tx| tx.compute_txid()) @@ -274,7 +274,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { client.full_scan(request, 6, 1, false)? }; let txs: HashSet<_> = full_scan_update - .graph_update + .tx_update .txs .iter() .map(|tx| tx.compute_txid()) @@ -478,7 +478,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { )?; // Retain a snapshot of all anchors before reorg process. - let initial_anchors = update.graph_update.anchors.clone(); + let initial_anchors = update.tx_update.anchors.clone(); assert_eq!(initial_anchors.len(), REORG_COUNT); for i in 0..REORG_COUNT { let (anchor, txid) = initial_anchors.iter().nth(i).unwrap(); @@ -509,7 +509,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> { )?; // Check that no new anchors are added during current reorg. - assert!(initial_anchors.is_superset(&update.graph_update.anchors)); + assert!(initial_anchors.is_superset(&update.tx_update.anchors)); assert_eq!( get_balance(&recv_chain, &recv_graph)?, diff --git a/crates/esplora/src/async_ext.rs b/crates/esplora/src/async_ext.rs index 041205c5e..04e07c7c4 100644 --- a/crates/esplora/src/async_ext.rs +++ b/crates/esplora/src/async_ext.rs @@ -3,7 +3,7 @@ use bdk_core::collections::{BTreeMap, BTreeSet, HashSet}; use bdk_core::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; use bdk_core::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, - tx_graph, BlockId, CheckPoint, ConfirmationBlockTime, Indexed, + BlockId, CheckPoint, ConfirmationBlockTime, Indexed, TxUpdate, }; use futures::{stream::FuturesOrdered, TryStreamExt}; @@ -67,7 +67,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { None }; - let mut graph_update = tx_graph::Update::::default(); + let mut tx_update = TxUpdate::::default(); let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in keychains { @@ -80,7 +80,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { parallel_requests, ) .await?; - graph_update.extend(update); + tx_update.extend(update); if let Some(last_active_index) = last_active_index { last_active_indices.insert(keychain, last_active_index); } @@ -88,14 +88,14 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { let chain_update = match (chain_tip, latest_blocks) { (Some(chain_tip), Some(latest_blocks)) => { - Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?) + Some(chain_update(self, &latest_blocks, &chain_tip, &tx_update.anchors).await?) } _ => None, }; Ok(FullScanResult { chain_update, - graph_update, + tx_update, last_active_indices, }) } @@ -114,9 +114,9 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { None }; - let mut graph_update = tx_graph::Update::::default(); + let mut tx_update = TxUpdate::::default(); let mut inserted_txs = HashSet::::new(); - graph_update.extend( + tx_update.extend( fetch_txs_with_spks( self, &mut inserted_txs, @@ -125,7 +125,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { ) .await?, ); - graph_update.extend( + tx_update.extend( fetch_txs_with_txids( self, &mut inserted_txs, @@ -134,7 +134,7 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { ) .await?, ); - graph_update.extend( + tx_update.extend( fetch_txs_with_outpoints( self, &mut inserted_txs, @@ -146,14 +146,14 @@ impl EsploraAsyncExt for esplora_client::AsyncClient { let chain_update = match (chain_tip, latest_blocks) { (Some(chain_tip), Some(latest_blocks)) => { - Some(chain_update(self, &latest_blocks, &chain_tip, &graph_update.anchors).await?) + Some(chain_update(self, &latest_blocks, &chain_tip, &tx_update.anchors).await?) } _ => None, }; Ok(SyncResult { chain_update, - graph_update, + tx_update, }) } } @@ -277,10 +277,10 @@ async fn fetch_txs_with_keychain_spks> + S mut keychain_spks: I, stop_gap: usize, parallel_requests: usize, -) -> Result<(tx_graph::Update, Option), Error> { +) -> Result<(TxUpdate, Option), Error> { type TxsOfSpkIndex = (u32, Vec); - let mut update = tx_graph::Update::::default(); + let mut update = TxUpdate::::default(); let mut last_index = Option::::None; let mut last_active_index = Option::::None; @@ -351,7 +351,7 @@ async fn fetch_txs_with_spks + Send>( inserted_txs: &mut HashSet, spks: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { @@ -377,11 +377,11 @@ async fn fetch_txs_with_txids + Send>( inserted_txs: &mut HashSet, txids: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { - let mut update = tx_graph::Update::::default(); + let mut update = TxUpdate::::default(); // Only fetch for non-inserted txs. let mut txids = txids .into_iter() @@ -426,12 +426,12 @@ async fn fetch_txs_with_outpoints + Send>( inserted_txs: &mut HashSet, outpoints: I, parallel_requests: usize, -) -> Result, Error> +) -> Result, Error> where I::IntoIter: Send, { let outpoints = outpoints.into_iter().collect::>(); - let mut update = tx_graph::Update::::default(); + let mut update = TxUpdate::::default(); // make sure txs exists in graph and tx statuses are updated // TODO: We should maintain a tx cache (like we do with Electrum). diff --git a/crates/esplora/src/blocking_ext.rs b/crates/esplora/src/blocking_ext.rs index d0744b4ed..2552e6e83 100644 --- a/crates/esplora/src/blocking_ext.rs +++ b/crates/esplora/src/blocking_ext.rs @@ -2,7 +2,7 @@ use bdk_core::collections::{BTreeMap, BTreeSet, HashSet}; use bdk_core::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult}; use bdk_core::{ bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid}, - tx_graph, BlockId, CheckPoint, ConfirmationBlockTime, Indexed, + BlockId, CheckPoint, ConfirmationBlockTime, Indexed, TxUpdate, }; use esplora_client::{OutputStatus, Tx}; use std::thread::JoinHandle; @@ -62,7 +62,7 @@ impl EsploraExt for esplora_client::BlockingClient { None }; - let mut graph_update = tx_graph::Update::default(); + let mut tx_update = TxUpdate::default(); let mut inserted_txs = HashSet::::new(); let mut last_active_indices = BTreeMap::::new(); for keychain in request.keychains() { @@ -74,7 +74,7 @@ impl EsploraExt for esplora_client::BlockingClient { stop_gap, parallel_requests, )?; - graph_update.extend(update); + tx_update.extend(update); if let Some(last_active_index) = last_active_index { last_active_indices.insert(keychain, last_active_index); } @@ -85,14 +85,14 @@ impl EsploraExt for esplora_client::BlockingClient { self, &latest_blocks, &chain_tip, - &graph_update.anchors, + &tx_update.anchors, )?), _ => None, }; Ok(FullScanResult { chain_update, - graph_update, + tx_update, last_active_indices, }) } @@ -111,21 +111,21 @@ impl EsploraExt for esplora_client::BlockingClient { None }; - let mut graph_update = tx_graph::Update::::default(); + let mut tx_update = TxUpdate::::default(); let mut inserted_txs = HashSet::::new(); - graph_update.extend(fetch_txs_with_spks( + tx_update.extend(fetch_txs_with_spks( self, &mut inserted_txs, request.iter_spks(), parallel_requests, )?); - graph_update.extend(fetch_txs_with_txids( + tx_update.extend(fetch_txs_with_txids( self, &mut inserted_txs, request.iter_txids(), parallel_requests, )?); - graph_update.extend(fetch_txs_with_outpoints( + tx_update.extend(fetch_txs_with_outpoints( self, &mut inserted_txs, request.iter_outpoints(), @@ -137,14 +137,14 @@ impl EsploraExt for esplora_client::BlockingClient { self, &latest_blocks, &chain_tip, - &graph_update.anchors, + &tx_update.anchors, )?), _ => None, }; Ok(SyncResult { chain_update, - graph_update, + tx_update, }) } } @@ -254,10 +254,10 @@ fn fetch_txs_with_keychain_spks>>( mut keychain_spks: I, stop_gap: usize, parallel_requests: usize, -) -> Result<(tx_graph::Update, Option), Error> { +) -> Result<(TxUpdate, Option), Error> { type TxsOfSpkIndex = (u32, Vec); - let mut update = tx_graph::Update::::default(); + let mut update = TxUpdate::::default(); let mut last_index = Option::::None; let mut last_active_index = Option::::None; @@ -331,7 +331,7 @@ fn fetch_txs_with_spks>( inserted_txs: &mut HashSet, spks: I, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { fetch_txs_with_keychain_spks( client, inserted_txs, @@ -353,8 +353,8 @@ fn fetch_txs_with_txids>( inserted_txs: &mut HashSet, txids: I, parallel_requests: usize, -) -> Result, Error> { - let mut update = tx_graph::Update::::default(); +) -> Result, Error> { + let mut update = TxUpdate::::default(); // Only fetch for non-inserted txs. let mut txids = txids .into_iter() @@ -405,9 +405,9 @@ fn fetch_txs_with_outpoints>( inserted_txs: &mut HashSet, outpoints: I, parallel_requests: usize, -) -> Result, Error> { +) -> Result, Error> { let outpoints = outpoints.into_iter().collect::>(); - let mut update = tx_graph::Update::::default(); + let mut update = TxUpdate::::default(); // make sure txs exists in graph and tx statuses are updated // TODO: We should maintain a tx cache (like we do with Electrum). diff --git a/crates/esplora/src/lib.rs b/crates/esplora/src/lib.rs index 7aad133b4..a166b6f9f 100644 --- a/crates/esplora/src/lib.rs +++ b/crates/esplora/src/lib.rs @@ -21,7 +21,7 @@ //! [`esplora_client::AsyncClient`]. use bdk_core::bitcoin::{Amount, OutPoint, TxOut, Txid}; -use bdk_core::{tx_graph, BlockId, ConfirmationBlockTime}; +use bdk_core::{BlockId, ConfirmationBlockTime, TxUpdate}; use esplora_client::TxStatus; pub use esplora_client; @@ -37,7 +37,7 @@ mod async_ext; pub use async_ext::*; fn insert_anchor_from_status( - update: &mut tx_graph::Update, + update: &mut TxUpdate, txid: Txid, status: TxStatus, ) { @@ -59,7 +59,7 @@ fn insert_anchor_from_status( /// Inserts floating txouts into `tx_graph` using [`Vin`](esplora_client::api::Vin)s returned by /// Esplora. fn insert_prevouts( - update: &mut tx_graph::Update, + update: &mut TxUpdate, esplora_inputs: impl IntoIterator, ) { let prevouts = esplora_inputs diff --git a/crates/esplora/tests/async_ext.rs b/crates/esplora/tests/async_ext.rs index 7b0ef7fa6..b535d2bfa 100644 --- a/crates/esplora/tests/async_ext.rs +++ b/crates/esplora/tests/async_ext.rs @@ -78,15 +78,15 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { "update should not alter original checkpoint tip since we already started with all checkpoints", ); - let graph_update = sync_update.graph_update; + let tx_update = sync_update.tx_update; let updated_graph = { let mut graph = TxGraph::::default(); - let _ = graph.apply_update(graph_update.clone()); + let _ = graph.apply_update(tx_update.clone()); graph }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in &graph_update.txs { + for tx in &tx_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); @@ -108,7 +108,7 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { } assert_eq!( - graph_update + tx_update .txs .iter() .map(|tx| tx.compute_txid()) @@ -177,7 +177,7 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1).await? }; - assert!(full_scan_update.graph_update.txs.is_empty()); + assert!(full_scan_update.tx_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -187,7 +187,7 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; assert_eq!( full_scan_update - .graph_update + .tx_update .txs .first() .unwrap() @@ -221,7 +221,7 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { client.full_scan(request, 5, 1).await? }; let txs: HashSet<_> = full_scan_update - .graph_update + .tx_update .txs .iter() .map(|tx| tx.compute_txid()) @@ -236,7 +236,7 @@ pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> { client.full_scan(request, 6, 1).await? }; let txs: HashSet<_> = full_scan_update - .graph_update + .tx_update .txs .iter() .map(|tx| tx.compute_txid()) diff --git a/crates/esplora/tests/blocking_ext.rs b/crates/esplora/tests/blocking_ext.rs index b3833b899..d4191ceb0 100644 --- a/crates/esplora/tests/blocking_ext.rs +++ b/crates/esplora/tests/blocking_ext.rs @@ -78,15 +78,15 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { "update should not alter original checkpoint tip since we already started with all checkpoints", ); - let graph_update = sync_update.graph_update; + let tx_update = sync_update.tx_update; let updated_graph = { let mut graph = TxGraph::::default(); - let _ = graph.apply_update(graph_update.clone()); + let _ = graph.apply_update(tx_update.clone()); graph }; // Check to see if we have the floating txouts available from our two created transactions' // previous outputs in order to calculate transaction fees. - for tx in &graph_update.txs { + for tx in &tx_update.txs { // Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the // floating txouts available from the transactions' previous outputs. let fee = updated_graph.calculate_fee(tx).expect("Fee must exist"); @@ -108,7 +108,7 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> { } assert_eq!( - graph_update + tx_update .txs .iter() .map(|tx| tx.compute_txid()) @@ -177,7 +177,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { .spks_for_keychain(0, spks.clone()); client.full_scan(request, 3, 1)? }; - assert!(full_scan_update.graph_update.txs.is_empty()); + assert!(full_scan_update.tx_update.txs.is_empty()); assert!(full_scan_update.last_active_indices.is_empty()); let full_scan_update = { let request = FullScanRequest::builder() @@ -187,7 +187,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { }; assert_eq!( full_scan_update - .graph_update + .tx_update .txs .first() .unwrap() @@ -221,7 +221,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { client.full_scan(request, 5, 1)? }; let txs: HashSet<_> = full_scan_update - .graph_update + .tx_update .txs .iter() .map(|tx| tx.compute_txid()) @@ -236,7 +236,7 @@ pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> { client.full_scan(request, 6, 1)? }; let txs: HashSet<_> = full_scan_update - .graph_update + .tx_update .txs .iter() .map(|tx| tx.compute_txid()) diff --git a/crates/wallet/src/wallet/export.rs b/crates/wallet/src/wallet/export.rs index 386d9d4e3..ad5a6b2a8 100644 --- a/crates/wallet/src/wallet/export.rs +++ b/crates/wallet/src/wallet/export.rs @@ -255,7 +255,7 @@ mod test { }; wallet .apply_update(Update { - graph: tx_graph::Update { + tx_update: tx_graph::TxUpdate { anchors: [(anchor, txid)].into_iter().collect(), ..Default::default() }, diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 72c42ddfe..5cad6bb86 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -33,7 +33,7 @@ use bdk_chain::{ FullScanRequest, FullScanRequestBuilder, FullScanResult, SyncRequest, SyncRequestBuilder, SyncResult, }, - tx_graph::{CanonicalTx, TxGraph, TxNode}, + tx_graph::{CanonicalTx, TxGraph, TxNode, TxUpdate}, BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, DescriptorExt, FullTxOut, Indexed, IndexedTxGraph, Merge, }; @@ -132,7 +132,7 @@ pub struct Update { pub last_active_indices: BTreeMap, /// Update for the wallet's internal [`TxGraph`]. - pub graph: chain::tx_graph::Update, + pub tx_update: TxUpdate, /// Update for the wallet's internal [`LocalChain`]. /// @@ -144,7 +144,7 @@ impl From> for Update { fn from(value: FullScanResult) -> Self { Self { last_active_indices: value.last_active_indices, - graph: value.graph_update, + tx_update: value.tx_update, chain: value.chain_update, } } @@ -154,7 +154,7 @@ impl From for Update { fn from(value: SyncResult) -> Self { Self { last_active_indices: BTreeMap::new(), - graph: value.graph_update, + tx_update: value.tx_update, chain: value.chain_update, } } @@ -2318,7 +2318,7 @@ impl Wallet { changeset.merge(index_changeset.into()); changeset.merge( self.indexed_graph - .apply_update_at(update.graph, seen_at) + .apply_update_at(update.tx_update, seen_at) .into(), ); self.stage.merge(changeset); @@ -2624,7 +2624,7 @@ macro_rules! doctest_wallet { block_id, }; let update = Update { - graph: tx_graph::Update { + tx_update: tx_graph::TxUpdate { anchors: [(anchor, txid)].into_iter().collect(), ..Default::default() }, diff --git a/crates/wallet/tests/common.rs b/crates/wallet/tests/common.rs index 561a9a5fb..375d680d1 100644 --- a/crates/wallet/tests/common.rs +++ b/crates/wallet/tests/common.rs @@ -220,7 +220,7 @@ pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: Confir wallet .apply_update(Update { - graph: tx_graph::Update { + tx_update: tx_graph::TxUpdate { anchors: [(anchor, txid)].into(), ..Default::default() }, diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 243161658..2da25cd28 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -83,7 +83,7 @@ fn insert_seen_at(wallet: &mut Wallet, txid: Txid, seen_at: u64) { use bdk_wallet::Update; wallet .apply_update(Update { - graph: tx_graph::Update { + tx_update: tx_graph::TxUpdate { seen_ats: [(txid, seen_at)].into_iter().collect(), ..Default::default() }, diff --git a/example-crates/example_electrum/src/main.rs b/example-crates/example_electrum/src/main.rs index 21910cf66..9c705a3df 100644 --- a/example-crates/example_electrum/src/main.rs +++ b/example-crates/example_electrum/src/main.rs @@ -136,7 +136,7 @@ fn main() -> anyhow::Result<()> { .map(|tx_node| tx_node.tx), ); - let (chain_update, graph_update, keychain_update) = match electrum_cmd.clone() { + let (chain_update, tx_update, keychain_update) = match electrum_cmd.clone() { ElectrumCommands::Scan { stop_gap, scan_options, @@ -182,7 +182,7 @@ fn main() -> anyhow::Result<()> { .context("scanning the blockchain")?; ( res.chain_update, - res.graph_update, + res.tx_update, Some(res.last_active_indices), ) } @@ -251,7 +251,7 @@ fn main() -> anyhow::Result<()> { // drop lock on graph and chain drop((graph, chain)); - (res.chain_update, res.graph_update, None) + (res.chain_update, res.tx_update, None) } }; @@ -267,7 +267,7 @@ fn main() -> anyhow::Result<()> { let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update); indexed_tx_graph_changeset.merge(keychain_changeset.into()); } - indexed_tx_graph_changeset.merge(graph.apply_update(graph_update)); + indexed_tx_graph_changeset.merge(graph.apply_update(tx_update)); ChangeSet { local_chain: chain_changeset, diff --git a/example-crates/example_esplora/src/main.rs b/example-crates/example_esplora/src/main.rs index 7a4005878..cba86b862 100644 --- a/example-crates/example_esplora/src/main.rs +++ b/example-crates/example_esplora/src/main.rs @@ -164,7 +164,7 @@ fn main() -> anyhow::Result<()> { }; // The client scans keychain spks for transaction histories, stopping after `stop_gap` - // is reached. It returns a `TxGraph` update (`graph_update`) and a structure that + // is reached. It returns a `TxGraph` update (`tx_update`) and a structure that // represents the last active spk derivation indices of keychains // (`keychain_indices_update`). let update = client @@ -183,7 +183,7 @@ fn main() -> anyhow::Result<()> { let index_changeset = graph .index .reveal_to_target_multi(&update.last_active_indices); - let mut indexed_tx_graph_changeset = graph.apply_update(update.graph_update); + let mut indexed_tx_graph_changeset = graph.apply_update(update.tx_update); indexed_tx_graph_changeset.merge(index_changeset.into()); indexed_tx_graph_changeset }, @@ -269,7 +269,7 @@ fn main() -> anyhow::Result<()> { .lock() .unwrap() .apply_update(update.chain_update.expect("request has chain tip"))?, - graph.lock().unwrap().apply_update(update.graph_update), + graph.lock().unwrap().apply_update(update.tx_update), ) } }; From 092e9be454b3f1f4754f3fefb70b203d7e86c423 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 25 Aug 2024 10:13:59 -0500 Subject: [PATCH 31/77] Bump bdk version to 1.0.0-beta.2 bdk_chain to 0.18.0 bdk_bitcoind_rpc to 0.14.0 bdk_electrum to 0.17.0 bdk_esplora to 0.17.0 bdk_file_store to 0.15.0 bdk_testenv to 0.8.0 bdk_hwi to 0.5.0 --- crates/bitcoind_rpc/Cargo.toml | 4 ++-- crates/chain/Cargo.toml | 4 ++-- crates/core/Cargo.toml | 2 +- crates/electrum/Cargo.toml | 6 +++--- crates/esplora/Cargo.toml | 4 ++-- crates/file_store/Cargo.toml | 4 ++-- crates/hwi/Cargo.toml | 6 +++--- crates/testenv/Cargo.toml | 6 +++--- crates/wallet/Cargo.toml | 10 +++++----- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index e601d6fe2..c39bd5287 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_bitcoind_rpc" -version = "0.13.0" +version = "0.14.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -19,7 +19,7 @@ bdk_core = { path = "../core", version = "0.1", default-features = false } [dev-dependencies] bdk_testenv = { path = "../testenv", default-features = false } -bdk_chain = { path = "../chain", version = "0.17" } +bdk_chain = { path = "../chain", version = "0.18" } [features] default = ["std"] diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 42a77441f..ab0670d55 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_chain" -version = "0.17.0" +version = "0.18.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -20,7 +20,7 @@ miniscript = { version = "12.0.0", optional = true, default-features = false } # Feature dependencies rusqlite = { version = "0.31.0", features = ["bundled"], optional = true } -serde_json = {version = "1", optional = true } +serde_json = { version = "1", optional = true } [dev-dependencies] rand = "0.8" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 80741b074..8220d70a2 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -21,4 +21,4 @@ std = ["bitcoin/std"] serde = ["dep:serde", "bitcoin/serde", "hashbrown?/serde"] [dev-dependencies] -bdk_chain = { version = "0.17.0", path = "../chain" } +bdk_chain = { version = "0.18.0", path = "../chain" } diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index b91c13662..c807cf830 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_electrum" -version = "0.16.0" +version = "0.17.0" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -11,11 +11,11 @@ readme = "README.md" [dependencies] bdk_core = { path = "../core", version = "0.1" } -electrum-client = { version = "0.21", features = ["proxy"], default-features = false } +electrum-client = { version = "0.21", features = [ "proxy" ], default-features = false } [dev-dependencies] bdk_testenv = { path = "../testenv", default-features = false } -bdk_chain = { path = "../chain", version = "0.17.0" } +bdk_chain = { path = "../chain", version = "0.18.0" } [features] default = ["use-rustls"] diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 8e0cf0464..d7e5290f3 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_esplora" -version = "0.16.0" +version = "0.17.0" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -19,7 +19,7 @@ futures = { version = "0.3.26", optional = true } miniscript = { version = "12.0.0", optional = true, default-features = false } [dev-dependencies] -bdk_chain = { path = "../chain", version = "0.17.0" } +bdk_chain = { path = "../chain", version = "0.18.0" } bdk_testenv = { path = "../testenv", default-features = false } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index 3e4c11d0c..8e8966bb4 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_file_store" -version = "0.14.0" +version = "0.15.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/bitcoindevkit/bdk" @@ -11,7 +11,7 @@ authors = ["Bitcoin Dev Kit Developers"] readme = "README.md" [dependencies] -bdk_chain = { path = "../chain", version = "0.17.0", features = [ "serde", "miniscript" ] } +bdk_chain = { path = "../chain", version = "0.18.0", features = [ "serde", "miniscript" ] } bincode = { version = "1" } serde = { version = "1", features = ["derive"] } diff --git a/crates/hwi/Cargo.toml b/crates/hwi/Cargo.toml index b4ae39fe9..947bb8358 100644 --- a/crates/hwi/Cargo.toml +++ b/crates/hwi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_hwi" -version = "0.4.0" +version = "0.5.0" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -9,5 +9,5 @@ license = "MIT OR Apache-2.0" readme = "README.md" [dependencies] -bdk_wallet = { path = "../wallet", version = "1.0.0-beta.1" } -hwi = { version = "0.9.0", features = [ "miniscript"] } +bdk_wallet = { path = "../wallet", version = "1.0.0-beta.2" } +hwi = { version = "0.9.0", features = [ "miniscript" ] } diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml index 2d9c26caa..688f81fbe 100644 --- a/crates/testenv/Cargo.toml +++ b/crates/testenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_testenv" -version = "0.7.0" +version = "0.8.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -13,8 +13,8 @@ readme = "README.md" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bdk_chain = { path = "../chain", version = "0.17", default-features = false } -electrsd = { version = "0.28.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] } +bdk_chain = { path = "../chain", version = "0.18", default-features = false } +electrsd = { version = "0.28.0", features = [ "bitcoind_25_0", "esplora_a33e97e1", "legacy" ] } [features] default = ["std"] diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index 44314a968..afdcf688c 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bdk_wallet" homepage = "https://bitcoindevkit.org" -version = "1.0.0-beta.1" +version = "1.0.0-beta.2" repository = "https://github.com/bitcoindevkit/bdk" documentation = "https://docs.rs/bdk" description = "A modern, lightweight, descriptor-based wallet library" @@ -14,12 +14,12 @@ rust-version = "1.63" [dependencies] rand_core = { version = "0.6.0" } -miniscript = { version = "12.0.0", features = ["serde"], default-features = false } -bitcoin = { version = "0.32.0", features = ["serde", "base64"], default-features = false } +miniscript = { version = "12.0.0", features = [ "serde" ], default-features = false } +bitcoin = { version = "0.32.0", features = [ "serde", "base64" ], default-features = false } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } -bdk_chain = { path = "../chain", version = "0.17.0", features = ["miniscript", "serde"], default-features = false } -bdk_file_store = { path = "../file_store", version = "0.14.0", optional = true } +bdk_chain = { path = "../chain", version = "0.18.0", features = [ "miniscript", "serde" ], default-features = false } +bdk_file_store = { path = "../file_store", version = "0.15.0", optional = true } # Optional dependencies bip39 = { version = "2.0", optional = true } From 9bd500f5f198df4dfc72a1f71c93f9b20d65431d Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 25 Aug 2024 12:19:36 -0500 Subject: [PATCH 32/77] chore(core): add missing README.md --- crates/core/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 crates/core/README.md diff --git a/crates/core/README.md b/crates/core/README.md new file mode 100644 index 000000000..08eae24fa --- /dev/null +++ b/crates/core/README.md @@ -0,0 +1,3 @@ +# BDK Core + +This crate is a collection of core structures used by the bdk_chain, bdk_wallet, and bdk chain data source crates. From 48b6a66f6e0edce305d41653f37b8549aaf7c060 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Sun, 25 Aug 2024 12:25:23 -0500 Subject: [PATCH 33/77] chore(core): remove bdk_chain dev-dependency version --- crates/core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 8220d70a2..5e27f768e 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -21,4 +21,4 @@ std = ["bitcoin/std"] serde = ["dep:serde", "bitcoin/serde", "hashbrown?/serde"] [dev-dependencies] -bdk_chain = { version = "0.18.0", path = "../chain" } +bdk_chain = { path = "../chain" } From 054d1483bb19e8415745033853ae0fa6943f01d9 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 27 Aug 2024 23:07:48 -0500 Subject: [PATCH 34/77] ci: add token for cron-update-rust.yml --- .github/workflows/cron-update-rust.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/cron-update-rust.yml b/.github/workflows/cron-update-rust.yml index 3801f78a8..918bea07e 100644 --- a/.github/workflows/cron-update-rust.yml +++ b/.github/workflows/cron-update-rust.yml @@ -10,6 +10,11 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - uses: tibdex/github-app-token@v1 + id: generate-token + with: + app_id: ${{ secrets.APP_ID }} + private_key: ${{ secrets.APP_PRIVATE_KEY }} - name: Update rust-version to use latest stable run: | set -x @@ -30,7 +35,7 @@ jobs: if: env.changes_made == 'true' uses: peter-evans/create-pull-request@v6 with: - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ steps.generate-token.outputs.token }} author: Update Rustc Bot committer: Update Rustc Bot branch: create-pull-request/update-rust-version From b140b32648241884e0e5e222fd7a0fc4e05908f1 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 27 Aug 2024 23:54:01 -0500 Subject: [PATCH 35/77] ci: gpg commit signing for cron-update-rust.yml --- .github/workflows/cron-update-rust.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cron-update-rust.yml b/.github/workflows/cron-update-rust.yml index 918bea07e..5741aa6b8 100644 --- a/.github/workflows/cron-update-rust.yml +++ b/.github/workflows/cron-update-rust.yml @@ -15,6 +15,11 @@ jobs: with: app_id: ${{ secrets.APP_ID }} private_key: ${{ secrets.APP_PRIVATE_KEY }} + - uses: crazy-max/ghaction-import-gpg@v5 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + git_user_signingkey: true + git_commit_gpgsign: true - name: Update rust-version to use latest stable run: | set -x @@ -36,8 +41,8 @@ jobs: uses: peter-evans/create-pull-request@v6 with: token: ${{ steps.generate-token.outputs.token }} - author: Update Rustc Bot - committer: Update Rustc Bot + author: Github Action + committer: Github Action branch: create-pull-request/update-rust-version title: | ci: automated update to rustc ${{ env.rust_version }} From 6b881f8ab46bd0f3c612e522df21dc48ae5e6955 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Fri, 30 Aug 2024 14:09:33 -0400 Subject: [PATCH 36/77] docs: update CONTRIBUTING.md --- CONTRIBUTING.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6a83ccf57..9c1dc6ff7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,10 +46,10 @@ Every new feature should be covered by functional tests where possible. When refactoring, structure your PR to make it easy to review and don't hesitate to split it into multiple small, focused PRs. -The Minimal Supported Rust Version is **1.57.0** (enforced by our CI). +The Minimum Supported Rust Version is **1.63.0** (enforced by our CI). Commits should cover both the issue fixed and the solution's rationale. -These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in mind. Commit messages should follow the ["Conventional Commits 1.0.0"](https://www.conventionalcommits.org/en/v1.0.0/) to make commit histories easier to read by humans and automated tools. +These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in mind. Commit messages follow the ["Conventional Commits 1.0.0"](https://www.conventionalcommits.org/en/v1.0.0/) to make commit histories easier to read by humans and automated tools. All commits must be [GPG signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). To facilitate communication with other contributors, the project is making use of GitHub's "assignee" field. First check that no one is assigned and then @@ -81,12 +81,19 @@ well as test out the patch set and opine on the technical merits of the patch. PR should be reviewed first on the conceptual level before focusing on code style or grammar fixes. +To merge a PR we require all CI tests to pass, the PR has at least one approving review by a maintainer with write access, and reasonable criticisms have been addressed. + Coding Conventions ------------------ This codebase uses spaces, not tabs. Use `cargo fmt` with the default settings to format code before committing. This is also enforced by the CI. +All public items must be documented. We adhere to the [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/about.html) with respect to documentation. + +The library is written using safe rust. Special consideration must be given to code which proposes an exception to the rule. + +All new features require testing. Tests should be unique and self-describing. If a test is in development or is broken or no longer useful, then a reason should be given for adding the `#[ignore]` attribute. Security -------- From 67d5fa695f3f748d176c182d25be5ef70f5127ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 2 Sep 2024 17:32:19 +0800 Subject: [PATCH 37/77] feat(chain): make various insert tx methods more generic Instead of having `Transaction` as input, we have a generic parameter where the bound is `Into>` for the following methods: * `IndexedTxGraph::insert_tx` * `IndexedTxGraph::batch_insert_unconfirmed` * `TxGraph::batch_insert_unconfirmed` --- crates/chain/src/indexed_tx_graph.rs | 17 +++++++---------- crates/chain/src/tx_graph.rs | 5 +++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index ed2a1f0ce..b2da7bf75 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -2,7 +2,7 @@ //! [`IndexedTxGraph`] documentation for more. use core::fmt::Debug; -use alloc::vec::Vec; +use alloc::{sync::Arc, vec::Vec}; use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; use crate::{ @@ -133,13 +133,10 @@ where } /// Insert and index a transaction into the graph. - pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet { - let graph = self.graph.insert_tx(tx); - let indexer = self.index_tx_graph_changeset(&graph); - ChangeSet { - tx_graph: graph, - indexer, - } + pub fn insert_tx>>(&mut self, tx: T) -> ChangeSet { + let tx_graph = self.graph.insert_tx(tx); + let indexer = self.index_tx_graph_changeset(&tx_graph); + ChangeSet { tx_graph, indexer } } /// Insert an `anchor` for a given transaction. @@ -239,9 +236,9 @@ where /// To filter out irrelevant transactions, use [`batch_insert_relevant_unconfirmed`] instead. /// /// [`batch_insert_relevant_unconfirmed`]: IndexedTxGraph::batch_insert_relevant_unconfirmed - pub fn batch_insert_unconfirmed( + pub fn batch_insert_unconfirmed>>( &mut self, - txs: impl IntoIterator, + txs: impl IntoIterator, ) -> ChangeSet { let graph = self.graph.batch_insert_unconfirmed(txs); let indexer = self.index_tx_graph_changeset(&graph); diff --git a/crates/chain/src/tx_graph.rs b/crates/chain/src/tx_graph.rs index 127b47cf2..9a32ccdfc 100644 --- a/crates/chain/src/tx_graph.rs +++ b/crates/chain/src/tx_graph.rs @@ -606,12 +606,13 @@ impl TxGraph { /// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The /// *last seen* communicates when the transaction is last seen in mempool which is used for /// conflict-resolution (refer to [`TxGraph::insert_seen_at`] for details). - pub fn batch_insert_unconfirmed( + pub fn batch_insert_unconfirmed>>( &mut self, - txs: impl IntoIterator, + txs: impl IntoIterator, ) -> ChangeSet { let mut changeset = ChangeSet::::default(); for (tx, seen_at) in txs { + let tx: Arc = tx.into(); changeset.merge(self.insert_seen_at(tx.compute_txid(), seen_at)); changeset.merge(self.insert_tx(tx)); } From c39284d8299c5a8b34b73ce947e93425d5cdc121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Tue, 3 Sep 2024 13:18:20 +0800 Subject: [PATCH 38/77] feat(wallet): make `Wallet::insert_tx` generic Instead of having `Transaction` as input, have `T: Into>`. --- crates/wallet/src/wallet/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 5cad6bb86..8513251ae 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -1093,7 +1093,7 @@ impl Wallet { /// By default the inserted `tx` won't be considered "canonical" because it's not known /// whether the transaction exists in the best chain. To know whether it exists, the tx /// must be broadcast to the network and the wallet synced via a chain source. - pub fn insert_tx(&mut self, tx: Transaction) -> bool { + pub fn insert_tx>>(&mut self, tx: T) -> bool { let mut changeset = ChangeSet::default(); changeset.merge(self.indexed_graph.insert_tx(tx).into()); let ret = !changeset.is_empty(); From 87e61212f556bccfaa2a64b74f727cedc0f70e9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Mon, 2 Sep 2024 17:59:09 +0800 Subject: [PATCH 39/77] feat(chain,wallet)!: change methods to take in generic instead of `&Transaction` * `Wallet::apply_unconfirmed_txs` * `IndexedTxGraph::batch_insert_relevant` * `IndexedTxGraph::batch_insert_relevant_unconfirmed` --- crates/chain/src/indexed_tx_graph.rs | 31 ++++++++++--------- crates/chain/tests/test_indexed_tx_graph.rs | 9 +++--- crates/wallet/src/wallet/mod.rs | 4 +-- .../example_bitcoind_rpc_polling/src/main.rs | 11 +++---- example-crates/wallet_rpc/src/main.rs | 2 +- 5 files changed, 30 insertions(+), 27 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index b2da7bf75..9cb1e820e 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -156,9 +156,9 @@ where /// /// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant /// transactions in `txs` will be ignored. `txs` do not need to be in topological order. - pub fn batch_insert_relevant<'t>( + pub fn batch_insert_relevant>>( &mut self, - txs: impl IntoIterator)>, + txs: impl IntoIterator)>, ) -> ChangeSet { // The algorithm below allows for non-topologically ordered transactions by using two loops. // This is achieved by: @@ -166,28 +166,28 @@ where // not store anything about them. // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` // returns true or not. (in a second loop). - let txs = txs.into_iter().collect::>(); + let txs = txs + .into_iter() + .map(|(tx, anchors)| (>>::into(tx), anchors)) + .collect::>(); let mut indexer = I::ChangeSet::default(); for (tx, _) in &txs { indexer.merge(self.index.index_tx(tx)); } - let mut graph = tx_graph::ChangeSet::default(); + let mut tx_graph = tx_graph::ChangeSet::default(); for (tx, anchors) in txs { - if self.index.is_tx_relevant(tx) { + if self.index.is_tx_relevant(&tx) { let txid = tx.compute_txid(); - graph.merge(self.graph.insert_tx(tx.clone())); + tx_graph.merge(self.graph.insert_tx(tx.clone())); for anchor in anchors { - graph.merge(self.graph.insert_anchor(txid, anchor)); + tx_graph.merge(self.graph.insert_anchor(txid, anchor)); } } } - ChangeSet { - tx_graph: graph, - indexer, - } + ChangeSet { tx_graph, indexer } } /// Batch insert unconfirmed transactions, filtering out those that are irrelevant. @@ -198,9 +198,9 @@ where /// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The /// *last seen* communicates when the transaction is last seen in the mempool which is used for /// conflict-resolution in [`TxGraph`] (refer to [`TxGraph::insert_seen_at`] for details). - pub fn batch_insert_relevant_unconfirmed<'t>( + pub fn batch_insert_relevant_unconfirmed>>( &mut self, - unconfirmed_txs: impl IntoIterator, + unconfirmed_txs: impl IntoIterator, ) -> ChangeSet { // The algorithm below allows for non-topologically ordered transactions by using two loops. // This is achieved by: @@ -208,7 +208,10 @@ where // not store anything about them. // 2. decide whether to insert them into the graph depending on whether `is_tx_relevant` // returns true or not. (in a second loop). - let txs = unconfirmed_txs.into_iter().collect::>(); + let txs = unconfirmed_txs + .into_iter() + .map(|(tx, last_seen)| (>>::into(tx), last_seen)) + .collect::>(); let mut indexer = I::ChangeSet::default(); for (tx, _) in &txs { diff --git a/crates/chain/tests/test_indexed_tx_graph.rs b/crates/chain/tests/test_indexed_tx_graph.rs index 361e5ab5c..1fbbcd0df 100644 --- a/crates/chain/tests/test_indexed_tx_graph.rs +++ b/crates/chain/tests/test_indexed_tx_graph.rs @@ -81,7 +81,7 @@ fn insert_relevant_txs() { }; assert_eq!( - graph.batch_insert_relevant(txs.iter().map(|tx| (tx, None))), + graph.batch_insert_relevant(txs.iter().cloned().map(|tx| (tx, None))), changeset, ); @@ -237,10 +237,10 @@ fn test_list_owned_txouts() { // Insert unconfirmed txs with a last_seen timestamp let _ = - graph.batch_insert_relevant([&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| { + graph.batch_insert_relevant([&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, &tx)| { let height = i as u32; ( - *tx, + tx.clone(), local_chain .get(height) .map(|cp| cp.block_id()) @@ -251,7 +251,8 @@ fn test_list_owned_txouts() { ) })); - let _ = graph.batch_insert_relevant_unconfirmed([&tx4, &tx5].iter().map(|tx| (*tx, 100))); + let _ = + graph.batch_insert_relevant_unconfirmed([&tx4, &tx5].iter().map(|&tx| (tx.clone(), 100))); // A helper lambda to extract and filter data from the graph. let fetch = diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 8513251ae..118fae088 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2441,9 +2441,9 @@ impl Wallet { /// **WARNING**: You must persist the changes resulting from one or more calls to this method /// if you need the applied unconfirmed transactions to be reloaded after closing the wallet. /// See [`Wallet::reveal_next_address`]. - pub fn apply_unconfirmed_txs<'t>( + pub fn apply_unconfirmed_txs>>( &mut self, - unconfirmed_txs: impl IntoIterator, + unconfirmed_txs: impl IntoIterator, ) { let indexed_graph_changeset = self .indexed_graph diff --git a/example-crates/example_bitcoind_rpc_polling/src/main.rs b/example-crates/example_bitcoind_rpc_polling/src/main.rs index d1833b071..95c547967 100644 --- a/example-crates/example_bitcoind_rpc_polling/src/main.rs +++ b/example-crates/example_bitcoind_rpc_polling/src/main.rs @@ -201,9 +201,10 @@ fn main() -> anyhow::Result<()> { } let mempool_txs = emitter.mempool()?; - let graph_changeset = graph.lock().unwrap().batch_insert_relevant_unconfirmed( - mempool_txs.iter().map(|(tx, time)| (tx, *time)), - ); + let graph_changeset = graph + .lock() + .unwrap() + .batch_insert_relevant_unconfirmed(mempool_txs); { let db = &mut *db.lock().unwrap(); db_stage.merge(ChangeSet { @@ -286,9 +287,7 @@ fn main() -> anyhow::Result<()> { (chain_changeset, graph_changeset) } Emission::Mempool(mempool_txs) => { - let graph_changeset = graph.batch_insert_relevant_unconfirmed( - mempool_txs.iter().map(|(tx, time)| (tx, *time)), - ); + let graph_changeset = graph.batch_insert_relevant_unconfirmed(mempool_txs); (local_chain::ChangeSet::default(), graph_changeset) } Emission::Tip(h) => { diff --git a/example-crates/wallet_rpc/src/main.rs b/example-crates/wallet_rpc/src/main.rs index 388ccaf67..af6dcafc4 100644 --- a/example-crates/wallet_rpc/src/main.rs +++ b/example-crates/wallet_rpc/src/main.rs @@ -157,7 +157,7 @@ fn main() -> anyhow::Result<()> { } Emission::Mempool(mempool_emission) => { let start_apply_mempool = Instant::now(); - wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time))); + wallet.apply_unconfirmed_txs(mempool_emission); wallet.persist(&mut db)?; println!( "Applied unconfirmed transactions in {}s", From 7d2faa4a5b09a1ec3f4e87552dacb8716ac235bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Tue, 3 Sep 2024 15:28:27 +0800 Subject: [PATCH 40/77] feat(core): add `TxUpdate::map_anchors` --- crates/core/src/tx_update.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/core/src/tx_update.rs b/crates/core/src/tx_update.rs index a1ff75de7..29d6c2530 100644 --- a/crates/core/src/tx_update.rs +++ b/crates/core/src/tx_update.rs @@ -33,6 +33,23 @@ impl Default for TxUpdate { } impl TxUpdate { + /// Transforms the [`TxUpdate`] to have `anchors` (`A`) of another type (`A2`). + /// + /// This takes in a closure with signature `FnMut(A) -> A2` which is called for each anchor to + /// transform it. + pub fn map_anchors A2>(self, mut map: F) -> TxUpdate { + TxUpdate { + txs: self.txs, + txouts: self.txouts, + anchors: self + .anchors + .into_iter() + .map(|(a, txid)| (map(a), txid)) + .collect(), + seen_ats: self.seen_ats, + } + } + /// Extend this update with `other`. pub fn extend(&mut self, other: TxUpdate) { self.txs.extend(other.txs); From ea6876b70eb2b1ca9ee73e5719f1a9850e11827c Mon Sep 17 00:00:00 2001 From: Wei Chen Date: Thu, 5 Sep 2024 11:50:48 +0800 Subject: [PATCH 41/77] ci: pin `tokio-util` dependency version to build with rust 1.63 --- .github/workflows/cont_integration.yml | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index b41406617..6fa15c1f3 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -48,6 +48,7 @@ jobs: cargo update -p url --precise "2.5.0" cargo update -p cc --precise "1.0.105" cargo update -p tokio --precise "1.38.1" + cargo update -p tokio-util --precise "0.7.11" - name: Build run: cargo build ${{ matrix.features }} - name: Test diff --git a/README.md b/README.md index 812c0a5c4..5af2647eb 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ cargo update -p proptest --precise "1.2.0" cargo update -p url --precise "2.5.0" cargo update -p cc --precise "1.0.105" cargo update -p tokio --precise "1.38.1" +cargo update -p tokio-util --precise "0.7.11" ``` ## License From 83a0247e8ea6afd8329af5dd9b28f9892bd5e9c1 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Mon, 17 Jun 2024 20:48:03 -0500 Subject: [PATCH 42/77] feat(wallet): add transactions_sort_by function Added type WalletTx<'a> as an alias for CanonicalTx<'a, Arc, ConfirmationBlockTime>. --- crates/wallet/src/wallet/mod.rs | 48 +++++++++++++++++++++++---------- crates/wallet/tests/wallet.rs | 17 +++++++++++- 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 5cad6bb86..9f12fac4e 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -45,6 +45,7 @@ use bitcoin::{ use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt}; use bitcoin::{constants::genesis_block, Amount}; use bitcoin::{secp256k1::Secp256k1, Weight}; +use core::cmp::Ordering; use core::fmt; use core::mem; use core::ops::Deref; @@ -291,6 +292,9 @@ impl fmt::Display for ApplyBlockError { #[cfg(feature = "std")] impl std::error::Error for ApplyBlockError {} +/// A `CanonicalTx` managed by a `Wallet`. +pub type WalletTx<'a> = CanonicalTx<'a, Arc, ConfirmationBlockTime>; + impl Wallet { /// Build a new single descriptor [`Wallet`]. /// @@ -1002,9 +1006,9 @@ impl Wallet { self.indexed_graph.index.sent_and_received(tx, ..) } - /// Get a single transaction from the wallet as a [`CanonicalTx`] (if the transaction exists). + /// Get a single transaction from the wallet as a [`WalletTx`] (if the transaction exists). /// - /// `CanonicalTx` contains the full transaction alongside meta-data such as: + /// `WalletTx` contains the full transaction alongside meta-data such as: /// * Blocks that the transaction is [`Anchor`]ed in. These may or may not be blocks that exist /// in the best chain. /// * The [`ChainPosition`] of the transaction in the best chain - whether the transaction is @@ -1018,13 +1022,13 @@ impl Wallet { /// # let wallet: Wallet = todo!(); /// # let my_txid: bitcoin::Txid = todo!(); /// - /// let canonical_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist"); + /// let wallet_tx = wallet.get_tx(my_txid).expect("panic if tx does not exist"); /// /// // get reference to full transaction - /// println!("my tx: {:#?}", canonical_tx.tx_node.tx); + /// println!("my tx: {:#?}", wallet_tx.tx_node.tx); /// /// // list all transaction anchors - /// for anchor in canonical_tx.tx_node.anchors { + /// for anchor in wallet_tx.tx_node.anchors { /// println!( /// "tx is anchored by block of hash {}", /// anchor.anchor_block().hash @@ -1032,7 +1036,7 @@ impl Wallet { /// } /// /// // get confirmation status of transaction - /// match canonical_tx.chain_position { + /// match wallet_tx.chain_position { /// ChainPosition::Confirmed(anchor) => println!( /// "tx is confirmed at height {}, we know this since {}:{} is in the best chain", /// anchor.block_id.height, anchor.block_id.height, anchor.block_id.hash, @@ -1045,13 +1049,10 @@ impl Wallet { /// ``` /// /// [`Anchor`]: bdk_chain::Anchor - pub fn get_tx( - &self, - txid: Txid, - ) -> Option, ConfirmationBlockTime>> { + pub fn get_tx(&self, txid: Txid) -> Option { let graph = self.indexed_graph.graph(); - Some(CanonicalTx { + Some(WalletTx { chain_position: graph.get_chain_position( &self.chain, self.chain.tip().block_id(), @@ -1102,14 +1103,33 @@ impl Wallet { } /// Iterate over the transactions in the wallet. - pub fn transactions( - &self, - ) -> impl Iterator, ConfirmationBlockTime>> + '_ { + pub fn transactions(&self) -> impl Iterator + '_ { self.indexed_graph .graph() .list_canonical_txs(&self.chain, self.chain.tip().block_id()) } + /// Array of transactions in the wallet sorted with a comparator function. + /// + /// # Example + /// + /// ```rust,no_run + /// # use bdk_wallet::{LoadParams, Wallet, WalletTx}; + /// # let mut wallet:Wallet = todo!(); + /// // Transactions by chain position: first unconfirmed then descending by confirmed height. + /// let sorted_txs: Vec = + /// wallet.transactions_sort_by(|tx1, tx2| tx2.chain_position.cmp(&tx1.chain_position)); + /// # Ok::<(), anyhow::Error>(()) + /// ``` + pub fn transactions_sort_by(&self, compare: F) -> Vec + where + F: FnMut(&WalletTx, &WalletTx) -> Ordering, + { + let mut txs: Vec = self.transactions().collect(); + txs.sort_unstable_by(compare); + txs + } + /// Return the balance, separated into available, trusted-pending, untrusted-pending and immature /// values. pub fn balance(&self) -> Balance { diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 2da25cd28..21f391ca6 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -13,7 +13,7 @@ use bdk_wallet::error::CreateTxError; use bdk_wallet::psbt::PsbtUtils; use bdk_wallet::signer::{SignOptions, SignerError}; use bdk_wallet::tx_builder::AddForeignUtxoError; -use bdk_wallet::{AddressInfo, Balance, ChangeSet, Wallet, WalletPersister}; +use bdk_wallet::{AddressInfo, Balance, ChangeSet, Wallet, WalletPersister, WalletTx}; use bdk_wallet::{KeychainKind, LoadError, LoadMismatch, LoadWithPersistError}; use bitcoin::constants::ChainHash; use bitcoin::hashes::Hash; @@ -4203,3 +4203,18 @@ fn single_descriptor_wallet_can_create_tx_and_receive_change() { "tx change should go to external keychain" ); } + +#[test] +fn test_transactions_sort_by() { + let (mut wallet, _txid) = get_funded_wallet_wpkh(); + receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0)); + + // sort by chain position, unconfirmed then confirmed by descending block height + let sorted_txs: Vec = + wallet.transactions_sort_by(|t1, t2| t2.chain_position.cmp(&t1.chain_position)); + let conf_heights: Vec> = sorted_txs + .iter() + .map(|tx| tx.chain_position.confirmation_height_upper_bound()) + .collect(); + assert_eq!([None, Some(2000), Some(1000)], conf_heights.as_slice()); +} From 292ec3cb3a41d7b9f42cfea536c39b27dba16d4c Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 7 Sep 2024 19:21:04 -0400 Subject: [PATCH 43/77] refactor(wallet): use `Amount` everywhere --- crates/wallet/src/wallet/mod.rs | 26 +++++++++---------- crates/wallet/src/wallet/tx_builder.rs | 17 +++++------- crates/wallet/src/wallet/utils.rs | 10 +++++-- example-crates/wallet_electrum/src/main.rs | 14 ++++------ .../wallet_esplora_async/src/main.rs | 6 ++--- .../wallet_esplora_blocking/src/main.rs | 10 +++---- example-crates/wallet_rpc/src/main.rs | 4 +-- 7 files changed, 41 insertions(+), 46 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 9f12fac4e..318d75ed1 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -1405,7 +1405,7 @@ impl Wallet { if let Some(previous_fee) = params.bumping_fee { if fee < previous_fee.absolute { return Err(CreateTxError::FeeTooLow { - required: Amount::from_sat(previous_fee.absolute), + required: previous_fee.absolute, }); } } @@ -1423,7 +1423,7 @@ impl Wallet { }); } } - (rate, 0) + (rate, Amount::ZERO) } }; @@ -1449,20 +1449,20 @@ impl Wallet { } if self.is_mine(script_pubkey.clone()) { - received += Amount::from_sat(value); + received += value; } let new_out = TxOut { script_pubkey: script_pubkey.clone(), - value: Amount::from_sat(value), + value, }; tx.output.push(new_out); - outgoing += Amount::from_sat(value); + outgoing += value; } - fee_amount += (fee_rate * tx.weight()).to_sat(); + fee_amount += fee_rate * tx.weight(); let (required_utxos, optional_utxos) = self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32())); @@ -1490,7 +1490,7 @@ impl Wallet { required_utxos.clone(), optional_utxos.clone(), fee_rate, - outgoing.to_sat() + fee_amount, + outgoing.to_sat() + fee_amount.to_sat(), &drain_script, ) { Ok(res) => res, @@ -1503,7 +1503,7 @@ impl Wallet { coin_selection::single_random_draw( required_utxos, optional_utxos, - outgoing.to_sat() + fee_amount, + outgoing.to_sat() + fee_amount.to_sat(), &drain_script, fee_rate, rng, @@ -1511,7 +1511,7 @@ impl Wallet { } }, }; - fee_amount += coin_selection.fee_amount; + fee_amount += Amount::from_sat(coin_selection.fee_amount); let excess = &coin_selection.excess; tx.input = coin_selection @@ -1553,12 +1553,12 @@ impl Wallet { match excess { NoChange { remaining_amount, .. - } => fee_amount += remaining_amount, + } => fee_amount += Amount::from_sat(*remaining_amount), Change { amount, fee } => { if self.is_mine(drain_script.clone()) { received += Amount::from_sat(*amount); } - fee_amount += fee; + fee_amount += Amount::from_sat(*fee); // create drain output let drain_output = TxOut { @@ -1740,11 +1740,11 @@ impl Wallet { recipients: tx .output .into_iter() - .map(|txout| (txout.script_pubkey, txout.value.to_sat())) + .map(|txout| (txout.script_pubkey, txout.value)) .collect(), utxos: original_utxos, bumping_fee: Some(tx_builder::PreviousFee { - absolute: fee.to_sat(), + absolute: fee, rate: fee_rate, }), ..Default::default() diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs index 9d0adc729..08b0f3249 100644 --- a/crates/wallet/src/wallet/tx_builder.rs +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -122,7 +122,7 @@ pub struct TxBuilder<'a, Cs> { //TODO: TxParams should eventually be exposed publicly. #[derive(Default, Debug, Clone)] pub(crate) struct TxParams { - pub(crate) recipients: Vec<(ScriptBuf, u64)>, + pub(crate) recipients: Vec<(ScriptBuf, Amount)>, pub(crate) drain_wallet: bool, pub(crate) drain_to: Option, pub(crate) fee_policy: Option, @@ -147,14 +147,14 @@ pub(crate) struct TxParams { #[derive(Clone, Copy, Debug)] pub(crate) struct PreviousFee { - pub absolute: u64, + pub absolute: Amount, pub rate: FeeRate, } #[derive(Debug, Clone, Copy)] pub(crate) enum FeePolicy { FeeRate(FeeRate), - FeeAmount(u64), + FeeAmount(Amount), } impl Default for FeePolicy { @@ -200,7 +200,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// overshoot it slightly since adding a change output to drain the remaining /// excess might not be viable. pub fn fee_absolute(&mut self, fee_amount: Amount) -> &mut Self { - self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount.to_sat())); + self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount)); self } @@ -601,18 +601,13 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// Replace the recipients already added with a new list pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, Amount)>) -> &mut Self { - self.params.recipients = recipients - .into_iter() - .map(|(script, amount)| (script, amount.to_sat())) - .collect(); + self.params.recipients = recipients; self } /// Add a recipient to the internal list pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: Amount) -> &mut Self { - self.params - .recipients - .push((script_pubkey, amount.to_sat())); + self.params.recipients.push((script_pubkey, amount)); self } diff --git a/crates/wallet/src/wallet/utils.rs b/crates/wallet/src/wallet/utils.rs index b3ec51cb0..bca07b48a 100644 --- a/crates/wallet/src/wallet/utils.rs +++ b/crates/wallet/src/wallet/utils.rs @@ -10,7 +10,7 @@ // licenses. use bitcoin::secp256k1::{All, Secp256k1}; -use bitcoin::{absolute, relative, Script, Sequence}; +use bitcoin::{absolute, relative, Amount, Script, Sequence}; use miniscript::{MiniscriptKey, Satisfier, ToPublicKey}; @@ -26,9 +26,15 @@ pub trait IsDust { fn is_dust(&self, script: &Script) -> bool; } +impl IsDust for Amount { + fn is_dust(&self, script: &Script) -> bool { + *self < script.minimal_non_dust() + } +} + impl IsDust for u64 { fn is_dust(&self, script: &Script) -> bool { - *self < script.minimal_non_dust().to_sat() + Amount::from_sat(*self).is_dust(script) } } diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/wallet_electrum/src/main.rs index 47cbfa15d..f3320d821 100644 --- a/example-crates/wallet_electrum/src/main.rs +++ b/example-crates/wallet_electrum/src/main.rs @@ -1,12 +1,11 @@ use bdk_wallet::file_store::Store; use bdk_wallet::Wallet; use std::io::Write; -use std::str::FromStr; use bdk_electrum::electrum_client; use bdk_electrum::BdkElectrumClient; +use bdk_wallet::bitcoin::Amount; use bdk_wallet::bitcoin::Network; -use bdk_wallet::bitcoin::{Address, Amount}; use bdk_wallet::chain::collections::HashSet; use bdk_wallet::{KeychainKind, SignOptions}; @@ -43,7 +42,7 @@ fn main() -> Result<(), anyhow::Error> { println!("Generated Address: {}", address); let balance = wallet.balance(); - println!("Wallet balance before syncing: {} sats", balance.total()); + println!("Wallet balance before syncing: {}", balance.total()); print!("Syncing..."); let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?); @@ -72,22 +71,19 @@ fn main() -> Result<(), anyhow::Error> { wallet.persist(&mut db)?; let balance = wallet.balance(); - println!("Wallet balance after syncing: {} sats", balance.total()); + println!("Wallet balance after syncing: {}", balance.total()); if balance.total() < SEND_AMOUNT { println!( - "Please send at least {} sats to the receiving address", + "Please send at least {} to the receiving address", SEND_AMOUNT ); std::process::exit(0); } - let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")? - .require_network(Network::Testnet)?; - let mut tx_builder = wallet.build_tx(); tx_builder - .add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT) + .add_recipient(address.script_pubkey(), SEND_AMOUNT) .enable_rbf(); let mut psbt = tx_builder.finish()?; diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/wallet_esplora_async/src/main.rs index 6fd215dff..4133982c6 100644 --- a/example-crates/wallet_esplora_async/src/main.rs +++ b/example-crates/wallet_esplora_async/src/main.rs @@ -40,7 +40,7 @@ async fn main() -> Result<(), anyhow::Error> { println!("Next unused address: ({}) {}", address.index, address); let balance = wallet.balance(); - println!("Wallet balance before syncing: {} sats", balance.total()); + println!("Wallet balance before syncing: {}", balance.total()); print!("Syncing..."); let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?; @@ -66,11 +66,11 @@ async fn main() -> Result<(), anyhow::Error> { println!(); let balance = wallet.balance(); - println!("Wallet balance after syncing: {} sats", balance.total()); + println!("Wallet balance after syncing: {}", balance.total()); if balance.total() < SEND_AMOUNT { println!( - "Please send at least {} sats to the receiving address", + "Please send at least {} to the receiving address", SEND_AMOUNT ); std::process::exit(0); diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/wallet_esplora_blocking/src/main.rs index 45e4685b7..d12dbd926 100644 --- a/example-crates/wallet_esplora_blocking/src/main.rs +++ b/example-crates/wallet_esplora_blocking/src/main.rs @@ -42,7 +42,7 @@ fn main() -> Result<(), anyhow::Error> { ); let balance = wallet.balance(); - println!("Wallet balance before syncing: {} sats", balance.total()); + println!("Wallet balance before syncing: {}", balance.total()); print!("Syncing..."); let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking(); @@ -62,17 +62,15 @@ fn main() -> Result<(), anyhow::Error> { let update = client.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)?; wallet.apply_update(update)?; - if let Some(changeset) = wallet.take_staged() { - db.append_changeset(&changeset)?; - } + wallet.persist(&mut db)?; println!(); let balance = wallet.balance(); - println!("Wallet balance after syncing: {} sats", balance.total()); + println!("Wallet balance after syncing: {}", balance.total()); if balance.total() < SEND_AMOUNT { println!( - "Please send at least {} sats to the receiving address", + "Please send at least {} to the receiving address", SEND_AMOUNT ); std::process::exit(0); diff --git a/example-crates/wallet_rpc/src/main.rs b/example-crates/wallet_rpc/src/main.rs index 388ccaf67..d12573c13 100644 --- a/example-crates/wallet_rpc/src/main.rs +++ b/example-crates/wallet_rpc/src/main.rs @@ -106,7 +106,7 @@ fn main() -> anyhow::Result<()> { ); let balance = wallet.balance(); - println!("Wallet balance before syncing: {} sats", balance.total()); + println!("Wallet balance before syncing: {}", balance.total()); let wallet_tip = wallet.latest_checkpoint(); println!( @@ -179,7 +179,7 @@ fn main() -> anyhow::Result<()> { wallet_tip_end.height(), wallet_tip_end.hash() ); - println!("Wallet balance is {} sats", balance.total()); + println!("Wallet balance is {}", balance.total()); println!( "Wallet has {} transactions and {} utxos", wallet.transactions().count(), From 028f687b21872b66a2907701e5a8e143d0f867a0 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Fri, 6 Sep 2024 14:24:00 -0400 Subject: [PATCH 44/77] doc(wallet): Add docs to explain the lookahead --- crates/wallet/src/wallet/mod.rs | 8 ++------ crates/wallet/src/wallet/params.rs | 14 ++++++++++++-- crates/wallet/src/wallet/signer.rs | 2 +- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 9f12fac4e..347c9217a 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -313,7 +313,7 @@ impl Wallet { /// Additionally because this wallet has no internal (change) keychain, all methods that /// require a [`KeychainKind`] as input, e.g. [`reveal_next_address`] should only be called /// using the [`External`] variant. In most cases passing [`Internal`] is treated as the - /// equivalent of [`External`] but can lead to confusing results. + /// equivalent of [`External`] but this behavior must not be relied on. /// /// # Example /// @@ -1070,8 +1070,6 @@ impl Wallet { /// **WARNING**: You must persist the changes resulting from one or more calls to this method /// if you need the inserted checkpoint data to be reloaded after closing the wallet. /// See [`Wallet::reveal_next_address`]. - /// - /// [`commit`]: Self::commit pub fn insert_checkpoint( &mut self, block_id: BlockId, @@ -2294,9 +2292,7 @@ impl Wallet { /// transactions related to your wallet into it. /// /// After applying updates you should persist the staged wallet changes. For an example of how - /// to persist staged wallet changes see [`Wallet::reveal_next_address`]. ` - /// - /// [`commit`]: Self::commit + /// to persist staged wallet changes see [`Wallet::reveal_next_address`]. #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn apply_update(&mut self, update: impl Into) -> Result<(), CannotConnectError> { diff --git a/crates/wallet/src/wallet/params.rs b/crates/wallet/src/wallet/params.rs index 7f4c8a36c..7cf3bdd27 100644 --- a/crates/wallet/src/wallet/params.rs +++ b/crates/wallet/src/wallet/params.rs @@ -107,7 +107,12 @@ impl CreateParams { self } - /// Use custom lookahead value. + /// Use a custom `lookahead` value. + /// + /// The `lookahead` defines a number of script pubkeys to derive over and above the last + /// revealed index. Without a lookahead the indexer will miss outputs you own when processing + /// transactions whose output script pubkeys lie beyond the last revealed index. In most cases + /// the default value [`DEFAULT_LOOKAHEAD`] is sufficient. pub fn lookahead(mut self, lookahead: u32) -> Self { self.lookahead = lookahead; self @@ -211,7 +216,12 @@ impl LoadParams { self } - /// Use custom lookahead value. + /// Use a custom `lookahead` value. + /// + /// The `lookahead` defines a number of script pubkeys to derive over and above the last + /// revealed index. Without a lookahead the indexer will miss outputs you own when processing + /// transactions whose output script pubkeys lie beyond the last revealed index. In most cases + /// the default value [`DEFAULT_LOOKAHEAD`] is sufficient. pub fn lookahead(mut self, lookahead: u32) -> Self { self.lookahead = lookahead; self diff --git a/crates/wallet/src/wallet/signer.rs b/crates/wallet/src/wallet/signer.rs index 946ac20de..e08a43418 100644 --- a/crates/wallet/src/wallet/signer.rs +++ b/crates/wallet/src/wallet/signer.rs @@ -751,7 +751,7 @@ pub struct SignOptions { /// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been /// provided /// - /// Defaults to `false` to mitigate the "SegWit bug" which should trick the wallet into + /// Defaults to `false` to mitigate the "SegWit bug" which could trick the wallet into /// paying a fee larger than expected. /// /// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for From 5aecf4d6c48b75e610bbb9e31ad552878d797b5c Mon Sep 17 00:00:00 2001 From: valued mammal Date: Sat, 31 Aug 2024 13:02:25 -0400 Subject: [PATCH 45/77] fix(wallet): do `check_wallet_descriptor` when creating and loading --- crates/wallet/src/wallet/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 9f12fac4e..306ca60f1 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -390,6 +390,7 @@ impl Wallet { let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash); let (descriptor, mut descriptor_keymap) = (params.descriptor)(&secp, network)?; + check_wallet_descriptor(&descriptor)?; descriptor_keymap.extend(params.descriptor_keymap); let signers = Arc::new(SignersContainer::build( @@ -401,6 +402,7 @@ impl Wallet { let (change_descriptor, change_signers) = match params.change_descriptor { Some(make_desc) => { let (change_descriptor, mut internal_keymap) = make_desc(&secp, network)?; + check_wallet_descriptor(&change_descriptor)?; internal_keymap.extend(params.change_descriptor_keymap); let change_signers = Arc::new(SignersContainer::build( internal_keymap, @@ -582,6 +584,7 @@ impl Wallet { } // parameters must match Some(make_desc) => { + check_wallet_descriptor(&desc).map_err(LoadError::Descriptor)?; let (exp_desc, keymap) = make_desc(&secp, network).map_err(LoadError::Descriptor)?; if desc.descriptor_id() != exp_desc.descriptor_id() { From b60d1d29cb8908c354b43c49237acbea373c3dc7 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Tue, 27 Aug 2024 11:54:25 -0400 Subject: [PATCH 46/77] fix(wallet): only mark change address used if `create_tx` succeeds If no drain script is specified in tx params then we get it from the change keychain by looking at the next unused address. We want to mark the change address used so that other callers won't attempt to use the same address between the time we create the tx and when it appears on chain. Before, we marked the index used regardless of whether a change output is finally added. Then if creating a PSBT failed, we never restored the unused status of the change address, so creating the next tx would have revealed an extra one. Now we only mark the change address used if we successfully create a PSBT and the drain script is used in the change output. --- crates/wallet/src/wallet/mod.rs | 35 +++++++++++++++++++++++++++------ 1 file changed, 29 insertions(+), 6 deletions(-) diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 9f12fac4e..25803f139 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -88,7 +88,7 @@ use crate::descriptor::{ use crate::psbt::PsbtUtils; use crate::signer::SignerError; use crate::types::*; -use crate::wallet::coin_selection::Excess::{Change, NoChange}; +use crate::wallet::coin_selection::Excess::{self, Change, NoChange}; use crate::wallet::error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError}; use self::coin_selection::Error; @@ -1468,17 +1468,28 @@ impl Wallet { self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32())); // get drain script + let mut drain_index = Option::<(KeychainKind, u32)>::None; let drain_script = match params.drain_to { Some(ref drain_recipient) => drain_recipient.clone(), None => { let change_keychain = self.map_keychain(KeychainKind::Internal); - let ((index, spk), index_changeset) = self + let (index, spk) = self .indexed_graph .index - .next_unused_spk(change_keychain) - .expect("keychain must exist"); - self.indexed_graph.index.mark_used(change_keychain, index); - self.stage.merge(index_changeset.into()); + .unused_keychain_spks(change_keychain) + .next() + .unwrap_or_else(|| { + let (next_index, _) = self + .indexed_graph + .index + .next_index(change_keychain) + .expect("keychain must exist"); + let spk = self + .peek_address(change_keychain, next_index) + .script_pubkey(); + (next_index, spk) + }); + drain_index = Some((change_keychain, index)); spk } }; @@ -1577,6 +1588,18 @@ impl Wallet { params.ordering.sort_tx_with_aux_rand(&mut tx, rng); let psbt = self.complete_transaction(tx, coin_selection.selected, params)?; + + // recording changes to the change keychain + if let (Excess::Change { .. }, Some((keychain, index))) = (excess, drain_index) { + let (_, index_changeset) = self + .indexed_graph + .index + .reveal_to_target(keychain, index) + .expect("must not be None"); + self.stage.merge(index_changeset.into()); + self.mark_used(keychain, index); + } + Ok(psbt) } From 75989d8cde3902f226bfa89aae05803b93a7cf1d Mon Sep 17 00:00:00 2001 From: valued mammal Date: Tue, 27 Aug 2024 12:24:41 -0400 Subject: [PATCH 47/77] test(wallet): Add `test_create_tx_increment_change_index` --- crates/wallet/tests/wallet.rs | 121 ++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 21f391ca6..2a2a68fa6 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -1392,6 +1392,127 @@ fn test_create_tx_global_xpubs_with_origin() { assert_eq!(psbt.xpub.get(&key), Some(&(fingerprint, path))); } +#[test] +fn test_create_tx_increment_change_index() { + // Test derivation index and unused index of change keychain when creating a transaction + // Cases include wildcard and non-wildcard descriptors with and without an internal keychain + // note the test assumes that the first external address is revealed since we're using + // `receive_output` + struct TestCase { + name: &'static str, + descriptor: &'static str, + change_descriptor: Option<&'static str>, + // amount to send + to_send: u64, + // (derivation index, next unused index) of *change keychain* + expect: (Option, u32), + } + // total wallet funds + let amount = 10_000; + let recipient = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5") + .unwrap() + .assume_checked() + .script_pubkey(); + let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc(); + [ + TestCase { + name: "two wildcard, builder error", + descriptor: desc, + change_descriptor: Some(change_desc), + to_send: amount + 1, + // should not use or derive change index + expect: (None, 0), + }, + TestCase { + name: "two wildcard, create change", + descriptor: desc, + change_descriptor: Some(change_desc), + to_send: 5_000, + // should use change index + expect: (Some(0), 1), + }, + TestCase { + name: "two wildcard, no change", + descriptor: desc, + change_descriptor: Some(change_desc), + to_send: 9_850, + // should not use change index + expect: (None, 0), + }, + TestCase { + name: "one wildcard, create change", + descriptor: desc, + change_descriptor: None, + to_send: 5_000, + // should use change index of external keychain + expect: (Some(1), 2), + }, + TestCase { + name: "one wildcard, no change", + descriptor: desc, + change_descriptor: None, + to_send: 9_850, + // should not use change index + expect: (Some(0), 1), + }, + TestCase { + name: "single key, create change", + descriptor: get_test_tr_single_sig(), + change_descriptor: None, + to_send: 5_000, + // single key only has one derivation index (0) + expect: (Some(0), 0), + }, + TestCase { + name: "single key, no change", + descriptor: get_test_tr_single_sig(), + change_descriptor: None, + to_send: 9_850, + expect: (Some(0), 0), + }, + ] + .into_iter() + .for_each(|test| { + // create wallet + let (params, change_keychain) = match test.change_descriptor { + Some(change_desc) => ( + Wallet::create(test.descriptor, change_desc), + KeychainKind::Internal, + ), + None => ( + Wallet::create_single(test.descriptor), + KeychainKind::External, + ), + }; + let mut wallet = params + .network(Network::Regtest) + .create_wallet_no_persist() + .unwrap(); + // fund wallet + receive_output(&mut wallet, amount, ConfirmationTime::unconfirmed(0)); + // create tx + let mut builder = wallet.build_tx(); + builder.add_recipient(recipient.clone(), Amount::from_sat(test.to_send)); + let res = builder.finish(); + if !test.name.contains("error") { + assert!(res.is_ok()); + } + let (exp_derivation_index, exp_next_unused) = test.expect; + assert_eq!( + wallet.derivation_index(change_keychain), + exp_derivation_index, + "derivation index test {}", + test.name, + ); + assert_eq!( + wallet.next_unused_address(change_keychain).index, + exp_next_unused, + "next unused index test {}", + test.name, + ); + }); +} + #[test] fn test_add_foreign_utxo() { let (mut wallet1, _) = get_funded_wallet_wpkh(); From 606fa0874db0f10cd1c64de0f1f097b12db3a16d Mon Sep 17 00:00:00 2001 From: valued mammal Date: Mon, 9 Sep 2024 11:51:11 -0400 Subject: [PATCH 48/77] ci: bump actions/upload-artifact to v4 --- .github/workflows/code_coverage.yml | 2 +- .github/workflows/nightly_docs.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index 54020bb90..5a91de04e 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -51,7 +51,7 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: coverage-report path: coverage-report.html diff --git a/.github/workflows/nightly_docs.yml b/.github/workflows/nightly_docs.yml index 5fa4ecb5d..59b19a591 100644 --- a/.github/workflows/nightly_docs.yml +++ b/.github/workflows/nightly_docs.yml @@ -22,7 +22,7 @@ jobs: env: RUSTDOCFLAGS: '--cfg docsrs -Dwarnings' - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: built-docs path: ./target/doc/* @@ -44,7 +44,7 @@ jobs: - name: Remove old latest run: rm -rf ./docs/.vuepress/public/docs-rs/bdk/nightly/latest - name: Download built docs - uses: actions/download-artifact@v1 + uses: actions/download-artifact@v4 with: name: built-docs path: ./docs/.vuepress/public/docs-rs/bdk/nightly/latest From bc83e41126b75c32374a4fa6ba054c114ee62704 Mon Sep 17 00:00:00 2001 From: Jose Storopoli Date: Tue, 10 Sep 2024 09:51:36 +0000 Subject: [PATCH 49/77] fix: typos --- crates/core/src/checkpoint.rs | 2 +- crates/core/src/tx_update.rs | 2 +- crates/wallet/src/wallet/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/core/src/checkpoint.rs b/crates/core/src/checkpoint.rs index 0abadda1d..f78aca9c5 100644 --- a/crates/core/src/checkpoint.rs +++ b/crates/core/src/checkpoint.rs @@ -169,7 +169,7 @@ impl CheckPoint { /// The effect of `insert` depends on whether a height already exists. If it doesn't the /// `block_id` we inserted and all pre-existing blocks higher than it will be re-inserted after /// it. If the height already existed and has a conflicting block hash then it will be purged - /// along with all block followin it. The returned chain will have a tip of the `block_id` + /// along with all block following it. The returned chain will have a tip of the `block_id` /// passed in. Of course, if the `block_id` was already present then this just returns `self`. #[must_use] pub fn insert(self, block_id: BlockId) -> Self { diff --git a/crates/core/src/tx_update.rs b/crates/core/src/tx_update.rs index 29d6c2530..7707578ee 100644 --- a/crates/core/src/tx_update.rs +++ b/crates/core/src/tx_update.rs @@ -2,7 +2,7 @@ use crate::collections::{BTreeMap, BTreeSet, HashMap}; use alloc::{sync::Arc, vec::Vec}; use bitcoin::{OutPoint, Transaction, TxOut, Txid}; -/// Data object used to communicate updates about relevant transactions from some chain data soruce +/// Data object used to communicate updates about relevant transactions from some chain data source /// to the core model (usually a `bdk_chain::TxGraph`). #[derive(Debug, Clone)] pub struct TxUpdate { diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index a70a70b9a..1319b8f05 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -2377,7 +2377,7 @@ impl Wallet { } } - /// Get a mutable reference of the staged [`ChangeSet`] that is yet to be commited (if any). + /// Get a mutable reference of the staged [`ChangeSet`] that is yet to be committed (if any). pub fn staged_mut(&mut self) -> Option<&mut ChangeSet> { if self.stage.is_empty() { None From c18204d05925c4398e139c6e62c9640dacb67e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Wed, 28 Aug 2024 09:49:55 +0000 Subject: [PATCH 50/77] feat(wallet)!: allow custom fallback algorithm for bnb Signature of `CoinSelectionAlgorithm::coin_select` has been changed to take in a `&mut RangCore`. This allows us to pass the random number generator directly to the cs algorithm. Single random draw is now it's own type `SingleRandomDraw` and impls `CoinSelectionAlgorithm`. `BranchAndBoundCoinSelection` now handles it's own fallback algorithm internally, and a generic type parameter is added to specify the fallback algorithm. `coin_selection::Error` is renamed to `InsufficientFunds` and the BnB error variants are removed. The BnB error variants are no longer needed since those cases are handled internally by `BranchAndBoundCoinSelection` (via calling the fallback algorithm). Add test_bnb_fallback_algorithm test and docs cleanup suggested by @ValuedMammal. --- crates/wallet/src/wallet/coin_selection.rs | 338 +++++++++++++-------- crates/wallet/src/wallet/error.rs | 6 +- crates/wallet/src/wallet/mod.rs | 43 +-- crates/wallet/tests/wallet.rs | 6 +- 4 files changed, 230 insertions(+), 163 deletions(-) diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 8dc36b198..306124016 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -31,18 +31,20 @@ //! # use bdk_wallet::*; //! # use bdk_wallet::coin_selection::decide_change; //! # use anyhow::Error; +//! # use rand_core::RngCore; //! #[derive(Debug)] //! struct AlwaysSpendEverything; //! //! impl CoinSelectionAlgorithm for AlwaysSpendEverything { -//! fn coin_select( +//! fn coin_select( //! &self, //! required_utxos: Vec, //! optional_utxos: Vec, //! fee_rate: FeeRate, //! target_amount: u64, //! drain_script: &Script, -//! ) -> Result { +//! rand: &mut R, +//! ) -> Result { //! let mut selected_amount = 0; //! let mut additional_weight = Weight::ZERO; //! let all_utxos_selected = required_utxos @@ -63,7 +65,7 @@ //! let additional_fees = (fee_rate * additional_weight).to_sat(); //! let amount_needed_with_fees = additional_fees + target_amount; //! if selected_amount < amount_needed_with_fees { -//! return Err(coin_selection::Error::InsufficientFunds { +//! return Err(coin_selection::InsufficientFunds { //! needed: amount_needed_with_fees, //! available: selected_amount, //! }); @@ -118,44 +120,31 @@ use rand_core::RngCore; use super::utils::shuffle_slice; /// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not /// overridden -pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; +pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection; -/// Errors that can be thrown by the [`coin_selection`](crate::wallet::coin_selection) module -#[derive(Debug)] -pub enum Error { - /// Wallet's UTXO set is not enough to cover recipient's requested plus fee - InsufficientFunds { - /// Sats needed for some transaction - needed: u64, - /// Sats available for spending - available: u64, - }, - /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for - /// the desired outputs plus fee, if there is not such combination this error is thrown - BnBNoExactMatch, - /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow - /// exponentially, thus a limit is set, and when hit, this error is thrown - BnBTotalTriesExceeded, +/// Wallet's UTXO set is not enough to cover recipient's requested plus fee. +/// +/// This is thrown by [`CoinSelectionAlgorithm`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InsufficientFunds { + /// Sats needed for some transaction + pub needed: u64, + /// Sats available for spending + pub available: u64, } -impl fmt::Display for Error { +impl fmt::Display for InsufficientFunds { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match self { - Self::InsufficientFunds { needed, available } => write!( - f, - "Insufficient funds: {} sat available of {} sat needed", - available, needed - ), - Self::BnBTotalTriesExceeded => { - write!(f, "Branch and bound coin selection: total tries exceeded") - } - Self::BnBNoExactMatch => write!(f, "Branch and bound coin selection: not exact match"), - } + write!( + f, + "Insufficient funds: {} sat available of {} sat needed", + self.available, self.needed + ) } } #[cfg(feature = "std")] -impl std::error::Error for Error {} +impl std::error::Error for InsufficientFunds {} #[derive(Debug)] /// Remaining amount after performing coin selection @@ -216,8 +205,6 @@ impl CoinSelectionResult { pub trait CoinSelectionAlgorithm: core::fmt::Debug { /// Perform the coin selection /// - /// - `database`: a reference to the wallet's database that can be used to lookup additional - /// details for a specific UTXO /// - `required_utxos`: the utxos that must be spent regardless of `target_amount` with their /// weight cost /// - `optional_utxos`: the remaining available utxos to satisfy `target_amount` with their @@ -226,15 +213,17 @@ pub trait CoinSelectionAlgorithm: core::fmt::Debug { /// - `target_amount`: the outgoing amount in satoshis and the fees already /// accumulated from added outputs and transaction’s header. /// - `drain_script`: the script to use in case of change + /// - `rand`: random number generated used by some coin selection algorithms such as [`SingleRandomDraw`] #[allow(clippy::too_many_arguments)] - fn coin_select( + fn coin_select( &self, required_utxos: Vec, optional_utxos: Vec, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, - ) -> Result; + rand: &mut R, + ) -> Result; } /// Simple and dumb coin selection @@ -245,14 +234,15 @@ pub trait CoinSelectionAlgorithm: core::fmt::Debug { pub struct LargestFirstCoinSelection; impl CoinSelectionAlgorithm for LargestFirstCoinSelection { - fn coin_select( + fn coin_select( &self, required_utxos: Vec, mut optional_utxos: Vec, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, - ) -> Result { + _: &mut R, + ) -> Result { // We put the "required UTXOs" first and make sure the optional UTXOs are sorted, // initially smallest to largest, before being reversed with `.rev()`. let utxos = { @@ -275,14 +265,15 @@ impl CoinSelectionAlgorithm for LargestFirstCoinSelection { pub struct OldestFirstCoinSelection; impl CoinSelectionAlgorithm for OldestFirstCoinSelection { - fn coin_select( + fn coin_select( &self, required_utxos: Vec, mut optional_utxos: Vec, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, - ) -> Result { + _: &mut R, + ) -> Result { // We put the "required UTXOs" first and make sure the optional UTXOs are sorted from // oldest to newest according to blocktime // For utxo that doesn't exist in DB, they will have lowest priority to be selected @@ -334,7 +325,7 @@ fn select_sorted_utxos( fee_rate: FeeRate, target_amount: u64, drain_script: &Script, -) -> Result { +) -> Result { let mut selected_amount = 0; let mut fee_amount = 0; let selected = utxos @@ -359,7 +350,7 @@ fn select_sorted_utxos( let amount_needed_with_fees = target_amount + fee_amount; if selected_amount < amount_needed_with_fees { - return Err(Error::InsufficientFunds { + return Err(InsufficientFunds { needed: amount_needed_with_fees, available: selected_amount, }); @@ -407,56 +398,73 @@ impl OutputGroup { /// /// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: #[derive(Debug, Clone)] -pub struct BranchAndBoundCoinSelection { +pub struct BranchAndBoundCoinSelection { size_of_change: u64, + fallback_algorithm: FA, +} + +/// Error returned by branch and bond coin selection. +#[derive(Debug)] +enum BnBError { + /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for + /// the desired outputs plus fee, if there is not such combination this error is thrown + NoExactMatch, + /// Branch and bound coin selection possible attempts with sufficiently big UTXO set could grow + /// exponentially, thus a limit is set, and when hit, this error is thrown + TotalTriesExceeded, } -impl Default for BranchAndBoundCoinSelection { +impl Default for BranchAndBoundCoinSelection { fn default() -> Self { Self { // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes) size_of_change: 8 + 1 + 22, + fallback_algorithm: FA::default(), } } } -impl BranchAndBoundCoinSelection { - /// Create new instance with target size for change output - pub fn new(size_of_change: u64) -> Self { - Self { size_of_change } +impl BranchAndBoundCoinSelection { + /// Create new instance with a target `size_of_change` and `fallback_algorithm`. + pub fn new(size_of_change: u64, fallback_algorithm: FA) -> Self { + Self { + size_of_change, + fallback_algorithm, + } } } const BNB_TOTAL_TRIES: usize = 100_000; -impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { - fn coin_select( +impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { + fn coin_select( &self, required_utxos: Vec, optional_utxos: Vec, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, - ) -> Result { + rand: &mut R, + ) -> Result { // Mapping every (UTXO, usize) to an output group - let required_utxos: Vec = required_utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) + let required_ogs: Vec = required_utxos + .iter() + .map(|u| OutputGroup::new(u.clone(), fee_rate)) .collect(); // Mapping every (UTXO, usize) to an output group, filtering UTXOs with a negative // effective value - let optional_utxos: Vec = optional_utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) + let optional_ogs: Vec = optional_utxos + .iter() + .map(|u| OutputGroup::new(u.clone(), fee_rate)) .filter(|u| u.effective_value.is_positive()) .collect(); - let curr_value = required_utxos + let curr_value = required_ogs .iter() .fold(0, |acc, x| acc + x.effective_value); - let curr_available_value = optional_utxos + let curr_available_value = optional_ogs .iter() .fold(0, |acc, x| acc + x.effective_value); @@ -480,57 +488,63 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { _ => { // Assume we spend all the UTXOs we can (all the required + all the optional with // positive effective value), sum their value and their fee cost. - let (utxo_fees, utxo_value) = required_utxos - .iter() - .chain(optional_utxos.iter()) - .fold((0, 0), |(mut fees, mut value), utxo| { + let (utxo_fees, utxo_value) = required_ogs.iter().chain(optional_ogs.iter()).fold( + (0, 0), + |(mut fees, mut value), utxo| { fees += utxo.fee; value += utxo.weighted_utxo.utxo.txout().value.to_sat(); (fees, value) - }); + }, + ); // Add to the target the fee cost of the UTXOs - return Err(Error::InsufficientFunds { + return Err(InsufficientFunds { needed: target_amount + utxo_fees, available: utxo_value, }); } } - let target_amount = target_amount + let signed_target_amount = target_amount .try_into() .expect("Bitcoin amount to fit into i64"); - if curr_value > target_amount { + if curr_value > signed_target_amount { // remaining_amount can't be negative as that would mean the // selection wasn't successful // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (curr_value - target_amount) as u64; + let remaining_amount = (curr_value - signed_target_amount) as u64; let excess = decide_change(remaining_amount, fee_rate, drain_script); - return Ok(BranchAndBoundCoinSelection::calculate_cs_result( - vec![], - required_utxos, - excess, - )); + return Ok(calculate_cs_result(vec![], required_ogs, excess)); } - self.bnb( - required_utxos.clone(), - optional_utxos.clone(), + match self.bnb( + required_ogs.clone(), + optional_ogs.clone(), curr_value, curr_available_value, - target_amount, + signed_target_amount, cost_of_change, drain_script, fee_rate, - ) + ) { + Ok(r) => Ok(r), + Err(_) => self.fallback_algorithm.coin_select( + required_utxos, + optional_utxos, + fee_rate, + target_amount, + drain_script, + rand, + ), + } } } -impl BranchAndBoundCoinSelection { +impl BranchAndBoundCoinSelection { // TODO: make this more Rust-onic :) // (And perhaps refactor with less arguments?) #[allow(clippy::too_many_arguments)] @@ -544,7 +558,7 @@ impl BranchAndBoundCoinSelection { cost_of_change: u64, drain_script: &Script, fee_rate: FeeRate, - ) -> Result { + ) -> Result { // current_selection[i] will contain true if we are using optional_utxos[i], // false otherwise. Note that current_selection.len() could be less than // optional_utxos.len(), it just means that we still haven't decided if we should keep @@ -600,7 +614,7 @@ impl BranchAndBoundCoinSelection { // We have walked back to the first utxo and no branch is untraversed. All solutions searched // If best selection is empty, then there's no exact match if best_selection.is_empty() { - return Err(Error::BnBNoExactMatch); + return Err(BnBError::NoExactMatch); } break; } @@ -627,7 +641,7 @@ impl BranchAndBoundCoinSelection { // Check for solution if best_selection.is_empty() { - return Err(Error::BnBTotalTriesExceeded); + return Err(BnBError::TotalTriesExceeded); } // Set output set @@ -646,30 +660,32 @@ impl BranchAndBoundCoinSelection { let excess = decide_change(remaining_amount, fee_rate, drain_script); - Ok(BranchAndBoundCoinSelection::calculate_cs_result( - selected_utxos, - required_utxos, - excess, - )) + Ok(calculate_cs_result(selected_utxos, required_utxos, excess)) } +} - fn calculate_cs_result( - mut selected_utxos: Vec, - mut required_utxos: Vec, - excess: Excess, - ) -> CoinSelectionResult { - selected_utxos.append(&mut required_utxos); - let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::(); - let selected = selected_utxos - .into_iter() - .map(|u| u.weighted_utxo.utxo) - .collect::>(); +/// Pull UTXOs at random until we have enough to meet the target. +#[derive(Debug, Clone, Copy, Default)] +pub struct SingleRandomDraw; - CoinSelectionResult { - selected, - fee_amount, - excess, - } +impl CoinSelectionAlgorithm for SingleRandomDraw { + fn coin_select( + &self, + required_utxos: Vec, + optional_utxos: Vec, + fee_rate: FeeRate, + target_amount: u64, + drain_script: &Script, + rand: &mut R, + ) -> Result { + Ok(single_random_draw( + required_utxos, + optional_utxos, + target_amount, + drain_script, + fee_rate, + rand, + )) } } @@ -722,7 +738,26 @@ pub(crate) fn single_random_draw( let excess = decide_change(remaining_amount, fee_rate, drain_script); - BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess) + calculate_cs_result(selected_utxos.1, required_utxos, excess) +} + +fn calculate_cs_result( + mut selected_utxos: Vec, + mut required_utxos: Vec, + excess: Excess, +) -> CoinSelectionResult { + selected_utxos.append(&mut required_utxos); + let fee_amount = selected_utxos.iter().map(|u| u.fee).sum::(); + let selected = selected_utxos + .into_iter() + .map(|u| u.weighted_utxo.utxo) + .collect::>(); + + CoinSelectionResult { + selected, + fee_amount, + excess, + } } /// Remove duplicate UTXOs. @@ -758,7 +793,7 @@ mod test { use crate::wallet::coin_selection::filter_duplicates; use rand::prelude::SliceRandom; - use rand::{Rng, RngCore, SeedableRng}; + use rand::{thread_rng, Rng, RngCore, SeedableRng}; // signature len (1WU) + signature and sighash (72WU) // + pubkey len (1WU) + pubkey (33WU) @@ -907,6 +942,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -928,6 +964,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -949,6 +986,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -971,6 +1009,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); } @@ -989,6 +1028,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1000), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); } @@ -1006,6 +1046,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -1027,6 +1068,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -1048,6 +1090,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -1070,6 +1113,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); } @@ -1093,6 +1137,7 @@ mod test { FeeRate::from_sat_per_vb_unchecked(1000), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); } @@ -1106,13 +1151,14 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 250_000 + FEE_AMOUNT; - let result = BranchAndBoundCoinSelection::default() + let result = BranchAndBoundCoinSelection::::default() .coin_select( vec![], utxos, FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -1127,13 +1173,14 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; - let result = BranchAndBoundCoinSelection::default() + let result = BranchAndBoundCoinSelection::::default() .coin_select( utxos.clone(), utxos, FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -1149,13 +1196,14 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 299756 + FEE_AMOUNT; - let result = BranchAndBoundCoinSelection::default() + let result = BranchAndBoundCoinSelection::::default() .coin_select( vec![], utxos, FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -1206,13 +1254,14 @@ mod test { let target_amount = 150_000 + FEE_AMOUNT; - let result = BranchAndBoundCoinSelection::default() + let result = BranchAndBoundCoinSelection::::default() .coin_select( required, optional, FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); @@ -1228,13 +1277,14 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 500_000 + FEE_AMOUNT; - BranchAndBoundCoinSelection::default() + BranchAndBoundCoinSelection::::default() .coin_select( vec![], utxos, FeeRate::from_sat_per_vb_unchecked(1), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); } @@ -1246,13 +1296,14 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 250_000 + FEE_AMOUNT; - BranchAndBoundCoinSelection::default() + BranchAndBoundCoinSelection::::default() .coin_select( vec![], utxos, FeeRate::from_sat_per_vb_unchecked(1000), target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); } @@ -1264,8 +1315,15 @@ mod test { let target_amount = 99932; // first utxo's effective value let feerate = FeeRate::BROADCAST_MIN; - let result = BranchAndBoundCoinSelection::new(0) - .coin_select(vec![], utxos, feerate, target_amount, &drain_script) + let result = BranchAndBoundCoinSelection::new(0, SingleRandomDraw) + .coin_select( + vec![], + utxos, + feerate, + target_amount, + &drain_script, + &mut thread_rng(), + ) .unwrap(); assert_eq!(result.selected.len(), 1); @@ -1286,13 +1344,14 @@ mod test { let mut optional_utxos = generate_random_utxos(&mut rng, 16); let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); let drain_script = ScriptBuf::default(); - let result = BranchAndBoundCoinSelection::new(0) + let result = BranchAndBoundCoinSelection::new(0, SingleRandomDraw) .coin_select( vec![], optional_utxos, FeeRate::ZERO, target_amount, &drain_script, + &mut thread_rng(), ) .unwrap(); assert_eq!(result.selected_amount(), target_amount); @@ -1300,7 +1359,7 @@ mod test { } #[test] - #[should_panic(expected = "BnBNoExactMatch")] + #[should_panic(expected = "NoExactMatch")] fn test_bnb_function_no_exact_match() { let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); let utxos: Vec = get_test_utxos() @@ -1315,7 +1374,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; - BranchAndBoundCoinSelection::new(size_of_change) + BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw) .bnb( vec![], utxos, @@ -1330,7 +1389,7 @@ mod test { } #[test] - #[should_panic(expected = "BnBTotalTriesExceeded")] + #[should_panic(expected = "TotalTriesExceeded")] fn test_bnb_function_tries_exceeded() { let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); let utxos: Vec = generate_same_value_utxos(100_000, 100_000) @@ -1346,7 +1405,7 @@ mod test { let drain_script = ScriptBuf::default(); - BranchAndBoundCoinSelection::new(size_of_change) + BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw) .bnb( vec![], utxos, @@ -1382,7 +1441,7 @@ mod test { let drain_script = ScriptBuf::default(); - let result = BranchAndBoundCoinSelection::new(size_of_change) + let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw) .bnb( vec![], utxos, @@ -1422,7 +1481,7 @@ mod test { let drain_script = ScriptBuf::default(); - let result = BranchAndBoundCoinSelection::new(0) + let result = BranchAndBoundCoinSelection::new(0, SingleRandomDraw) .bnb( vec![], optional_utxos, @@ -1443,17 +1502,18 @@ mod test { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); - let selection = BranchAndBoundCoinSelection::default().coin_select( + let selection = BranchAndBoundCoinSelection::::default().coin_select( vec![], utxos, FeeRate::from_sat_per_vb_unchecked(10), 500_000, &drain_script, + &mut thread_rng(), ); assert_matches!( selection, - Err(Error::InsufficientFunds { + Err(InsufficientFunds { available: 300_000, .. }) @@ -1469,17 +1529,18 @@ mod test { |u| matches!(u, WeightedUtxo { utxo, .. } if utxo.txout().value.to_sat() < 1000), ); - let selection = BranchAndBoundCoinSelection::default().coin_select( + let selection = BranchAndBoundCoinSelection::::default().coin_select( required, optional, FeeRate::from_sat_per_vb_unchecked(10), 500_000, &drain_script, + &mut thread_rng(), ); assert_matches!( selection, - Err(Error::InsufficientFunds { + Err(InsufficientFunds { available: 300_010, .. }) @@ -1491,23 +1552,46 @@ mod test { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); - let selection = BranchAndBoundCoinSelection::default().coin_select( + let selection = BranchAndBoundCoinSelection::::default().coin_select( utxos, vec![], FeeRate::from_sat_per_vb_unchecked(10_000), 500_000, &drain_script, + &mut thread_rng(), ); assert_matches!( selection, - Err(Error::InsufficientFunds { + Err(InsufficientFunds { available: 300_010, .. }) ); } + #[test] + fn test_bnb_fallback_algorithm() { + // utxo value + // 120k + 80k + 300k + let optional_utxos = get_oldest_first_test_utxos(); + let feerate = FeeRate::BROADCAST_MIN; + let target_amount = 190_000; + let drain_script = ScriptBuf::new(); + // bnb won't find exact match and should select oldest first + let res = BranchAndBoundCoinSelection::::default() + .coin_select( + vec![], + optional_utxos, + feerate, + target_amount, + &drain_script, + &mut thread_rng(), + ) + .unwrap(); + assert_eq!(res.selected_amount(), 200_000); + } + #[test] fn test_filter_duplicates() { fn utxo(txid: &str, value: u64) -> WeightedUtxo { diff --git a/crates/wallet/src/wallet/error.rs b/crates/wallet/src/wallet/error.rs index b6c9375a4..2264aac9e 100644 --- a/crates/wallet/src/wallet/error.rs +++ b/crates/wallet/src/wallet/error.rs @@ -89,7 +89,7 @@ pub enum CreateTxError { /// Output created is under the dust limit, 546 satoshis OutputBelowDustLimit(usize), /// There was an error with coin selection - CoinSelection(coin_selection::Error), + CoinSelection(coin_selection::InsufficientFunds), /// Cannot build a tx without recipients NoRecipients, /// Partially signed bitcoin transaction error @@ -204,8 +204,8 @@ impl From for CreateTxError { } } -impl From for CreateTxError { - fn from(err: coin_selection::Error) -> Self { +impl From for CreateTxError { + fn from(err: coin_selection::InsufficientFunds) -> Self { CreateTxError::CoinSelection(err) } } diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index a70a70b9a..5c908ea2a 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -75,7 +75,7 @@ pub mod error; pub use utils::IsDust; -use coin_selection::DefaultCoinSelectionAlgorithm; +use coin_selection::{DefaultCoinSelectionAlgorithm, InsufficientFunds}; use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner}; use tx_builder::{FeePolicy, TxBuilder, TxParams}; use utils::{check_nsequence_rbf, After, Older, SecpCtx}; @@ -91,8 +91,6 @@ use crate::types::*; use crate::wallet::coin_selection::Excess::{self, Change, NoChange}; use crate::wallet::error::{BuildFeeBumpError, CreateTxError, MiniscriptPsbtError}; -use self::coin_selection::Error; - const COINBASE_MATURITY: u32 = 100; /// A Bitcoin wallet @@ -1497,31 +1495,16 @@ impl Wallet { let (required_utxos, optional_utxos) = coin_selection::filter_duplicates(required_utxos, optional_utxos); - let coin_selection = match coin_selection.coin_select( - required_utxos.clone(), - optional_utxos.clone(), - fee_rate, - outgoing.to_sat() + fee_amount.to_sat(), - &drain_script, - ) { - Ok(res) => res, - Err(e) => match e { - coin_selection::Error::InsufficientFunds { .. } => { - return Err(CreateTxError::CoinSelection(e)); - } - coin_selection::Error::BnBNoExactMatch - | coin_selection::Error::BnBTotalTriesExceeded => { - coin_selection::single_random_draw( - required_utxos, - optional_utxos, - outgoing.to_sat() + fee_amount.to_sat(), - &drain_script, - fee_rate, - rng, - ) - } - }, - }; + let coin_selection = coin_selection + .coin_select( + required_utxos.clone(), + optional_utxos.clone(), + fee_rate, + outgoing.to_sat() + fee_amount.to_sat(), + &drain_script, + rng, + ) + .map_err(CreateTxError::CoinSelection)?; fee_amount += Amount::from_sat(coin_selection.fee_amount); let excess = &coin_selection.excess; @@ -1551,7 +1534,7 @@ impl Wallet { change_fee, } = excess { - return Err(CreateTxError::CoinSelection(Error::InsufficientFunds { + return Err(CreateTxError::CoinSelection(InsufficientFunds { needed: *dust_threshold, available: remaining_amount.saturating_sub(*change_fee), })); @@ -2657,7 +2640,7 @@ macro_rules! doctest_wallet { script_pubkey: address.script_pubkey(), }], }; - let txid = tx.txid(); + let txid = tx.compute_txid(); let block_id = BlockId { height: 500, hash: BlockHash::all_zeros() }; let _ = wallet.insert_checkpoint(block_id); let _ = wallet.insert_checkpoint(BlockId { height: 1_000, hash: BlockHash::all_zeros() }); diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 2a2a68fa6..637593780 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -730,7 +730,7 @@ fn test_create_tx_change_policy() { assert!(matches!( builder.finish(), Err(CreateTxError::CoinSelection( - coin_selection::Error::InsufficientFunds { .. } + coin_selection::InsufficientFunds { .. } )), )); } @@ -3943,7 +3943,7 @@ fn test_spend_coinbase() { assert!(matches!( builder.finish(), Err(CreateTxError::CoinSelection( - coin_selection::Error::InsufficientFunds { + coin_selection::InsufficientFunds { needed: _, available: 0 } @@ -3958,7 +3958,7 @@ fn test_spend_coinbase() { assert_matches!( builder.finish(), Err(CreateTxError::CoinSelection( - coin_selection::Error::InsufficientFunds { + coin_selection::InsufficientFunds { needed: _, available: 0 } From 43257cfca6c8c2a3d5dc2b78da455ff9f16da5bd Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 15 Aug 2024 21:41:29 -0300 Subject: [PATCH 51/77] refactor(wallet)!: remove dangling unused `hardwaresigner.rs` file It seems this file was left out on the previous migration from hardware signers from `bdk_wallet` crate to the `bdk_hwi`, but it should've been removed. --- crates/wallet/src/wallet/hardwaresigner.rs | 94 ---------------------- 1 file changed, 94 deletions(-) delete mode 100644 crates/wallet/src/wallet/hardwaresigner.rs diff --git a/crates/wallet/src/wallet/hardwaresigner.rs b/crates/wallet/src/wallet/hardwaresigner.rs deleted file mode 100644 index 3ec8d9a54..000000000 --- a/crates/wallet/src/wallet/hardwaresigner.rs +++ /dev/null @@ -1,94 +0,0 @@ -// Bitcoin Dev Kit -// Written in 2020 by Alekos Filini -// -// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these -// licenses. - -//! HWI Signer -//! -//! This module contains HWISigner, an implementation of a [TransactionSigner] to be -//! used with hardware wallets. -//! ```no_run -//! # use bdk_wallet::bitcoin::Network; -//! # use bdk_wallet::signer::SignerOrdering; -//! # use bdk_wallet::hardwaresigner::HWISigner; -//! # use bdk_wallet::AddressIndex::New; -//! # use bdk_wallet::{CreateParams, KeychainKind, SignOptions}; -//! # use hwi::HWIClient; -//! # use std::sync::Arc; -//! # -//! # fn main() -> Result<(), Box> { -//! let mut devices = HWIClient::enumerate()?; -//! if devices.is_empty() { -//! panic!("No devices found!"); -//! } -//! let first_device = devices.remove(0)?; -//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?; -//! -//! # let mut wallet = CreateParams::new("", "", Network::Testnet)?.create_wallet_no_persist()?; -//! # -//! // Adding the hardware signer to the BDK wallet -//! wallet.add_signer( -//! KeychainKind::External, -//! SignerOrdering(200), -//! Arc::new(custom_signer), -//! ); -//! -//! # Ok(()) -//! # } -//! ``` - -use bitcoin::bip32::Fingerprint; -use bitcoin::secp256k1::{All, Secp256k1}; -use bitcoin::Psbt; - -use hwi::error::Error; -use hwi::types::{HWIChain, HWIDevice}; -use hwi::HWIClient; - -use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; - -#[derive(Debug)] -/// Custom signer for Hardware Wallets -/// -/// This ignores `sign_options` and leaves the decisions up to the hardware wallet. -pub struct HWISigner { - fingerprint: Fingerprint, - client: HWIClient, -} - -impl HWISigner { - /// Create a instance from the specified device and chain - pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { - let client = HWIClient::get_client(device, false, chain)?; - Ok(HWISigner { - fingerprint: device.fingerprint, - client, - }) - } -} - -impl SignerCommon for HWISigner { - fn id(&self, _secp: &Secp256k1) -> SignerId { - SignerId::Fingerprint(self.fingerprint) - } -} - -/// This implementation ignores `sign_options` -impl TransactionSigner for HWISigner { - fn sign_transaction( - &self, - psbt: &mut Psbt, - _sign_options: &crate::SignOptions, - _secp: &crate::wallet::utils::SecpCtx, - ) -> Result<(), SignerError> { - psbt.combine(self.client.sign_tx(psbt)?.psbt) - .expect("Failed to combine HW signed psbt with passed PSBT"); - Ok(()) - } -} From b118b82fb08ee661ac10ac1771640b0965cf1ee8 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 15 Aug 2024 21:59:29 -0300 Subject: [PATCH 52/77] refactor(bdk_hwi)!: remove `bdk_hwi` - removes `bdk_hwi` crate, as `HWISigner`'s being moved to rust-hwi. - please refer to: https://github.com/bitcoindevkit/rust-hwi/pull/104 --- Cargo.toml | 1 - crates/hwi/Cargo.toml | 16 ------- crates/hwi/README.md | 3 -- crates/hwi/src/lib.rs | 39 ----------------- crates/hwi/src/signer.rs | 94 ---------------------------------------- 5 files changed, 153 deletions(-) delete mode 100644 crates/hwi/Cargo.toml delete mode 100644 crates/hwi/README.md delete mode 100644 crates/hwi/src/lib.rs delete mode 100644 crates/hwi/src/signer.rs diff --git a/Cargo.toml b/Cargo.toml index 26ca48b76..a6e6eb6e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,6 @@ members = [ "crates/electrum", "crates/esplora", "crates/bitcoind_rpc", - "crates/hwi", "crates/testenv", "example-crates/example_cli", "example-crates/example_electrum", diff --git a/crates/hwi/Cargo.toml b/crates/hwi/Cargo.toml deleted file mode 100644 index fdb146626..000000000 --- a/crates/hwi/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "bdk_hwi" -version = "0.5.0" -edition = "2021" -homepage = "https://bitcoindevkit.org" -repository = "https://github.com/bitcoindevkit/bdk" -description = "Utilities to use bdk with hardware wallets" -license = "MIT OR Apache-2.0" -readme = "README.md" - -[lints] -workspace = true - -[dependencies] -bdk_wallet = { path = "../wallet", version = "1.0.0-beta.2" } -hwi = { version = "0.9.0", features = [ "miniscript" ] } diff --git a/crates/hwi/README.md b/crates/hwi/README.md deleted file mode 100644 index f31601b97..000000000 --- a/crates/hwi/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# BDK HWI Signer - -This crate contains `HWISigner`, an implementation of a `TransactionSigner` to be used with hardware wallets. \ No newline at end of file diff --git a/crates/hwi/src/lib.rs b/crates/hwi/src/lib.rs deleted file mode 100644 index 7bb7e4caf..000000000 --- a/crates/hwi/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -//! HWI Signer -//! -//! This crate contains HWISigner, an implementation of a [`TransactionSigner`] to be -//! used with hardware wallets. -//! ```no_run -//! # use bdk_wallet::bitcoin::Network; -//! # use bdk_wallet::descriptor::Descriptor; -//! # use bdk_wallet::signer::SignerOrdering; -//! # use bdk_hwi::HWISigner; -//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet}; -//! # use hwi::HWIClient; -//! # use std::sync::Arc; -//! # use std::str::FromStr; -//! # -//! # fn main() -> Result<(), Box> { -//! let mut devices = HWIClient::enumerate()?; -//! if devices.is_empty() { -//! panic!("No devices found!"); -//! } -//! let first_device = devices.remove(0)?; -//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?; -//! -//! # let mut wallet = Wallet::create("", "").network(Network::Testnet).create_wallet_no_persist()?; -//! # -//! // Adding the hardware signer to the BDK wallet -//! wallet.add_signer( -//! KeychainKind::External, -//! SignerOrdering(200), -//! Arc::new(custom_signer), -//! ); -//! -//! # Ok(()) -//! # } -//! ``` -//! -//! [`TransactionSigner`]: bdk_wallet::signer::TransactionSigner - -mod signer; -pub use signer::*; diff --git a/crates/hwi/src/signer.rs b/crates/hwi/src/signer.rs deleted file mode 100644 index f1b569e35..000000000 --- a/crates/hwi/src/signer.rs +++ /dev/null @@ -1,94 +0,0 @@ -use bdk_wallet::bitcoin::bip32::Fingerprint; -use bdk_wallet::bitcoin::secp256k1::{All, Secp256k1}; -use bdk_wallet::bitcoin::Psbt; - -use hwi::error::Error; -use hwi::types::{HWIChain, HWIDevice}; -use hwi::HWIClient; - -use bdk_wallet::signer::{SignerCommon, SignerError, SignerId, TransactionSigner}; - -#[derive(Debug)] -/// Custom signer for Hardware Wallets -/// -/// This ignores `sign_options` and leaves the decisions up to the hardware wallet. -pub struct HWISigner { - fingerprint: Fingerprint, - client: HWIClient, -} - -impl HWISigner { - /// Create a instance from the specified device and chain - pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result { - let client = HWIClient::get_client(device, false, chain)?; - Ok(HWISigner { - fingerprint: device.fingerprint, - client, - }) - } -} - -impl SignerCommon for HWISigner { - fn id(&self, _secp: &Secp256k1) -> SignerId { - SignerId::Fingerprint(self.fingerprint) - } -} - -impl TransactionSigner for HWISigner { - fn sign_transaction( - &self, - psbt: &mut Psbt, - _sign_options: &bdk_wallet::SignOptions, - _secp: &Secp256k1, - ) -> Result<(), SignerError> { - psbt.combine( - self.client - .sign_tx(psbt) - .map_err(|e| { - SignerError::External(format!("While signing with hardware wallet: {}", e)) - })? - .psbt, - ) - .expect("Failed to combine HW signed psbt with passed PSBT"); - Ok(()) - } -} - -// TODO: re-enable this once we have the `get_funded_wallet` test util -// #[cfg(test)] -// mod tests { -// #[test] -// fn test_hardware_signer() { -// use std::sync::Arc; -// -// use bdk_wallet::tests::get_funded_wallet; -// use bdk_wallet::signer::SignerOrdering; -// use bdk_wallet::bitcoin::Network; -// use crate::HWISigner; -// use hwi::HWIClient; -// -// let mut devices = HWIClient::enumerate().unwrap(); -// if devices.is_empty() { -// panic!("No devices found!"); -// } -// let device = devices.remove(0).unwrap(); -// let client = HWIClient::get_client(&device, true, Network::Regtest.into()).unwrap(); -// let descriptors = client.get_descriptors::(None).unwrap(); -// let custom_signer = HWISigner::from_device(&device, Network::Regtest.into()).unwrap(); -// -// let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]); -// wallet.add_signer( -// bdk_wallet::KeychainKind::External, -// SignerOrdering(200), -// Arc::new(custom_signer), -// ); -// -// let addr = wallet.get_address(bdk_wallet::AddressIndex::LastUnused); -// let mut builder = wallet.build_tx(); -// builder.drain_to(addr.script_pubkey()).drain_wallet(); -// let (mut psbt, _) = builder.finish().unwrap(); -// -// let finalized = wallet.sign(&mut psbt, Default::default()).unwrap(); -// assert!(finalized); -// } -// } From 75c97a6018033bbeaeeba6b69ca74b67dc47161d Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 15 Aug 2024 22:09:37 -0300 Subject: [PATCH 53/77] fix(ci)!: remove `Dockerfile.ledger` and `hwi` steps from coverage step --- .github/workflows/code_coverage.yml | 11 ----------- ci/Dockerfile.ledger | 9 --------- 2 files changed, 20 deletions(-) delete mode 100644 ci/Dockerfile.ledger diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index 5a91de04e..613a015d8 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -27,17 +27,6 @@ jobs: uses: Swatinem/rust-cache@v2.2.1 - name: Install grcov run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi - # TODO: re-enable the hwi tests - - name: Build simulator image - run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger - - name: Run simulator image - run: docker run --name simulator --network=host hwi/ledger_emulator & - - name: Install Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - - name: Install python dependencies - run: pip install hwi==2.1.1 protobuf==3.20.1 - name: Test run: cargo test --all-features - name: Make coverage directory diff --git a/ci/Dockerfile.ledger b/ci/Dockerfile.ledger deleted file mode 100644 index fb4c6bc41..000000000 --- a/ci/Dockerfile.ledger +++ /dev/null @@ -1,9 +0,0 @@ -# Taken from bitcoindevkit/rust-hwi -FROM ghcr.io/ledgerhq/speculos - -RUN apt-get update -RUN apt-get install wget -y -RUN wget "https://github.com/LedgerHQ/speculos/blob/master/apps/nanos%23btc%232.1%231c8db8da.elf?raw=true" -O /speculos/btc.elf -ADD automation.json /speculos/automation.json - -ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--model", "nanos", "--display", "headless", "--vnc-port", "41000", "btc.elf"] From cd8ddfe18441c562342bcaccf7cae3a0d9ebbd92 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Wed, 11 Sep 2024 13:10:25 -0300 Subject: [PATCH 54/77] chore: remove `ci/automation.json` file, used by Dockerfile.ledger --- ci/automation.json | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 ci/automation.json diff --git a/ci/automation.json b/ci/automation.json deleted file mode 100644 index 9de2f60e6..000000000 --- a/ci/automation.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "version": 1, - "rules": [ - { - "regexp": "Address \\(\\d/\\d\\)|Message hash \\(\\d/\\d\\)|Confirm|Fees|Review|Amount", - "actions": [ - [ "button", 2, true ], - [ "button", 2, false ] - ] - }, - { - "text": "Sign", - "conditions": [ - [ "seen", false ] - ], - "actions": [ - [ "button", 2, true ], - [ "button", 2, false ], - [ "setbool", "seen", true ] - ] - }, - { - "regexp": "Approve|Sign|Accept", - "actions": [ - [ "button", 3, true ], - [ "button", 3, false ] - ] - } - ] -} From 65be4ead03126430ab47bf192d0de0db1b1b883a Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 10 Sep 2024 21:38:06 -0500 Subject: [PATCH 55/77] test(coin_selection): add test for deterministic utxo selection Added back ignored branch and bounnd tests and cleaned up calculation for target amounts. --- crates/wallet/src/wallet/coin_selection.rs | 166 +++++++++++++++++---- 1 file changed, 136 insertions(+), 30 deletions(-) diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 306124016..8cb3b874f 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -214,7 +214,6 @@ pub trait CoinSelectionAlgorithm: core::fmt::Debug { /// accumulated from added outputs and transaction’s header. /// - `drain_script`: the script to use in case of change /// - `rand`: random number generated used by some coin selection algorithms such as [`SingleRandomDraw`] - #[allow(clippy::too_many_arguments)] fn coin_select( &self, required_utxos: Vec, @@ -398,14 +397,14 @@ impl OutputGroup { /// /// Code adapted from Bitcoin Core's implementation and from Mark Erhardt Master's Thesis: #[derive(Debug, Clone)] -pub struct BranchAndBoundCoinSelection { +pub struct BranchAndBoundCoinSelection { size_of_change: u64, - fallback_algorithm: FA, + fallback_algorithm: Cs, } /// Error returned by branch and bond coin selection. #[derive(Debug)] -enum BnBError { +enum BnbError { /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for /// the desired outputs plus fee, if there is not such combination this error is thrown NoExactMatch, @@ -414,19 +413,19 @@ enum BnBError { TotalTriesExceeded, } -impl Default for BranchAndBoundCoinSelection { +impl Default for BranchAndBoundCoinSelection { fn default() -> Self { Self { // P2WPKH cost of change -> value (8 bytes) + script len (1 bytes) + script (22 bytes) size_of_change: 8 + 1 + 22, - fallback_algorithm: FA::default(), + fallback_algorithm: Cs::default(), } } } -impl BranchAndBoundCoinSelection { +impl BranchAndBoundCoinSelection { /// Create new instance with a target `size_of_change` and `fallback_algorithm`. - pub fn new(size_of_change: u64, fallback_algorithm: FA) -> Self { + pub fn new(size_of_change: u64, fallback_algorithm: Cs) -> Self { Self { size_of_change, fallback_algorithm, @@ -436,7 +435,7 @@ impl BranchAndBoundCoinSelection { const BNB_TOTAL_TRIES: usize = 100_000; -impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { +impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection { fn coin_select( &self, required_utxos: Vec, @@ -544,7 +543,7 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSe } } -impl BranchAndBoundCoinSelection { +impl BranchAndBoundCoinSelection { // TODO: make this more Rust-onic :) // (And perhaps refactor with less arguments?) #[allow(clippy::too_many_arguments)] @@ -558,7 +557,7 @@ impl BranchAndBoundCoinSelection { cost_of_change: u64, drain_script: &Script, fee_rate: FeeRate, - ) -> Result { + ) -> Result { // current_selection[i] will contain true if we are using optional_utxos[i], // false otherwise. Note that current_selection.len() could be less than // optional_utxos.len(), it just means that we still haven't decided if we should keep @@ -614,7 +613,7 @@ impl BranchAndBoundCoinSelection { // We have walked back to the first utxo and no branch is untraversed. All solutions searched // If best selection is empty, then there's no exact match if best_selection.is_empty() { - return Err(BnBError::NoExactMatch); + return Err(BnbError::NoExactMatch); } break; } @@ -641,7 +640,7 @@ impl BranchAndBoundCoinSelection { // Check for solution if best_selection.is_empty() { - return Err(BnBError::TotalTriesExceeded); + return Err(BnbError::TotalTriesExceeded); } // Set output set @@ -929,6 +928,14 @@ mod test { .sum() } + fn calc_target_amount(utxos: &[WeightedUtxo], fee_rate: FeeRate) -> u64 { + utxos + .iter() + .cloned() + .map(|utxo| u64::try_from(OutputGroup::new(utxo, fee_rate).effective_value).unwrap()) + .sum() + } + #[test] fn test_largest_first_coin_selection_success() { let utxos = get_test_utxos(); @@ -1143,7 +1150,6 @@ mod test { } #[test] - #[ignore = "SRD fn was moved out of BnB"] fn test_bnb_coin_selection_success() { // In this case bnb won't find a suitable match and single random draw will // select three outputs @@ -1190,17 +1196,18 @@ mod test { } #[test] - #[ignore = "no exact match for bnb, previously fell back to SRD"] fn test_bnb_coin_selection_optional_are_enough() { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); - let target_amount = 299756 + FEE_AMOUNT; + let fee_rate = FeeRate::BROADCAST_MIN; + // first and third utxo's effective value + let target_amount = calc_target_amount(&[utxos[0].clone(), utxos[2].clone()], fee_rate); let result = BranchAndBoundCoinSelection::::default() .coin_select( vec![], utxos, - FeeRate::from_sat_per_vb_unchecked(1), + fee_rate, target_amount, &drain_script, &mut thread_rng(), @@ -1233,7 +1240,6 @@ mod test { } #[test] - #[ignore] fn test_bnb_coin_selection_required_not_enough() { let utxos = get_test_utxos(); @@ -1252,13 +1258,15 @@ mod test { assert!(amount > 150_000); let drain_script = ScriptBuf::default(); - let target_amount = 150_000 + FEE_AMOUNT; + let fee_rate = FeeRate::BROADCAST_MIN; + // first and third utxo's effective value + let target_amount = calc_target_amount(&[utxos[0].clone(), utxos[2].clone()], fee_rate); let result = BranchAndBoundCoinSelection::::default() .coin_select( required, optional, - FeeRate::from_sat_per_vb_unchecked(1), + fee_rate, target_amount, &drain_script, &mut thread_rng(), @@ -1312,14 +1320,15 @@ mod test { fn test_bnb_coin_selection_check_fee_rate() { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); - let target_amount = 99932; // first utxo's effective value - let feerate = FeeRate::BROADCAST_MIN; + let fee_rate = FeeRate::BROADCAST_MIN; + // first utxo's effective value + let target_amount = calc_target_amount(&utxos[0..1], fee_rate); - let result = BranchAndBoundCoinSelection::new(0, SingleRandomDraw) + let result = BranchAndBoundCoinSelection::::default() .coin_select( vec![], utxos, - feerate, + fee_rate, target_amount, &drain_script, &mut thread_rng(), @@ -1332,7 +1341,7 @@ mod test { TxIn::default().segwit_weight().to_wu() + P2WPKH_SATISFACTION_SIZE as u64; // the final fee rate should be exactly the same as the fee rate given let result_feerate = Amount::from_sat(result.fee_amount) / Weight::from_wu(input_weight); - assert_eq!(result_feerate, feerate); + assert_eq!(result_feerate, fee_rate); } #[test] @@ -1344,7 +1353,7 @@ mod test { let mut optional_utxos = generate_random_utxos(&mut rng, 16); let target_amount = sum_random_utxos(&mut rng, &mut optional_utxos); let drain_script = ScriptBuf::default(); - let result = BranchAndBoundCoinSelection::new(0, SingleRandomDraw) + let result = BranchAndBoundCoinSelection::::default() .coin_select( vec![], optional_utxos, @@ -1374,7 +1383,7 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; - BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw) + BranchAndBoundCoinSelection::::default() .bnb( vec![], utxos, @@ -1405,7 +1414,7 @@ mod test { let drain_script = ScriptBuf::default(); - BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw) + BranchAndBoundCoinSelection::::default() .bnb( vec![], utxos, @@ -1441,7 +1450,7 @@ mod test { let drain_script = ScriptBuf::default(); - let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw) + let result = BranchAndBoundCoinSelection::::default() .bnb( vec![], utxos, @@ -1481,7 +1490,7 @@ mod test { let drain_script = ScriptBuf::default(); - let result = BranchAndBoundCoinSelection::new(0, SingleRandomDraw) + let result = BranchAndBoundCoinSelection::::default() .bnb( vec![], optional_utxos, @@ -1681,4 +1690,101 @@ mod test { ); } } + + #[test] + fn test_deterministic_coin_selection_picks_same_utxos() { + enum CoinSelectionAlgo { + BranchAndBound, + OldestFirst, + LargestFirst, + } + + struct TestCase<'a> { + name: &'a str, + coin_selection_algo: CoinSelectionAlgo, + exp_vouts: &'a [u32], + } + + let test_cases = [ + TestCase { + name: "branch and bound", + coin_selection_algo: CoinSelectionAlgo::BranchAndBound, + // note: we expect these to be sorted largest first, which indicates + // BnB succeeded with no fallback + exp_vouts: &[29, 28, 27], + }, + TestCase { + name: "oldest first", + coin_selection_algo: CoinSelectionAlgo::OldestFirst, + exp_vouts: &[0, 1, 2], + }, + TestCase { + name: "largest first", + coin_selection_algo: CoinSelectionAlgo::LargestFirst, + exp_vouts: &[29, 28, 27], + }, + ]; + + let optional = generate_same_value_utxos(100_000, 30); + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); + let target_amount = calc_target_amount(&optional[0..3], fee_rate); + assert_eq!(target_amount, 299_796); + let drain_script = ScriptBuf::default(); + + for tc in test_cases { + let optional = optional.clone(); + + let result = match tc.coin_selection_algo { + CoinSelectionAlgo::BranchAndBound => { + BranchAndBoundCoinSelection::::default().coin_select( + vec![], + optional, + fee_rate, + target_amount, + &drain_script, + &mut thread_rng(), + ) + } + CoinSelectionAlgo::OldestFirst => OldestFirstCoinSelection.coin_select( + vec![], + optional, + fee_rate, + target_amount, + &drain_script, + &mut thread_rng(), + ), + CoinSelectionAlgo::LargestFirst => LargestFirstCoinSelection.coin_select( + vec![], + optional, + fee_rate, + target_amount, + &drain_script, + &mut thread_rng(), + ), + }; + + assert!(result.is_ok(), "coin_select failed {}", tc.name); + let result = result.unwrap(); + assert!(matches!(result.excess, Excess::NoChange { .. },)); + assert_eq!( + result.selected.len(), + 3, + "wrong selected len for {}", + tc.name + ); + assert_eq!( + result.selected_amount(), + 300_000, + "wrong selected amount for {}", + tc.name + ); + assert_eq!(result.fee_amount, 204, "wrong fee amount for {}", tc.name); + let vouts = result + .selected + .iter() + .map(|utxo| utxo.outpoint().vout) + .collect::>(); + assert_eq!(vouts, tc.exp_vouts, "wrong selected vouts for {}", tc.name); + } + } } From 2970b83f30ca7071f0502de395327eb3671a512b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 12 Sep 2024 16:44:21 +0800 Subject: [PATCH 56/77] fix(core): calling `CheckPoint::insert` with existing block must succeed Previously, we were panicking when the caller tried to insert a block at height 0. However, this should succeed if the block hash does not change. --- crates/core/src/checkpoint.rs | 3 +-- crates/core/tests/common.rs | 9 +++++++ crates/core/tests/test_checkpoint.rs | 36 ++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 crates/core/tests/common.rs create mode 100644 crates/core/tests/test_checkpoint.rs diff --git a/crates/core/src/checkpoint.rs b/crates/core/src/checkpoint.rs index 0abadda1d..089db230d 100644 --- a/crates/core/src/checkpoint.rs +++ b/crates/core/src/checkpoint.rs @@ -173,8 +173,6 @@ impl CheckPoint { /// passed in. Of course, if the `block_id` was already present then this just returns `self`. #[must_use] pub fn insert(self, block_id: BlockId) -> Self { - assert_ne!(block_id.height, 0, "cannot insert the genesis block"); - let mut cp = self.clone(); let mut tail = vec![]; let base = loop { @@ -182,6 +180,7 @@ impl CheckPoint { if cp.hash() == block_id.hash { return self; } + assert_ne!(cp.height(), 0, "cannot replace genesis block"); // if we have a conflict we just return the inserted block because the tail is by // implication invalid. tail = vec![]; diff --git a/crates/core/tests/common.rs b/crates/core/tests/common.rs new file mode 100644 index 000000000..347a90bf6 --- /dev/null +++ b/crates/core/tests/common.rs @@ -0,0 +1,9 @@ +#[allow(unused_macros)] +macro_rules! block_id { + ($height:expr, $hash:literal) => {{ + bdk_chain::BlockId { + height: $height, + hash: bitcoin::hashes::Hash::hash($hash.as_bytes()), + } + }}; +} diff --git a/crates/core/tests/test_checkpoint.rs b/crates/core/tests/test_checkpoint.rs new file mode 100644 index 000000000..66f945fe2 --- /dev/null +++ b/crates/core/tests/test_checkpoint.rs @@ -0,0 +1,36 @@ +#[macro_use] +mod common; + +use bdk_core::CheckPoint; + +/// Inserting a block that already exists in the checkpoint chain must always succeed. +#[test] +fn checkpoint_insert_existing() { + let blocks = &[ + block_id!(0, "genesis"), + block_id!(1, "A"), + block_id!(2, "B"), + block_id!(3, "C"), + ]; + + // Index `i` allows us to test with chains of different length. + // Index `j` allows us to test inserting different block heights. + for i in 0..blocks.len() { + let cp_chain = CheckPoint::from_block_ids(blocks[..=i].iter().copied()) + .expect("must construct valid chain"); + + for j in 0..i { + let block_to_insert = cp_chain + .get(j as u32) + .expect("cp of height must exist") + .block_id(); + let new_cp_chain = cp_chain.clone().insert(block_to_insert); + + assert_eq!( + new_cp_chain, cp_chain, + "must not divert from original chain" + ); + assert!(new_cp_chain.eq_ptr(&cp_chain), "pointers must still match"); + } + } +} From 22a2f83db5df5f48974a06deef6b45d41a05ef3b Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Thu, 12 Sep 2024 13:14:54 -0500 Subject: [PATCH 57/77] fix(wallet): fix SingleRandomDraw to throw an error if insufficient funds * fixed spelling and clippy errors * updated tests to check for error variant instead of a panic --- crates/wallet/src/wallet/coin_selection.rs | 289 +++++++++------------ 1 file changed, 129 insertions(+), 160 deletions(-) diff --git a/crates/wallet/src/wallet/coin_selection.rs b/crates/wallet/src/wallet/coin_selection.rs index 8cb3b874f..381ff65b2 100644 --- a/crates/wallet/src/wallet/coin_selection.rs +++ b/crates/wallet/src/wallet/coin_selection.rs @@ -402,7 +402,7 @@ pub struct BranchAndBoundCoinSelection { fallback_algorithm: Cs, } -/// Error returned by branch and bond coin selection. +/// Error returned by branch and bound coin selection. #[derive(Debug)] enum BnbError { /// Branch and bound coin selection tries to avoid needing a change by finding the right inputs for @@ -521,8 +521,8 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSe } match self.bnb( - required_ogs.clone(), - optional_ogs.clone(), + required_ogs, + optional_ogs, curr_value, curr_available_value, signed_target_amount, @@ -671,73 +671,25 @@ impl CoinSelectionAlgorithm for SingleRandomDraw { fn coin_select( &self, required_utxos: Vec, - optional_utxos: Vec, + mut optional_utxos: Vec, fee_rate: FeeRate, target_amount: u64, drain_script: &Script, rand: &mut R, ) -> Result { - Ok(single_random_draw( - required_utxos, - optional_utxos, - target_amount, - drain_script, - fee_rate, - rand, - )) - } -} - -// Pull UTXOs at random until we have enough to meet the target -pub(crate) fn single_random_draw( - required_utxos: Vec, - optional_utxos: Vec, - target_amount: u64, - drain_script: &Script, - fee_rate: FeeRate, - rng: &mut impl RngCore, -) -> CoinSelectionResult { - let target_amount = target_amount - .try_into() - .expect("Bitcoin amount to fit into i64"); - - let required_utxos: Vec = required_utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let mut optional_utxos: Vec = optional_utxos - .into_iter() - .map(|u| OutputGroup::new(u, fee_rate)) - .collect(); - - let curr_value = required_utxos - .iter() - .fold(0, |acc, x| acc + x.effective_value); - - shuffle_slice(&mut optional_utxos, rng); - - let selected_utxos = - optional_utxos - .into_iter() - .fold((curr_value, vec![]), |(mut amount, mut utxos), utxo| { - if amount >= target_amount { - (amount, utxos) - } else { - amount += utxo.effective_value; - utxos.push(utxo); - (amount, utxos) - } - }); - - // remaining_amount can't be negative as that would mean the - // selection wasn't successful - // target_amount = amount_needed + (fee_amount - vin_fees) - let remaining_amount = (selected_utxos.0 - target_amount) as u64; + // We put the required UTXOs first and then the randomize optional UTXOs to take as needed + let utxos = { + shuffle_slice(&mut optional_utxos, rand); - let excess = decide_change(remaining_amount, fee_rate, drain_script); + required_utxos + .into_iter() + .map(|utxo| (true, utxo)) + .chain(optional_utxos.into_iter().map(|utxo| (false, utxo))) + }; - calculate_cs_result(selected_utxos.1, required_utxos, excess) + // select required UTXOs and then random optional UTXOs. + select_sorted_utxos(utxos, fee_rate, target_amount, drain_script) + } } fn calculate_cs_result( @@ -1003,41 +955,37 @@ mod test { } #[test] - #[should_panic(expected = "InsufficientFunds")] fn test_largest_first_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); let target_amount = 500_000 + FEE_AMOUNT; - LargestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - &mut thread_rng(), - ) - .unwrap(); + let result = LargestFirstCoinSelection.coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + &mut thread_rng(), + ); + assert!(matches!(result, Err(InsufficientFunds { .. }))); } #[test] - #[should_panic(expected = "InsufficientFunds")] fn test_largest_first_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); let target_amount = 250_000 + FEE_AMOUNT; - LargestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1000), - target_amount, - &drain_script, - &mut thread_rng(), - ) - .unwrap(); + let result = LargestFirstCoinSelection.coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1000), + target_amount, + &drain_script, + &mut thread_rng(), + ); + assert!(matches!(result, Err(InsufficientFunds { .. }))); } #[test] @@ -1107,26 +1055,23 @@ mod test { } #[test] - #[should_panic(expected = "InsufficientFunds")] fn test_oldest_first_coin_selection_insufficient_funds() { let utxos = get_oldest_first_test_utxos(); let drain_script = ScriptBuf::default(); let target_amount = 600_000 + FEE_AMOUNT; - OldestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - &mut thread_rng(), - ) - .unwrap(); + let result = OldestFirstCoinSelection.coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + &mut thread_rng(), + ); + assert!(matches!(result, Err(InsufficientFunds { .. }))); } #[test] - #[should_panic(expected = "InsufficientFunds")] fn test_oldest_first_coin_selection_insufficient_funds_high_fees() { let utxos = get_oldest_first_test_utxos(); @@ -1137,16 +1082,15 @@ mod test { - 50; let drain_script = ScriptBuf::default(); - OldestFirstCoinSelection - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1000), - target_amount, - &drain_script, - &mut thread_rng(), - ) - .unwrap(); + let result = OldestFirstCoinSelection.coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1000), + target_amount, + &drain_script, + &mut thread_rng(), + ); + assert!(matches!(result, Err(InsufficientFunds { .. }))); } #[test] @@ -1227,16 +1171,46 @@ mod test { let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT; let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); let drain_script = ScriptBuf::default(); - let result = single_random_draw( + + let result = SingleRandomDraw.coin_select( vec![], utxos, + fee_rate, target_amount, &drain_script, + &mut thread_rng(), + ); + + assert!( + matches!(result, Ok(CoinSelectionResult {selected, fee_amount, ..}) + if selected.iter().map(|u| u.txout().value.to_sat()).sum::() > target_amount + && fee_amount == ((selected.len() * 68) as u64) + ) + ); + } + + #[test] + fn test_single_random_draw_function_error() { + let seed = [0; 32]; + let mut rng: StdRng = SeedableRng::from_seed(seed); + + // 100_000, 10, 200_000 + let utxos = get_test_utxos(); + let target_amount = 300_000 + FEE_AMOUNT; + let fee_rate = FeeRate::from_sat_per_vb_unchecked(1); + let drain_script = ScriptBuf::default(); + + let result = SingleRandomDraw.coin_select( + vec![], + utxos, fee_rate, + target_amount, + &drain_script, &mut rng, ); - assert!(result.selected_amount() > target_amount); - assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64); + + assert!(matches!(result, Err(InsufficientFunds {needed, available}) + if needed == 300_254 && available == 300_010)); } #[test] @@ -1279,41 +1253,38 @@ mod test { } #[test] - #[should_panic(expected = "InsufficientFunds")] fn test_bnb_coin_selection_insufficient_funds() { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); let target_amount = 500_000 + FEE_AMOUNT; - BranchAndBoundCoinSelection::::default() - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1), - target_amount, - &drain_script, - &mut thread_rng(), - ) - .unwrap(); + let result = BranchAndBoundCoinSelection::::default().coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1), + target_amount, + &drain_script, + &mut thread_rng(), + ); + + assert!(matches!(result, Err(InsufficientFunds { .. }))); } #[test] - #[should_panic(expected = "InsufficientFunds")] fn test_bnb_coin_selection_insufficient_funds_high_fees() { let utxos = get_test_utxos(); let drain_script = ScriptBuf::default(); let target_amount = 250_000 + FEE_AMOUNT; - BranchAndBoundCoinSelection::::default() - .coin_select( - vec![], - utxos, - FeeRate::from_sat_per_vb_unchecked(1000), - target_amount, - &drain_script, - &mut thread_rng(), - ) - .unwrap(); + let result = BranchAndBoundCoinSelection::::default().coin_select( + vec![], + utxos, + FeeRate::from_sat_per_vb_unchecked(1000), + target_amount, + &drain_script, + &mut thread_rng(), + ); + assert!(matches!(result, Err(InsufficientFunds { .. }))); } #[test] @@ -1368,7 +1339,6 @@ mod test { } #[test] - #[should_panic(expected = "NoExactMatch")] fn test_bnb_function_no_exact_match() { let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); let utxos: Vec = get_test_utxos() @@ -1383,22 +1353,20 @@ mod test { let drain_script = ScriptBuf::default(); let target_amount = 20_000 + FEE_AMOUNT; - BranchAndBoundCoinSelection::::default() - .bnb( - vec![], - utxos, - 0, - curr_available_value, - target_amount as i64, - cost_of_change, - &drain_script, - fee_rate, - ) - .unwrap(); + let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw).bnb( + vec![], + utxos, + 0, + curr_available_value, + target_amount as i64, + cost_of_change, + &drain_script, + fee_rate, + ); + assert!(matches!(result, Err(BnbError::NoExactMatch))); } #[test] - #[should_panic(expected = "TotalTriesExceeded")] fn test_bnb_function_tries_exceeded() { let fee_rate = FeeRate::from_sat_per_vb_unchecked(10); let utxos: Vec = generate_same_value_utxos(100_000, 100_000) @@ -1414,18 +1382,17 @@ mod test { let drain_script = ScriptBuf::default(); - BranchAndBoundCoinSelection::::default() - .bnb( - vec![], - utxos, - 0, - curr_available_value, - target_amount as i64, - cost_of_change, - &drain_script, - fee_rate, - ) - .unwrap(); + let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw).bnb( + vec![], + utxos, + 0, + curr_available_value, + target_amount as i64, + cost_of_change, + &drain_script, + fee_rate, + ); + assert!(matches!(result, Err(BnbError::TotalTriesExceeded))); } // The match won't be exact but still in the range @@ -1450,7 +1417,7 @@ mod test { let drain_script = ScriptBuf::default(); - let result = BranchAndBoundCoinSelection::::default() + let result = BranchAndBoundCoinSelection::new(size_of_change, SingleRandomDraw) .bnb( vec![], utxos, @@ -1588,7 +1555,9 @@ mod test { let target_amount = 190_000; let drain_script = ScriptBuf::new(); // bnb won't find exact match and should select oldest first - let res = BranchAndBoundCoinSelection::::default() + let bnb_with_oldest_first = + BranchAndBoundCoinSelection::new(8 + 1 + 22, OldestFirstCoinSelection); + let res = bnb_with_oldest_first .coin_select( vec![], optional_utxos, From e6aeaea0c69472e4c2dcf0f5e2f632f43733529d Mon Sep 17 00:00:00 2001 From: valued mammal Date: Thu, 12 Sep 2024 15:18:01 -0400 Subject: [PATCH 58/77] doc(core): document panic for `CheckPoint::insert` --- crates/core/src/checkpoint.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/core/src/checkpoint.rs b/crates/core/src/checkpoint.rs index 089db230d..10af13277 100644 --- a/crates/core/src/checkpoint.rs +++ b/crates/core/src/checkpoint.rs @@ -171,6 +171,10 @@ impl CheckPoint { /// it. If the height already existed and has a conflicting block hash then it will be purged /// along with all block followin it. The returned chain will have a tip of the `block_id` /// passed in. Of course, if the `block_id` was already present then this just returns `self`. + /// + /// # Panics + /// + /// This panics if called with a genesis block that differs from that of `self`. #[must_use] pub fn insert(self, block_id: BlockId) -> Self { let mut cp = self.clone(); From 3ae9ecba8c893750fa2ed5dfdbb1f4ee84a0b228 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Thu, 12 Sep 2024 15:20:16 -0400 Subject: [PATCH 59/77] test: fix off-by-one in `checkpoint_insert_existing` --- crates/core/tests/test_checkpoint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/core/tests/test_checkpoint.rs b/crates/core/tests/test_checkpoint.rs index 66f945fe2..705f1c474 100644 --- a/crates/core/tests/test_checkpoint.rs +++ b/crates/core/tests/test_checkpoint.rs @@ -19,7 +19,7 @@ fn checkpoint_insert_existing() { let cp_chain = CheckPoint::from_block_ids(blocks[..=i].iter().copied()) .expect("must construct valid chain"); - for j in 0..i { + for j in 0..=i { let block_to_insert = cp_chain .get(j as u32) .expect("cp of height must exist") From 7a501c1c92c2859feb1a486a96f3dd9917d00a8c Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Fri, 13 Sep 2024 09:47:45 -0500 Subject: [PATCH 60/77] Bump bdk_wallet version to 1.0.0-beta.3 bdk_core to 0.1.1 bdk_chain to 0.18.1 bdk_bitcoind_rpc to 0.14.1 bdk_electrum to 0.17.1 bdk_esplora to 0.17.1 bdk_file_store to 0.15.1 bdk_testenv to 0.8.1 --- crates/bitcoind_rpc/Cargo.toml | 4 ++-- crates/chain/Cargo.toml | 2 +- crates/core/Cargo.toml | 2 +- crates/electrum/Cargo.toml | 4 ++-- crates/esplora/Cargo.toml | 4 ++-- crates/file_store/Cargo.toml | 2 +- crates/testenv/Cargo.toml | 2 +- crates/wallet/Cargo.toml | 2 +- 8 files changed, 11 insertions(+), 11 deletions(-) diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index 791f6b177..6bc803b84 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_bitcoind_rpc" -version = "0.14.0" +version = "0.14.1" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -22,7 +22,7 @@ bdk_core = { path = "../core", version = "0.1", default-features = false } [dev-dependencies] bdk_testenv = { path = "../testenv", default-features = false } -bdk_chain = { path = "../chain", version = "0.18" } +bdk_chain = { path = "../chain" } [features] default = ["std"] diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 2cc2b44b2..edbc94561 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_chain" -version = "0.18.0" +version = "0.18.1" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 5e27f768e..3987c16d0 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_core" -version = "0.1.0" +version = "0.1.1" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index f65911f89..12b951fdf 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_electrum" -version = "0.17.0" +version = "0.17.1" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -18,7 +18,7 @@ electrum-client = { version = "0.21", features = [ "proxy" ], default-features = [dev-dependencies] bdk_testenv = { path = "../testenv", default-features = false } -bdk_chain = { path = "../chain", version = "0.18.0" } +bdk_chain = { path = "../chain" } [features] default = ["use-rustls"] diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 911c7a6e9..9223ffd37 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_esplora" -version = "0.17.0" +version = "0.17.1" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -22,7 +22,7 @@ futures = { version = "0.3.26", optional = true } miniscript = { version = "12.0.0", optional = true, default-features = false } [dev-dependencies] -bdk_chain = { path = "../chain", version = "0.18.0" } +bdk_chain = { path = "../chain" } bdk_testenv = { path = "../testenv", default-features = false } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index 29af6b70f..e191b9499 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_file_store" -version = "0.15.0" +version = "0.15.1" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/bitcoindevkit/bdk" diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml index c6cef80a2..3301c4d37 100644 --- a/crates/testenv/Cargo.toml +++ b/crates/testenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_testenv" -version = "0.8.0" +version = "0.8.1" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index 7e4a16d0f..52d1a0af9 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bdk_wallet" homepage = "https://bitcoindevkit.org" -version = "1.0.0-beta.2" +version = "1.0.0-beta.3" repository = "https://github.com/bitcoindevkit/bdk" documentation = "https://docs.rs/bdk" description = "A modern, lightweight, descriptor-based wallet library" From b34b7778f44be05827fde84660f6b1ee144d9845 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Fri, 13 Sep 2024 15:10:55 -0500 Subject: [PATCH 61/77] Bump bdk_wallet version to 1.0.0-beta.4 bdk_core to 0.2.0 bdk_chain to 0.19.0 bdk_bitcoind_rpc to 0.15.0 bdk_electrum to 0.18.0 bdk_esplora to 0.18.0 bdk_file_store to 0.16.0 bdk_testenv to 0.9.0 --- crates/bitcoind_rpc/Cargo.toml | 4 ++-- crates/chain/Cargo.toml | 4 ++-- crates/core/Cargo.toml | 2 +- crates/electrum/Cargo.toml | 4 ++-- crates/esplora/Cargo.toml | 4 ++-- crates/file_store/Cargo.toml | 4 ++-- crates/testenv/Cargo.toml | 4 ++-- crates/wallet/Cargo.toml | 6 +++--- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index 6bc803b84..5694c895a 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_bitcoind_rpc" -version = "0.14.1" +version = "0.15.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -18,7 +18,7 @@ workspace = true [dependencies] bitcoin = { version = "0.32.0", default-features = false } bitcoincore-rpc = { version = "0.19.0" } -bdk_core = { path = "../core", version = "0.1", default-features = false } +bdk_core = { path = "../core", version = "0.2.0", default-features = false } [dev-dependencies] bdk_testenv = { path = "../testenv", default-features = false } diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index edbc94561..95bcaf9f6 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_chain" -version = "0.18.1" +version = "0.19.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -17,7 +17,7 @@ workspace = true [dependencies] bitcoin = { version = "0.32.0", default-features = false } -bdk_core = { path = "../core", version = "0.1", default-features = false } +bdk_core = { path = "../core", version = "0.2.0", default-features = false } serde = { version = "1", optional = true, features = ["derive", "rc"] } miniscript = { version = "12.0.0", optional = true, default-features = false } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 3987c16d0..3749b9157 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_core" -version = "0.1.1" +version = "0.2.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index 12b951fdf..f2529dc2d 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_electrum" -version = "0.17.1" +version = "0.18.0" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -13,7 +13,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_core = { path = "../core", version = "0.1" } +bdk_core = { path = "../core", version = "0.2.0" } electrum-client = { version = "0.21", features = [ "proxy" ], default-features = false } [dev-dependencies] diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 9223ffd37..c13fbb17b 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_esplora" -version = "0.17.1" +version = "0.18.0" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -15,7 +15,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_core = { path = "../core", version = "0.1", default-features = false } +bdk_core = { path = "../core", version = "0.2.0", default-features = false } esplora-client = { version = "0.9.0", default-features = false } async-trait = { version = "0.1.66", optional = true } futures = { version = "0.3.26", optional = true } diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index e191b9499..9c48b3dc5 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_file_store" -version = "0.15.1" +version = "0.16.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/bitcoindevkit/bdk" @@ -14,7 +14,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_chain = { path = "../chain", version = "0.18.0", features = [ "serde", "miniscript" ] } +bdk_chain = { path = "../chain", version = "0.19.0", features = [ "serde", "miniscript" ] } bincode = { version = "1" } serde = { version = "1", features = ["derive"] } diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml index 3301c4d37..98afa8d00 100644 --- a/crates/testenv/Cargo.toml +++ b/crates/testenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_testenv" -version = "0.8.1" +version = "0.9.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -16,7 +16,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_chain = { path = "../chain", version = "0.18", default-features = false } +bdk_chain = { path = "../chain", version = "0.19.0", default-features = false } electrsd = { version = "0.28.0", features = [ "bitcoind_25_0", "esplora_a33e97e1", "legacy" ] } [features] diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index 52d1a0af9..ebec3a842 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bdk_wallet" homepage = "https://bitcoindevkit.org" -version = "1.0.0-beta.3" +version = "1.0.0-beta.4" repository = "https://github.com/bitcoindevkit/bdk" documentation = "https://docs.rs/bdk" description = "A modern, lightweight, descriptor-based wallet library" @@ -21,8 +21,8 @@ miniscript = { version = "12.0.0", features = [ "serde" ], default-features = fa bitcoin = { version = "0.32.0", features = [ "serde", "base64" ], default-features = false } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } -bdk_chain = { path = "../chain", version = "0.18.0", features = [ "miniscript", "serde" ], default-features = false } -bdk_file_store = { path = "../file_store", version = "0.15.0", optional = true } +bdk_chain = { path = "../chain", version = "0.19.0", features = [ "miniscript", "serde" ], default-features = false } +bdk_file_store = { path = "../file_store", version = "0.16.0", optional = true } # Optional dependencies bip39 = { version = "2.0", optional = true } From 503f315cdde32dabbc83efd3879ac5cb26aacdbc Mon Sep 17 00:00:00 2001 From: Github Action Date: Sun, 15 Sep 2024 01:07:19 +0000 Subject: [PATCH 62/77] ci: automated update to rustc 1.81.0 --- rust-version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-version b/rust-version index b3a8c61e6..dbd41264a 100644 --- a/rust-version +++ b/rust-version @@ -1 +1 @@ -1.79.0 +1.81.0 From 33942ece8f2f1f0b3d85a201d659cde72a560646 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Mon, 16 Sep 2024 12:51:29 -0300 Subject: [PATCH 63/77] fix(bdk_esplora): build with `--no-default-features` - add `blocking-https` as one of the default features, instead of `blocking-https-rustls`, they are basically the same in `esplora-client`. - add `async` and `blocking as required features for each test, using the `[[test]]` cargo target. --- crates/esplora/Cargo.toml | 17 +++++++++++++++-- crates/esplora/README.md | 3 +++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index c13fbb17b..e7190f4ec 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -27,10 +27,23 @@ bdk_testenv = { path = "../testenv", default-features = false } tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] } [features] -default = ["std", "async-https", "blocking-https-rustls"] +default = ["std", "async-https", "blocking-https"] std = ["bdk_chain/std", "miniscript?/std"] async = ["async-trait", "futures", "esplora-client/async"] async-https = ["async", "esplora-client/async-https"] async-https-rustls = ["async", "esplora-client/async-https-rustls"] +async-https-native = ["async", "esplora-client/async-https-native"] blocking = ["esplora-client/blocking"] -blocking-https-rustls = ["esplora-client/blocking-https-rustls"] +blocking-https = ["blocking", "esplora-client/blocking-https"] +blocking-https-rustls = ["blocking", "esplora-client/blocking-https-rustls"] +blocking-https-native = ["blocking", "esplora-client/blocking-https-native"] + +[[test]] +name = "blocking" +path = "tests/blocking_ext.rs" +required-features = ["blocking"] + +[[test]] +name = "async" +path = "tests/async_ext.rs" +required-features = ["async"] diff --git a/crates/esplora/README.md b/crates/esplora/README.md index 0535b9a38..d06e386cd 100644 --- a/crates/esplora/README.md +++ b/crates/esplora/README.md @@ -26,8 +26,11 @@ bdk_esplora = { version = "0.3", features = ["async-https"] } To use the extension traits: ```rust // for blocking +#[cfg(feature = "blocking")] use bdk_esplora::EsploraExt; + // for async +#[cfg(feature = "async")] use bdk_esplora::EsploraAsyncExt; ``` From 6e8f19607aff55b1949fc7e9f943a333b4fcfb8a Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Mon, 16 Sep 2024 12:54:36 -0300 Subject: [PATCH 64/77] fix(bdk_electrum): build with `--no-default-features` - add `use-rustls` as required features for `test_electrum.rs`, using the `[[test]]` cargo target approach. --- crates/electrum/Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index f2529dc2d..1770c90c7 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -24,3 +24,8 @@ bdk_chain = { path = "../chain" } default = ["use-rustls"] use-rustls = ["electrum-client/use-rustls"] use-rustls-ring = ["electrum-client/use-rustls-ring"] + +[[test]] +name = "use-rustls" +path = "tests/test_electrum.rs" +required-features = ["use-rustls"] From 9b7b19586e1f74613f71b6c2403a7df419e13b65 Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Fri, 20 Sep 2024 12:03:54 -0300 Subject: [PATCH 65/77] chore: use path as `name` for cargo test targets --- crates/electrum/Cargo.toml | 3 +-- crates/esplora/Cargo.toml | 6 ++---- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index 1770c90c7..ed1fd06a0 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -26,6 +26,5 @@ use-rustls = ["electrum-client/use-rustls"] use-rustls-ring = ["electrum-client/use-rustls-ring"] [[test]] -name = "use-rustls" -path = "tests/test_electrum.rs" +name = "test_electrum" required-features = ["use-rustls"] diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index e7190f4ec..fa799d811 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -39,11 +39,9 @@ blocking-https-rustls = ["blocking", "esplora-client/blocking-https-rustls"] blocking-https-native = ["blocking", "esplora-client/blocking-https-native"] [[test]] -name = "blocking" -path = "tests/blocking_ext.rs" +name = "blocking_ext" required-features = ["blocking"] [[test]] -name = "async" -path = "tests/async_ext.rs" +name = "async_ext" required-features = ["async"] From 519728cd596c3bda60c6e70cbc12b611017e2d7f Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 12 Sep 2024 12:01:36 -0300 Subject: [PATCH 66/77] chore(examples)!: update all examples to have `example_` prefix --- Cargo.toml | 8 ++++---- .../Cargo.toml | 2 +- .../src/main.rs | 0 .../Cargo.toml | 2 +- .../src/main.rs | 0 .../Cargo.toml | 2 +- .../src/main.rs | 0 .../{wallet_rpc => example_wallet_rpc}/Cargo.toml | 2 +- .../{wallet_rpc => example_wallet_rpc}/README.md | 6 +++--- .../{wallet_rpc => example_wallet_rpc}/src/main.rs | 0 10 files changed, 11 insertions(+), 11 deletions(-) rename example-crates/{wallet_electrum => example_wallet_electrum}/Cargo.toml (85%) rename example-crates/{wallet_electrum => example_wallet_electrum}/src/main.rs (100%) rename example-crates/{wallet_esplora_async => example_wallet_esplora_async}/Cargo.toml (91%) rename example-crates/{wallet_esplora_async => example_wallet_esplora_async}/src/main.rs (100%) rename example-crates/{wallet_esplora_blocking => example_wallet_esplora_blocking}/Cargo.toml (89%) rename example-crates/{wallet_esplora_blocking => example_wallet_esplora_blocking}/src/main.rs (100%) rename example-crates/{wallet_rpc => example_wallet_rpc}/Cargo.toml (93%) rename example-crates/{wallet_rpc => example_wallet_rpc}/README.md (88%) rename example-crates/{wallet_rpc => example_wallet_rpc}/src/main.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index a6e6eb6e6..2abc16bd8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,10 @@ members = [ "example-crates/example_electrum", "example-crates/example_esplora", "example-crates/example_bitcoind_rpc_polling", - "example-crates/wallet_electrum", - "example-crates/wallet_esplora_blocking", - "example-crates/wallet_esplora_async", - "example-crates/wallet_rpc", + "example-crates/example_wallet_electrum", + "example-crates/example_wallet_esplora_blocking", + "example-crates/example_wallet_esplora_async", + "example-crates/example_wallet_rpc", ] [workspace.package] diff --git a/example-crates/wallet_electrum/Cargo.toml b/example-crates/example_wallet_electrum/Cargo.toml similarity index 85% rename from example-crates/wallet_electrum/Cargo.toml rename to example-crates/example_wallet_electrum/Cargo.toml index 10b662e8f..07ba60d50 100644 --- a/example-crates/wallet_electrum/Cargo.toml +++ b/example-crates/example_wallet_electrum/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wallet_electrum_example" +name = "example_wallet_electrum" version = "0.2.0" edition = "2021" diff --git a/example-crates/wallet_electrum/src/main.rs b/example-crates/example_wallet_electrum/src/main.rs similarity index 100% rename from example-crates/wallet_electrum/src/main.rs rename to example-crates/example_wallet_electrum/src/main.rs diff --git a/example-crates/wallet_esplora_async/Cargo.toml b/example-crates/example_wallet_esplora_async/Cargo.toml similarity index 91% rename from example-crates/wallet_esplora_async/Cargo.toml rename to example-crates/example_wallet_esplora_async/Cargo.toml index aa18a5e9d..2121b72e9 100644 --- a/example-crates/wallet_esplora_async/Cargo.toml +++ b/example-crates/example_wallet_esplora_async/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wallet_esplora_async" +name = "example_wallet_esplora_async" version = "0.2.0" edition = "2021" diff --git a/example-crates/wallet_esplora_async/src/main.rs b/example-crates/example_wallet_esplora_async/src/main.rs similarity index 100% rename from example-crates/wallet_esplora_async/src/main.rs rename to example-crates/example_wallet_esplora_async/src/main.rs diff --git a/example-crates/wallet_esplora_blocking/Cargo.toml b/example-crates/example_wallet_esplora_blocking/Cargo.toml similarity index 89% rename from example-crates/wallet_esplora_blocking/Cargo.toml rename to example-crates/example_wallet_esplora_blocking/Cargo.toml index 4228c9837..f47d040db 100644 --- a/example-crates/wallet_esplora_blocking/Cargo.toml +++ b/example-crates/example_wallet_esplora_blocking/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wallet_esplora_blocking" +name = "example_wallet_esplora_blocking" version = "0.2.0" edition = "2021" publish = false diff --git a/example-crates/wallet_esplora_blocking/src/main.rs b/example-crates/example_wallet_esplora_blocking/src/main.rs similarity index 100% rename from example-crates/wallet_esplora_blocking/src/main.rs rename to example-crates/example_wallet_esplora_blocking/src/main.rs diff --git a/example-crates/wallet_rpc/Cargo.toml b/example-crates/example_wallet_rpc/Cargo.toml similarity index 93% rename from example-crates/wallet_rpc/Cargo.toml rename to example-crates/example_wallet_rpc/Cargo.toml index ffda1d3ef..558f43fed 100644 --- a/example-crates/wallet_rpc/Cargo.toml +++ b/example-crates/example_wallet_rpc/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "wallet_rpc" +name = "example_wallet_rpc" version = "0.1.0" edition = "2021" diff --git a/example-crates/wallet_rpc/README.md b/example-crates/example_wallet_rpc/README.md similarity index 88% rename from example-crates/wallet_rpc/README.md rename to example-crates/example_wallet_rpc/README.md index 28eb07b1f..ea2918aa2 100644 --- a/example-crates/wallet_rpc/README.md +++ b/example-crates/example_wallet_rpc/README.md @@ -1,13 +1,13 @@ # Wallet RPC Example ``` -$ cargo run --bin wallet_rpc -- --help +$ cargo run --bin example_wallet_rpc -- --help -wallet_rpc 0.1.0 +example_wallet_rpc 0.1.0 Bitcoind RPC example using `bdk_wallet::Wallet` USAGE: - wallet_rpc [OPTIONS] [CHANGE_DESCRIPTOR] + example_wallet_rpc [OPTIONS] [CHANGE_DESCRIPTOR] ARGS: Wallet descriptor [env: DESCRIPTOR=] diff --git a/example-crates/wallet_rpc/src/main.rs b/example-crates/example_wallet_rpc/src/main.rs similarity index 100% rename from example-crates/wallet_rpc/src/main.rs rename to example-crates/example_wallet_rpc/src/main.rs From 45be3172a4abaa94ef2ff55784da31832bdd732a Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 12 Sep 2024 12:06:20 -0300 Subject: [PATCH 67/77] refactor(ci)!: update CI to build and test example in specific job --- .github/workflows/cont_integration.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 6fa15c1f3..76e2795e4 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -50,9 +50,9 @@ jobs: cargo update -p tokio --precise "1.38.1" cargo update -p tokio-util --precise "0.7.11" - name: Build - run: cargo build ${{ matrix.features }} + run: cargo build --workspace --exclude 'example_*' ${{ matrix.features }} - name: Test - run: cargo test ${{ matrix.features }} + run: cargo test --workspace --exclude 'example_*' ${{ matrix.features }} check-no-std: name: Check no_std @@ -145,7 +145,7 @@ jobs: args: --all-features --all-targets -- -D warnings build-examples: - name: Build Examples + name: Build & Test Examples runs-on: ubuntu-latest strategy: matrix: @@ -154,10 +154,10 @@ jobs: - example_bitcoind_rpc_polling - example_electrum - example_esplora - - wallet_electrum - - wallet_esplora_async - - wallet_esplora_blocking - - wallet_rpc + - example_wallet_electrum + - example_wallet_esplora_async + - example_wallet_esplora_blocking + - example_wallet_rpc steps: - name: checkout uses: actions/checkout@v2 From d802d00a06035bec582eb5aadf0281f26bff6acd Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 12 Sep 2024 12:08:35 -0300 Subject: [PATCH 68/77] fix(RUSTSEC-2024-0370)!: bump `clap` to latest, removing transitive dependency on `proc-macro-error`. In #1593 it's mentioned that `proc-macro-error` is unmaintained for the past few years, with no fix other than using proc-macro-error2 instead. As on our scenario it's merely a transitive dependency of `clap`, through `clap_derive` feature, which in latest releases doesn't depend on `proc-macro-error` we can just bump it to latest. It's valid to note that by bumping it, both examples that relies on clap are no longer MSRV (1.63) compliant. --- example-crates/example_cli/Cargo.toml | 2 +- example-crates/example_wallet_rpc/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example-crates/example_cli/Cargo.toml b/example-crates/example_cli/Cargo.toml index 09f093ebf..290908a6a 100644 --- a/example-crates/example_cli/Cargo.toml +++ b/example-crates/example_cli/Cargo.toml @@ -12,7 +12,7 @@ bdk_file_store = { path = "../../crates/file_store" } bitcoin = { version = "0.32.0", features = ["base64"], default-features = false } anyhow = "1" -clap = { version = "3.2.23", features = ["derive", "env"] } +clap = { version = "4.5.17", features = ["derive", "env"] } rand = "0.8" serde = { version = "1", features = ["derive"] } serde_json = "1.0" diff --git a/example-crates/example_wallet_rpc/Cargo.toml b/example-crates/example_wallet_rpc/Cargo.toml index 558f43fed..15321a82e 100644 --- a/example-crates/example_wallet_rpc/Cargo.toml +++ b/example-crates/example_wallet_rpc/Cargo.toml @@ -10,5 +10,5 @@ bdk_wallet = { path = "../../crates/wallet", features = ["file_store"] } bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" } anyhow = "1" -clap = { version = "3.2.25", features = ["derive", "env"] } +clap = { version = "4.5.17", features = ["derive", "env"] } ctrlc = "2.0.1" From d7dfe38ff6c0f1b984e15defc83c79e52a44afa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 26 Sep 2024 16:28:38 +0800 Subject: [PATCH 69/77] fix!(wallet): `ChangeSet` should not be non-exhaustive As stated in #1591, it is handy to know when new values are added to `ChangeSet`. --- crates/wallet/src/wallet/changeset.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/wallet/src/wallet/changeset.rs b/crates/wallet/src/wallet/changeset.rs index 2d4b700ed..9c75a289d 100644 --- a/crates/wallet/src/wallet/changeset.rs +++ b/crates/wallet/src/wallet/changeset.rs @@ -8,7 +8,6 @@ type IndexedTxGraphChangeSet = /// A changeset for [`Wallet`](crate::Wallet). #[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)] -#[non_exhaustive] pub struct ChangeSet { /// Descriptor for recipient addresses. pub descriptor: Option>, From 993c4c055354ba6ddb0d81e8e5dbfa1f9bb631ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 26 Sep 2024 17:42:58 +0800 Subject: [PATCH 70/77] feat(chain,core)!: move `Merge` to `bdk_core` --- crates/chain/src/tx_data_traits.rs | 82 ------------------------------ crates/core/src/lib.rs | 3 ++ crates/core/src/merge.rs | 82 ++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 82 deletions(-) create mode 100644 crates/core/src/merge.rs diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index d3d562bf3..2ca314d27 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -1,6 +1,4 @@ -use crate::collections::{BTreeMap, BTreeSet}; use crate::{BlockId, ConfirmationBlockTime}; -use alloc::vec::Vec; /// Trait that "anchors" blockchain data to a specific block of height and hash. /// @@ -121,83 +119,3 @@ impl AnchorFromBlockPosition for ConfirmationBlockTime { } } } - -/// Trait that makes an object mergeable. -pub trait Merge: Default { - /// Merge another object of the same type onto `self`. - fn merge(&mut self, other: Self); - - /// Returns whether the structure is considered empty. - fn is_empty(&self) -> bool; - - /// Take the value, replacing it with the default value. - fn take(&mut self) -> Option { - if self.is_empty() { - None - } else { - Some(core::mem::take(self)) - } - } -} - -impl Merge for BTreeMap { - fn merge(&mut self, other: Self) { - // We use `extend` instead of `BTreeMap::append` due to performance issues with `append`. - // Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420 - BTreeMap::extend(self, other) - } - - fn is_empty(&self) -> bool { - BTreeMap::is_empty(self) - } -} - -impl Merge for BTreeSet { - fn merge(&mut self, other: Self) { - // We use `extend` instead of `BTreeMap::append` due to performance issues with `append`. - // Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420 - BTreeSet::extend(self, other) - } - - fn is_empty(&self) -> bool { - BTreeSet::is_empty(self) - } -} - -impl Merge for Vec { - fn merge(&mut self, mut other: Self) { - Vec::append(self, &mut other) - } - - fn is_empty(&self) -> bool { - Vec::is_empty(self) - } -} - -macro_rules! impl_merge_for_tuple { - ($($a:ident $b:tt)*) => { - impl<$($a),*> Merge for ($($a,)*) where $($a: Merge),* { - - fn merge(&mut self, _other: Self) { - $(Merge::merge(&mut self.$b, _other.$b) );* - } - - fn is_empty(&self) -> bool { - $(Merge::is_empty(&self.$b) && )* true - } - } - } -} - -impl_merge_for_tuple!(); -impl_merge_for_tuple!(T0 0); -impl_merge_for_tuple!(T0 0 T1 1); -impl_merge_for_tuple!(T0 0 T1 1 T2 2); -impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3); -impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4); -impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5); -impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6); -impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7); -impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8); -impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9); -impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9 T10 10); diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index aeb34dca3..95bebe907 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -68,4 +68,7 @@ pub use checkpoint::*; mod tx_update; pub use tx_update::*; +mod merge; +pub use merge::*; + pub mod spk_client; diff --git a/crates/core/src/merge.rs b/crates/core/src/merge.rs new file mode 100644 index 000000000..59c0d7f42 --- /dev/null +++ b/crates/core/src/merge.rs @@ -0,0 +1,82 @@ +use crate::alloc::vec::Vec; +use crate::collections::{BTreeMap, BTreeSet}; + +/// Trait that makes an object mergeable. +pub trait Merge: Default { + /// Merge another object of the same type onto `self`. + fn merge(&mut self, other: Self); + + /// Returns whether the structure is considered empty. + fn is_empty(&self) -> bool; + + /// Take the value, replacing it with the default value. + fn take(&mut self) -> Option { + if self.is_empty() { + None + } else { + Some(core::mem::take(self)) + } + } +} + +impl Merge for BTreeMap { + fn merge(&mut self, other: Self) { + // We use `extend` instead of `BTreeMap::append` due to performance issues with `append`. + // Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420 + BTreeMap::extend(self, other) + } + + fn is_empty(&self) -> bool { + BTreeMap::is_empty(self) + } +} + +impl Merge for BTreeSet { + fn merge(&mut self, other: Self) { + // We use `extend` instead of `BTreeMap::append` due to performance issues with `append`. + // Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420 + BTreeSet::extend(self, other) + } + + fn is_empty(&self) -> bool { + BTreeSet::is_empty(self) + } +} + +impl Merge for Vec { + fn merge(&mut self, mut other: Self) { + Vec::append(self, &mut other) + } + + fn is_empty(&self) -> bool { + Vec::is_empty(self) + } +} + +macro_rules! impl_merge_for_tuple { + ($($a:ident $b:tt)*) => { + impl<$($a),*> Merge for ($($a,)*) where $($a: Merge),* { + + fn merge(&mut self, _other: Self) { + $(Merge::merge(&mut self.$b, _other.$b) );* + } + + fn is_empty(&self) -> bool { + $(Merge::is_empty(&self.$b) && )* true + } + } + } +} + +impl_merge_for_tuple!(); +impl_merge_for_tuple!(T0 0); +impl_merge_for_tuple!(T0 0 T1 1); +impl_merge_for_tuple!(T0 0 T1 1 T2 2); +impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3); +impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4); +impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5); +impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6); +impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7); +impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8); +impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9); +impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9 T10 10); From a4cf905d75ad7e09730b6d0aab5ef6cd2e735c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BF=97=E5=AE=87?= Date: Thu, 26 Sep 2024 17:51:02 +0800 Subject: [PATCH 71/77] feat(file_store): rm `bdk_chain` dependency --- crates/file_store/Cargo.toml | 2 +- crates/file_store/src/store.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index 9c48b3dc5..c9728941d 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -14,7 +14,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_chain = { path = "../chain", version = "0.19.0", features = [ "serde", "miniscript" ] } +bdk_core = { path = "../core", version = "0.2.0", features = ["serde"]} bincode = { version = "1" } serde = { version = "1", features = ["derive"] } diff --git a/crates/file_store/src/store.rs b/crates/file_store/src/store.rs index 62c3d91b6..49ddc7731 100644 --- a/crates/file_store/src/store.rs +++ b/crates/file_store/src/store.rs @@ -1,5 +1,5 @@ use crate::{bincode_options, EntryIter, FileError, IterError}; -use bdk_chain::Merge; +use bdk_core::Merge; use bincode::Options; use std::{ fmt::{self, Debug}, From c60529436dcf82298ea923f782adefd059272cc9 Mon Sep 17 00:00:00 2001 From: valued mammal Date: Thu, 26 Sep 2024 11:38:39 -0400 Subject: [PATCH 72/77] deps(esplora): bump esplora-client to 0.10.0 --- crates/esplora/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index c13fbb17b..e95f0654a 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -16,7 +16,7 @@ workspace = true [dependencies] bdk_core = { path = "../core", version = "0.2.0", default-features = false } -esplora-client = { version = "0.9.0", default-features = false } +esplora-client = { version = "0.10.0", default-features = false } async-trait = { version = "0.1.66", optional = true } futures = { version = "0.3.26", optional = true } miniscript = { version = "12.0.0", optional = true, default-features = false } From ab8068b90ee476f8edf7d3d4a21d621818e30969 Mon Sep 17 00:00:00 2001 From: Jiri Jakes Date: Sun, 8 Sep 2024 16:05:30 +0800 Subject: [PATCH 73/77] refactor(chain)!: Replace trait `AnchorFromBlockPosition` with new struct This change replaces a way of creating new generic anchor from block, its height and transaction position. Previous way was using conversion trait, newly it is a struct and `From`. --- crates/chain/src/indexed_tx_graph.rs | 29 +++++++++++++++++--------- crates/chain/src/tx_data_traits.rs | 31 +++++++++++++++++----------- 2 files changed, 38 insertions(+), 22 deletions(-) diff --git a/crates/chain/src/indexed_tx_graph.rs b/crates/chain/src/indexed_tx_graph.rs index ed2a1f0ce..828dc5199 100644 --- a/crates/chain/src/indexed_tx_graph.rs +++ b/crates/chain/src/indexed_tx_graph.rs @@ -7,7 +7,7 @@ use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid}; use crate::{ tx_graph::{self, TxGraph}, - Anchor, AnchorFromBlockPosition, BlockId, Indexer, Merge, + Anchor, BlockId, Indexer, Merge, TxPosInBlock, }; /// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation. @@ -252,17 +252,17 @@ where } } -/// Methods are available if the anchor (`A`) implements [`AnchorFromBlockPosition`]. -impl IndexedTxGraph +/// Methods are available if the anchor (`A`) can be created from [`TxPosInBlock`]. +impl IndexedTxGraph where I::ChangeSet: Default + Merge, - A: AnchorFromBlockPosition, + for<'b> A: Anchor + From>, + I: Indexer, { /// Batch insert all transactions of the given `block` of `height`, filtering out those that are /// irrelevant. /// - /// Each inserted transaction's anchor will be constructed from - /// [`AnchorFromBlockPosition::from_block_position`]. + /// Each inserted transaction's anchor will be constructed using [`TxPosInBlock`]. /// /// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`. /// Irrelevant transactions in `txs` will be ignored. @@ -280,7 +280,12 @@ where changeset.indexer.merge(self.index.index_tx(tx)); if self.index.is_tx_relevant(tx) { let txid = tx.compute_txid(); - let anchor = A::from_block_position(block, block_id, tx_pos); + let anchor = TxPosInBlock { + block, + block_id, + tx_pos, + } + .into(); changeset.tx_graph.merge(self.graph.insert_tx(tx.clone())); changeset .tx_graph @@ -292,8 +297,7 @@ where /// Batch insert all transactions of the given `block` of `height`. /// - /// Each inserted transaction's anchor will be constructed from - /// [`AnchorFromBlockPosition::from_block_position`]. + /// Each inserted transaction's anchor will be constructed using [`TxPosInBlock`]. /// /// To only insert relevant transactions, use [`apply_block_relevant`] instead. /// @@ -305,7 +309,12 @@ where }; let mut graph = tx_graph::ChangeSet::default(); for (tx_pos, tx) in block.txdata.iter().enumerate() { - let anchor = A::from_block_position(&block, block_id, tx_pos); + let anchor = TxPosInBlock { + block: &block, + block_id, + tx_pos, + } + .into(); graph.merge(self.graph.insert_anchor(tx.compute_txid(), anchor)); graph.merge(self.graph.insert_tx(tx.clone())); } diff --git a/crates/chain/src/tx_data_traits.rs b/crates/chain/src/tx_data_traits.rs index d3d562bf3..ebf1e080a 100644 --- a/crates/chain/src/tx_data_traits.rs +++ b/crates/chain/src/tx_data_traits.rs @@ -100,24 +100,31 @@ impl Anchor for ConfirmationBlockTime { } } -/// An [`Anchor`] that can be constructed from a given block, block height and transaction position -/// within the block. -pub trait AnchorFromBlockPosition: Anchor { - /// Construct the anchor from a given `block`, block height and `tx_pos` within the block. - fn from_block_position(block: &bitcoin::Block, block_id: BlockId, tx_pos: usize) -> Self; +/// Set of parameters sufficient to construct an [`Anchor`]. +/// +/// Typically used as an additional constraint on anchor: +/// `for<'b> A: Anchor + From>`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TxPosInBlock<'b> { + /// Block in which the transaction appeared. + pub block: &'b bitcoin::Block, + /// Block's [`BlockId`]. + pub block_id: BlockId, + /// Position in the block on which the transaction appeared. + pub tx_pos: usize, } -impl AnchorFromBlockPosition for BlockId { - fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { - block_id +impl<'b> From> for BlockId { + fn from(pos: TxPosInBlock) -> Self { + pos.block_id } } -impl AnchorFromBlockPosition for ConfirmationBlockTime { - fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self { +impl<'b> From> for ConfirmationBlockTime { + fn from(pos: TxPosInBlock) -> Self { Self { - block_id, - confirmation_time: block.header.time as _, + block_id: pos.block_id, + confirmation_time: pos.block.header.time as _, } } } From f15551b56fb4eb91bc0be5f940950c40ac465084 Mon Sep 17 00:00:00 2001 From: Luis Schwab Date: Tue, 17 Sep 2024 14:04:01 -0300 Subject: [PATCH 74/77] feat(wallet)!: enable RBF by default on TxBuilder --- crates/wallet/README.md | 1 - crates/wallet/src/wallet/error.rs | 11 +- crates/wallet/src/wallet/mod.rs | 43 ++---- crates/wallet/src/wallet/tx_builder.rs | 48 ++----- crates/wallet/src/wallet/utils.rs | 10 +- crates/wallet/tests/wallet.rs | 129 +++++++----------- .../example_wallet_electrum/src/main.rs | 4 +- .../example_wallet_esplora_async/src/main.rs | 4 +- .../src/main.rs | 4 +- 9 files changed, 81 insertions(+), 173 deletions(-) diff --git a/crates/wallet/README.md b/crates/wallet/README.md index 3b5422b63..937176e04 100644 --- a/crates/wallet/README.md +++ b/crates/wallet/README.md @@ -174,7 +174,6 @@ println!("Your new receive address is: {}", receive_address.address); - diff --git a/crates/wallet/src/wallet/error.rs b/crates/wallet/src/wallet/error.rs index 2264aac9e..adce5b188 100644 --- a/crates/wallet/src/wallet/error.rs +++ b/crates/wallet/src/wallet/error.rs @@ -65,12 +65,10 @@ pub enum CreateTxError { /// Required `LockTime` required: absolute::LockTime, }, - /// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE - RbfSequence, /// Cannot enable RBF with `Sequence` given a required OP_CSV RbfSequenceCsv { /// Given RBF `Sequence` - rbf: Sequence, + sequence: Sequence, /// Required OP_CSV `Sequence` csv: Sequence, }, @@ -131,14 +129,11 @@ impl fmt::Display for CreateTxError { } => { write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested) } - CreateTxError::RbfSequence => { - write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE") - } - CreateTxError::RbfSequenceCsv { rbf, csv } => { + CreateTxError::RbfSequenceCsv { sequence, csv } => { write!( f, "Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`", - rbf, csv + sequence, csv ) } CreateTxError::FeeTooLow { required } => { diff --git a/crates/wallet/src/wallet/mod.rs b/crates/wallet/src/wallet/mod.rs index 1a25e7d7d..19604f441 100644 --- a/crates/wallet/src/wallet/mod.rs +++ b/crates/wallet/src/wallet/mod.rs @@ -1366,36 +1366,20 @@ impl Wallet { } }; - // The nSequence to be by default for inputs unless an explicit sequence is specified. - let n_sequence = match (params.rbf, requirements.csv) { - // No RBF or CSV but there's an nLockTime, so the nSequence cannot be final - (None, None) if lock_time != absolute::LockTime::ZERO => { - Sequence::ENABLE_LOCKTIME_NO_RBF - } - // No RBF, CSV or nLockTime, make the transaction final - (None, None) => Sequence::MAX, - - // No RBF requested, use the value from CSV. Note that this value is by definition - // non-final, so even if a timelock is enabled this nSequence is fine, hence why we - // don't bother checking for it here. The same is true for all the other branches below + // nSequence value for inputs + // When not explicitly specified, defaults to 0xFFFFFFFD, + // meaning RBF signaling is enabled + let n_sequence = match (params.sequence, requirements.csv) { + // Enable RBF by default + (None, None) => Sequence::ENABLE_RBF_NO_LOCKTIME, + // None requested, use required (None, Some(csv)) => csv, - - // RBF with a specific value but that value is too high - (Some(tx_builder::RbfValue::Value(rbf)), _) if !rbf.is_rbf() => { - return Err(CreateTxError::RbfSequence) + // Requested sequence is incompatible with requirements + (Some(sequence), Some(csv)) if !check_nsequence_rbf(sequence, csv) => { + return Err(CreateTxError::RbfSequenceCsv { sequence, csv }) } - // RBF with a specific value requested, but the value is incompatible with CSV - (Some(tx_builder::RbfValue::Value(rbf)), Some(csv)) - if !check_nsequence_rbf(rbf, csv) => - { - return Err(CreateTxError::RbfSequenceCsv { rbf, csv }) - } - - // RBF enabled with the default value with CSV also enabled. CSV takes precedence - (Some(tx_builder::RbfValue::Default), Some(csv)) => csv, - // Valid RBF, either default or with a specific value. We ignore the `CSV` value - // because we've already checked it before - (Some(rbf), _) => rbf.get_value(), + // Use requested nSequence value + (Some(sequence), _) => sequence, }; let (fee_rate, mut fee_amount) = match params.fee_policy.unwrap_or_default() { @@ -1609,8 +1593,7 @@ impl Wallet { /// let mut psbt = { /// let mut builder = wallet.build_tx(); /// builder - /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)) - /// .enable_rbf(); + /// .add_recipient(to_address.script_pubkey(), Amount::from_sat(50_000)); /// builder.finish()? /// }; /// let _ = wallet.sign(&mut psbt, SignOptions::default())?; diff --git a/crates/wallet/src/wallet/tx_builder.rs b/crates/wallet/src/wallet/tx_builder.rs index 08b0f3249..c3dde7330 100644 --- a/crates/wallet/src/wallet/tx_builder.rs +++ b/crates/wallet/src/wallet/tx_builder.rs @@ -31,9 +31,7 @@ //! // With a custom fee rate of 5.0 satoshi/vbyte //! .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")) //! // Only spend non-change outputs -//! .do_not_spend_change() -//! // Turn on RBF signaling -//! .enable_rbf(); +//! .do_not_spend_change(); //! let psbt = tx_builder.finish()?; //! # Ok::<(), anyhow::Error>(()) //! ``` @@ -134,7 +132,7 @@ pub(crate) struct TxParams { pub(crate) sighash: Option, pub(crate) ordering: TxOrdering, pub(crate) locktime: Option, - pub(crate) rbf: Option, + pub(crate) sequence: Option, pub(crate) version: Option, pub(crate) change_policy: ChangeSpendPolicy, pub(crate) only_witness_utxo: bool, @@ -554,23 +552,12 @@ impl<'a, Cs> TxBuilder<'a, Cs> { } } - /// Enable signaling RBF + /// Set an exact nSequence value /// - /// This will use the default nSequence value of `0xFFFFFFFD`. - pub fn enable_rbf(&mut self) -> &mut Self { - self.params.rbf = Some(RbfValue::Default); - self - } - - /// Enable signaling RBF with a specific nSequence value - /// - /// This can cause conflicts if the wallet's descriptors contain an "older" (OP_CSV) operator - /// and the given `nsequence` is lower than the CSV value. - /// - /// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not - /// be a valid nSequence to signal RBF. - pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self { - self.params.rbf = Some(RbfValue::Value(nsequence)); + /// This can cause conflicts if the wallet's descriptors contain an + /// "older" (OP_CSV) operator and the given `nsequence` is lower than the CSV value. + pub fn set_exact_sequence(&mut self, n_sequence: Sequence) -> &mut Self { + self.params.sequence = Some(n_sequence); self } @@ -654,8 +641,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> { /// .drain_wallet() /// // Send the excess (which is all the coins minus the fee) to this address. /// .drain_to(to_address.script_pubkey()) - /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")) - /// .enable_rbf(); + /// .fee_rate(FeeRate::from_sat_per_vb(5).expect("valid feerate")); /// let psbt = tx_builder.finish()?; /// # Ok::<(), anyhow::Error>(()) /// ``` @@ -835,24 +821,6 @@ impl Default for Version { } } -/// RBF nSequence value -/// -/// Has a default value of `0xFFFFFFFD` -#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] -pub(crate) enum RbfValue { - Default, - Value(Sequence), -} - -impl RbfValue { - pub(crate) fn get_value(&self) -> Sequence { - match self { - RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME, - RbfValue::Value(v) => *v, - } - } -} - /// Policy regarding the use of change outputs when creating a transaction #[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)] pub enum ChangeSpendPolicy { diff --git a/crates/wallet/src/wallet/utils.rs b/crates/wallet/src/wallet/utils.rs index bca07b48a..76d0aaa75 100644 --- a/crates/wallet/src/wallet/utils.rs +++ b/crates/wallet/src/wallet/utils.rs @@ -52,20 +52,20 @@ impl After { } } -pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool { - // The RBF value must enable relative timelocks - if !rbf.is_relative_lock_time() { +pub(crate) fn check_nsequence_rbf(sequence: Sequence, csv: Sequence) -> bool { + // The nSequence value must enable relative timelocks + if !sequence.is_relative_lock_time() { return false; } // Both values should be represented in the same unit (either time-based or // block-height based) - if rbf.is_time_locked() != csv.is_time_locked() { + if sequence.is_time_locked() != csv.is_time_locked() { return false; } // The value should be at least `csv` - if rbf < csv { + if sequence < csv { return false; } diff --git a/crates/wallet/tests/wallet.rs b/crates/wallet/tests/wallet.rs index 637593780..5914ce289 100644 --- a/crates/wallet/tests/wallet.rs +++ b/crates/wallet/tests/wallet.rs @@ -639,63 +639,65 @@ fn test_create_tx_custom_locktime_incompatible_with_cltv() { } #[test] -fn test_create_tx_no_rbf_csv() { +fn test_create_tx_custom_csv() { + // desc: wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6))) let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + builder + .set_exact_sequence(Sequence(42)) + .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); + // we allow setting a sequence higher than required + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(42)); } #[test] -fn test_create_tx_with_default_rbf_csv() { +fn test_create_tx_no_rbf_csv() { let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let psbt = builder.finish().unwrap(); - // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). - // It will be set to the OP_CSV value, in this case 6 + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); } #[test] -fn test_create_tx_with_custom_rbf_csv() { +fn test_create_tx_incompatible_csv() { let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf_with_sequence(Sequence(3)); + .set_exact_sequence(Sequence(3)); assert!(matches!(builder.finish(), - Err(CreateTxError::RbfSequenceCsv { rbf, csv }) - if rbf.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6)); + Err(CreateTxError::RbfSequenceCsv { sequence, csv }) + if sequence.to_consensus_u32() == 3 && csv.to_consensus_u32() == 6)); } #[test] -fn test_create_tx_no_rbf_cltv() { - let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); +fn test_create_tx_with_default_rbf_csv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_csv()); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let psbt = builder.finish().unwrap(); - - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); + // When CSV is enabled it takes precedence over the rbf value (unless forced by the user). + // It will be set to the OP_CSV value, in this case 6 + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(6)); } #[test] -fn test_create_tx_invalid_rbf_sequence() { - let (mut wallet, _) = get_funded_wallet_wpkh(); +fn test_create_tx_no_rbf_cltv() { + let (mut wallet, _) = get_funded_wallet(get_test_single_sig_cltv()); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf_with_sequence(Sequence(0xFFFFFFFE)); - assert!(matches!(builder.finish(), Err(CreateTxError::RbfSequence))); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + builder.set_exact_sequence(Sequence(0xFFFFFFFE)); + let psbt = builder.finish().unwrap(); + + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); } #[test] @@ -705,7 +707,7 @@ fn test_create_tx_custom_rbf_sequence() { let mut builder = wallet.build_tx(); builder .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf_with_sequence(Sequence(0xDEADBEEF)); + .set_exact_sequence(Sequence(0xDEADBEEF)); let psbt = builder.finish().unwrap(); assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xDEADBEEF)); @@ -743,7 +745,7 @@ fn test_create_tx_default_sequence() { builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let psbt = builder.finish().unwrap(); - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); } macro_rules! check_fee { @@ -1328,7 +1330,7 @@ fn test_create_tx_policy_path_no_csv() { .policy_path(path, KeychainKind::External); let psbt = builder.finish().unwrap(); - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFF)); + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); } #[test] @@ -1370,7 +1372,7 @@ fn test_create_tx_policy_path_ignored_subtree_with_csv() { .policy_path(path, KeychainKind::External); let psbt = builder.finish().unwrap(); - assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFE)); + assert_eq!(psbt.unsigned_tx.input[0].sequence, Sequence(0xFFFFFFFD)); } #[test] @@ -1797,6 +1799,7 @@ fn test_bump_fee_irreplaceable_tx() { let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + builder.set_exact_sequence(Sequence(0xFFFFFFFE)); let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx().expect("failed to extract tx"); @@ -1836,9 +1839,8 @@ fn test_bump_fee_low_fee_rate() { let (mut wallet, _) = get_funded_wallet_wpkh(); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); + let psbt = builder.finish().unwrap(); let feerate = psbt.fee_rate().unwrap(); @@ -1869,9 +1871,7 @@ fn test_bump_fee_low_abs() { let (mut wallet, _) = get_funded_wallet_wpkh(); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx().expect("failed to extract tx"); @@ -1891,9 +1891,7 @@ fn test_bump_fee_zero_abs() { let (mut wallet, _) = get_funded_wallet_wpkh(); let addr = wallet.next_unused_address(KeychainKind::External); let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx().expect("failed to extract tx"); @@ -1913,9 +1911,7 @@ fn test_bump_fee_reduce_change() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx(); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(25_000)); let psbt = builder.finish().unwrap(); let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); @@ -1928,7 +1924,7 @@ fn test_bump_fee_reduce_change() { let feerate = FeeRate::from_sat_per_kwu(625); // 2.5 sat/vb let mut builder = wallet.build_fee_bump(txid).unwrap(); - builder.fee_rate(feerate).enable_rbf(); + builder.fee_rate(feerate); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); @@ -1964,7 +1960,6 @@ fn test_bump_fee_reduce_change() { let mut builder = wallet.build_fee_bump(txid).unwrap(); builder.fee_absolute(Amount::from_sat(200)); - builder.enable_rbf(); let psbt = builder.finish().unwrap(); let sent_received = wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); @@ -2011,10 +2006,7 @@ fn test_bump_fee_reduce_single_recipient() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .enable_rbf(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); let psbt = builder.finish().unwrap(); let tx = psbt.clone().extract_tx().expect("failed to extract tx"); let original_sent_received = wallet.sent_and_received(&tx); @@ -2058,10 +2050,7 @@ fn test_bump_fee_absolute_reduce_single_recipient() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx(); - builder - .drain_to(addr.script_pubkey()) - .drain_wallet() - .enable_rbf(); + builder.drain_to(addr.script_pubkey()).drain_wallet(); let psbt = builder.finish().unwrap(); let original_fee = check_fee!(wallet, psbt); let tx = psbt.extract_tx().expect("failed to extract tx"); @@ -2135,8 +2124,7 @@ fn test_bump_fee_drain_wallet() { vout: 0, }) .unwrap() - .manually_selected_only() - .enable_rbf(); + .manually_selected_only(); let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx().expect("failed to extract tx"); let original_sent_received = wallet.sent_and_received(&tx); @@ -2201,8 +2189,7 @@ fn test_bump_fee_remove_output_manually_selected_only() { .drain_to(addr.script_pubkey()) .add_utxo(outpoint) .unwrap() - .manually_selected_only() - .enable_rbf(); + .manually_selected_only(); let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx().expect("failed to extract tx"); let original_sent_received = wallet.sent_and_received(&tx); @@ -2247,9 +2234,7 @@ fn test_bump_fee_add_input() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx().expect("failed to extract tx"); let original_details = wallet.sent_and_received(&tx); @@ -2303,9 +2288,7 @@ fn test_bump_fee_absolute_add_input() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); let psbt = builder.finish().unwrap(); let tx = psbt.extract_tx().expect("failed to extract tx"); let original_sent_received = wallet.sent_and_received(&tx); @@ -2366,8 +2349,7 @@ fn test_bump_fee_no_change_add_input_and_change() { .drain_to(addr.script_pubkey()) .add_utxo(op) .unwrap() - .manually_selected_only() - .enable_rbf(); + .manually_selected_only(); let psbt = builder.finish().unwrap(); let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); @@ -2428,9 +2410,7 @@ fn test_bump_fee_add_input_change_dust() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); let psbt = builder.finish().unwrap(); let original_sent_received = wallet.sent_and_received(&psbt.clone().extract_tx().expect("failed to extract tx")); @@ -2504,9 +2484,7 @@ fn test_bump_fee_force_add_input() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); let psbt = builder.finish().unwrap(); let mut tx = psbt.extract_tx().expect("failed to extract tx"); let original_sent_received = wallet.sent_and_received(&tx); @@ -2569,9 +2547,7 @@ fn test_bump_fee_absolute_force_add_input() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx().coin_selection(LargestFirstCoinSelection); - builder - .add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)) - .enable_rbf(); + builder.add_recipient(addr.script_pubkey(), Amount::from_sat(45_000)); let psbt = builder.finish().unwrap(); let mut tx = psbt.extract_tx().expect("failed to extract tx"); let original_sent_received = wallet.sent_and_received(&tx); @@ -2641,10 +2617,7 @@ fn test_bump_fee_unconfirmed_inputs_only() { .unwrap() .assume_checked(); let mut builder = wallet.build_tx(); - builder - .drain_wallet() - .drain_to(addr.script_pubkey()) - .enable_rbf(); + builder.drain_wallet().drain_to(addr.script_pubkey()); let psbt = builder.finish().unwrap(); // Now we receive one transaction with 0 confirmations. We won't be able to use that for // fee bumping, as it's still unconfirmed! @@ -2680,10 +2653,7 @@ fn test_bump_fee_unconfirmed_input() { // in the drain tx. receive_output(&mut wallet, 25_000, ConfirmationTime::unconfirmed(0)); let mut builder = wallet.build_tx(); - builder - .drain_wallet() - .drain_to(addr.script_pubkey()) - .enable_rbf(); + builder.drain_wallet().drain_to(addr.script_pubkey()); let psbt = builder.finish().unwrap(); let mut tx = psbt.extract_tx().expect("failed to extract tx"); let txid = tx.compute_txid(); @@ -2727,7 +2697,6 @@ fn test_fee_amount_negative_drain_val() { .add_recipient(send_to.script_pubkey(), Amount::from_sat(8630)) .add_utxo(incoming_op) .unwrap() - .enable_rbf() .fee_rate(fee_rate); let psbt = builder.finish().unwrap(); let fee = check_fee!(wallet, psbt); diff --git a/example-crates/example_wallet_electrum/src/main.rs b/example-crates/example_wallet_electrum/src/main.rs index f3320d821..5942714ef 100644 --- a/example-crates/example_wallet_electrum/src/main.rs +++ b/example-crates/example_wallet_electrum/src/main.rs @@ -82,9 +82,7 @@ fn main() -> Result<(), anyhow::Error> { } let mut tx_builder = wallet.build_tx(); - tx_builder - .add_recipient(address.script_pubkey(), SEND_AMOUNT) - .enable_rbf(); + tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT); let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; diff --git a/example-crates/example_wallet_esplora_async/src/main.rs b/example-crates/example_wallet_esplora_async/src/main.rs index 4133982c6..b6ab5d6dc 100644 --- a/example-crates/example_wallet_esplora_async/src/main.rs +++ b/example-crates/example_wallet_esplora_async/src/main.rs @@ -77,9 +77,7 @@ async fn main() -> Result<(), anyhow::Error> { } let mut tx_builder = wallet.build_tx(); - tx_builder - .add_recipient(address.script_pubkey(), SEND_AMOUNT) - .enable_rbf(); + tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT); let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; diff --git a/example-crates/example_wallet_esplora_blocking/src/main.rs b/example-crates/example_wallet_esplora_blocking/src/main.rs index d12dbd926..7966f19f5 100644 --- a/example-crates/example_wallet_esplora_blocking/src/main.rs +++ b/example-crates/example_wallet_esplora_blocking/src/main.rs @@ -77,9 +77,7 @@ fn main() -> Result<(), anyhow::Error> { } let mut tx_builder = wallet.build_tx(); - tx_builder - .add_recipient(address.script_pubkey(), SEND_AMOUNT) - .enable_rbf(); + tx_builder.add_recipient(address.script_pubkey(), SEND_AMOUNT); let mut psbt = tx_builder.finish()?; let finalized = wallet.sign(&mut psbt, SignOptions::default())?; From 0e8082437401eccdef30737e739381d295530547 Mon Sep 17 00:00:00 2001 From: Steve Myers Date: Tue, 1 Oct 2024 14:33:27 -0500 Subject: [PATCH 75/77] ci: fix build-test job with --no-default-features, add miniscript/no-std Until rust-miniscript removes the no-std feature we need to enable it when --no-default-features is used to build bdk_wallet or the whole workspace. See also the check-no-std job which does the same plus enables the bdk_chain/hashbrown feature which is also needed to build bdk_wallet with --no-default-features but is already enabled when building the whole workspace. --- .github/workflows/cont_integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cont_integration.yml b/.github/workflows/cont_integration.yml index 76e2795e4..43b041d88 100644 --- a/.github/workflows/cont_integration.yml +++ b/.github/workflows/cont_integration.yml @@ -25,7 +25,7 @@ jobs: clippy: true - version: 1.63.0 # MSRV features: - - --no-default-features + - --no-default-features --features miniscript/no-std - --all-features steps: - name: checkout From f602d1bfe24783449a88d5e82ab29f305cd2a02c Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Thu, 19 Sep 2024 23:06:28 -0300 Subject: [PATCH 76/77] feat(bdk_electrum): add `use-openssl` as a feature --- crates/electrum/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index ed1fd06a0..808586a13 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -24,6 +24,7 @@ bdk_chain = { path = "../chain" } default = ["use-rustls"] use-rustls = ["electrum-client/use-rustls"] use-rustls-ring = ["electrum-client/use-rustls-ring"] +use-openssl = ["electrum-client/use-openssl"] [[test]] name = "test_electrum" From b5e8e6b1ceed316a84bcc2660442cf0ba7bac7eb Mon Sep 17 00:00:00 2001 From: valued mammal Date: Wed, 2 Oct 2024 10:25:15 -0400 Subject: [PATCH 77/77] Bump bdk_wallet version to 1.0.0-beta.5 bdk_core to 0.3.0 bdk_chain to 0.20.0 bdk_bitcoind_rpc to 0.16.0 bdk_electrum to 0.19.0 bdk_esplora to 0.19.0 bdk_file_store to 0.17.0 bdk_testenv to 0.10.0 --- crates/bitcoind_rpc/Cargo.toml | 4 ++-- crates/chain/Cargo.toml | 4 ++-- crates/core/Cargo.toml | 2 +- crates/electrum/Cargo.toml | 4 ++-- crates/esplora/Cargo.toml | 4 ++-- crates/file_store/Cargo.toml | 4 ++-- crates/testenv/Cargo.toml | 4 ++-- crates/wallet/Cargo.toml | 6 +++--- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/bitcoind_rpc/Cargo.toml b/crates/bitcoind_rpc/Cargo.toml index 5694c895a..a34260074 100644 --- a/crates/bitcoind_rpc/Cargo.toml +++ b/crates/bitcoind_rpc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_bitcoind_rpc" -version = "0.15.0" +version = "0.16.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -18,7 +18,7 @@ workspace = true [dependencies] bitcoin = { version = "0.32.0", default-features = false } bitcoincore-rpc = { version = "0.19.0" } -bdk_core = { path = "../core", version = "0.2.0", default-features = false } +bdk_core = { path = "../core", version = "0.3.0", default-features = false } [dev-dependencies] bdk_testenv = { path = "../testenv", default-features = false } diff --git a/crates/chain/Cargo.toml b/crates/chain/Cargo.toml index 95bcaf9f6..9085b5ec8 100644 --- a/crates/chain/Cargo.toml +++ b/crates/chain/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_chain" -version = "0.19.0" +version = "0.20.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -17,7 +17,7 @@ workspace = true [dependencies] bitcoin = { version = "0.32.0", default-features = false } -bdk_core = { path = "../core", version = "0.2.0", default-features = false } +bdk_core = { path = "../core", version = "0.3.0", default-features = false } serde = { version = "1", optional = true, features = ["derive", "rc"] } miniscript = { version = "12.0.0", optional = true, default-features = false } diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 3749b9157..c218bf164 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_core" -version = "0.2.0" +version = "0.3.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" diff --git a/crates/electrum/Cargo.toml b/crates/electrum/Cargo.toml index 808586a13..03c3783b3 100644 --- a/crates/electrum/Cargo.toml +++ b/crates/electrum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_electrum" -version = "0.18.0" +version = "0.19.0" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -13,7 +13,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_core = { path = "../core", version = "0.2.0" } +bdk_core = { path = "../core", version = "0.3.0" } electrum-client = { version = "0.21", features = [ "proxy" ], default-features = false } [dev-dependencies] diff --git a/crates/esplora/Cargo.toml b/crates/esplora/Cargo.toml index 7086b5b82..2f42b4fd7 100644 --- a/crates/esplora/Cargo.toml +++ b/crates/esplora/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_esplora" -version = "0.18.0" +version = "0.19.0" edition = "2021" homepage = "https://bitcoindevkit.org" repository = "https://github.com/bitcoindevkit/bdk" @@ -15,7 +15,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_core = { path = "../core", version = "0.2.0", default-features = false } +bdk_core = { path = "../core", version = "0.3.0", default-features = false } esplora-client = { version = "0.10.0", default-features = false } async-trait = { version = "0.1.66", optional = true } futures = { version = "0.3.26", optional = true } diff --git a/crates/file_store/Cargo.toml b/crates/file_store/Cargo.toml index c9728941d..a45e73f62 100644 --- a/crates/file_store/Cargo.toml +++ b/crates/file_store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_file_store" -version = "0.16.0" +version = "0.17.0" edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/bitcoindevkit/bdk" @@ -14,7 +14,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_core = { path = "../core", version = "0.2.0", features = ["serde"]} +bdk_core = { path = "../core", version = "0.3.0", features = ["serde"]} bincode = { version = "1" } serde = { version = "1", features = ["derive"] } diff --git a/crates/testenv/Cargo.toml b/crates/testenv/Cargo.toml index 98afa8d00..ee7541ea8 100644 --- a/crates/testenv/Cargo.toml +++ b/crates/testenv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bdk_testenv" -version = "0.9.0" +version = "0.10.0" edition = "2021" rust-version = "1.63" homepage = "https://bitcoindevkit.org" @@ -16,7 +16,7 @@ readme = "README.md" workspace = true [dependencies] -bdk_chain = { path = "../chain", version = "0.19.0", default-features = false } +bdk_chain = { path = "../chain", version = "0.20.0", default-features = false } electrsd = { version = "0.28.0", features = [ "bitcoind_25_0", "esplora_a33e97e1", "legacy" ] } [features] diff --git a/crates/wallet/Cargo.toml b/crates/wallet/Cargo.toml index ebec3a842..18f72337a 100644 --- a/crates/wallet/Cargo.toml +++ b/crates/wallet/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "bdk_wallet" homepage = "https://bitcoindevkit.org" -version = "1.0.0-beta.4" +version = "1.0.0-beta.5" repository = "https://github.com/bitcoindevkit/bdk" documentation = "https://docs.rs/bdk" description = "A modern, lightweight, descriptor-based wallet library" @@ -21,8 +21,8 @@ miniscript = { version = "12.0.0", features = [ "serde" ], default-features = fa bitcoin = { version = "0.32.0", features = [ "serde", "base64" ], default-features = false } serde = { version = "^1.0", features = ["derive"] } serde_json = { version = "^1.0" } -bdk_chain = { path = "../chain", version = "0.19.0", features = [ "miniscript", "serde" ], default-features = false } -bdk_file_store = { path = "../file_store", version = "0.16.0", optional = true } +bdk_chain = { path = "../chain", version = "0.20.0", features = [ "miniscript", "serde" ], default-features = false } +bdk_file_store = { path = "../file_store", version = "0.17.0", optional = true } # Optional dependencies bip39 = { version = "2.0", optional = true }