Compare commits

..

26 Commits

Author SHA1 Message Date
LLFourn
8de422dcfd Add SyncOptions as the second argument to Wallet::sync
The current options are awkware and it would be good if we could
introduce more in the future without breaking changes.
2022-01-27 16:52:53 +11:00
LLFourn
733300623e Remove Blockchain from wallet
Although somewhat convenient to have, coupling the Wallet with
the blockchain trait causes development friction and complexity.
What if sometimes the wallet is "offline" (no access to the blockchain)
but sometimes its online?
The only thing the Wallet needs the blockchain for is to sync.
But not all applications will even use the sync method and the sync
method doesn't require the full blockchain functionality.
So we instead pass the blockchain in when we want to sync.

- To further reduce the coupling with blockchain I removed the get_height call from `new` and just use the height of the
last sync in the database.
- I split up the blockchain trait a bit into subtraits.
2022-01-26 20:11:22 +11:00
Steve Myers
b1346d4ccf Merge bitcoindevkit/bdk#505: Using dust value from rust-bitcoin in `is_dust`
5ac51dfe74 fix and test is_dust (James Taylor)
a0c140bb29 add doc comment for IsDust trait (James Taylor)
bf5994b14a fixed fee in test, removed unnecessary comment (James Taylor)
ca682819b3 using dust value from rust-bitcoin (James Taylor)

Pull request description:

  ### Description

  This PR aims to fix #472 . We can retrieve the dust value for a given ``bitcoin::blockdata::script::Script``, so I adjusted the ``is_dust`` function within the ``IsDust`` trait to receive such a ``&Script``. Thus, the ``is_dust`` function can make the proper comparison.
  Let me know if you think that there could be a better interface than this.

  Furthermore, because this new ``is_dust`` function provides a tighter upper bound on Bitcoin Core's ``GetDustThreshold()``, it actually invalidated a test. In the test, the drain output for a transaction was no longer considered dust and no longer included in the fee. Instead, the drain output was kicked back to the sender, invalidating the asserts in line 3436, 3437 and 3441 in ``src/wallet/mod.rs``. I increased the ``FeeRate`` in the test just enough that the drain output would be small enough to considered dust again and included in the total fee.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  afilini:
    ACK 5ac51dfe74
  notmandatory:
    re ACK 5ac51dfe74

Tree-SHA512: addf38fe065de581ddfcd3b4e6db92cd35d5bfa8cac78bd08c01f7a01292724a203ef59b09f3f5cd8e0fa0bb6d89efe72afda36efc11ded0424fc8105326af3f
2022-01-12 17:49:16 +01:00
Steve Myers
5107ff80c1 Merge bitcoindevkit/bdk#495: Disable reqwest's default features
380a4f2588 Disable reqwest's default features (Thomas Eizinger)

Pull request description:

  ### Description

  By default, reqwest uses openssl for TLS. Any consumer wanting to use
  rustls will thus pull in unnecessary dependencies.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * ~[ ] I've added tests for the new feature~
  * ~[ ] I've added docs for the new feature~
  * [x] I've updated `CHANGELOG.md`

  #### Bugfixes:

  * ~[ ] This pull request breaks the existing API~
  * ~[ ] I've added tests to reproduce the issue which are now passing~
  * ~[ ] I'm linking the issue being fixed by this PR~

ACKs for top commit:
  notmandatory:
    ACK 380a4f2588

Tree-SHA512: 17827fdd7656a1e97b4cc302bc3c4907a8493505c798fafd9b15fde12531a32cf60e7d63e878eb2001d6b3e95f7ae3da730e227eb85c73d9de55b56456cfb3a0
2022-01-12 09:16:24 +01:00
James Taylor
5ac51dfe74 fix and test is_dust 2022-01-11 18:21:35 -05:00
Steve Myers
04d58f7903 Merge commit 'refs/pull/508/head' of github.com:bitcoindevkit/bdk 2022-01-11 10:08:33 +01:00
Thomas Eizinger
380a4f2588 Disable reqwest's default features
By default, reqwest uses openssl for TLS. Any consumer wanting to use
rustls will thus pull in unnecessary dependencies. To make getting started
with bdk and reqwest easier, we add a `reqwest-default-tls` feature
that can be used to activate reqwest's `default-tls` feature. TLS is
necessary for the esplora integration. Adding this feature makes it possible
for people to use bdk with esplora without adding a reqwest dependency to
their manifest.
2022-01-10 13:57:22 +11:00
Steve Myers
9e30a79027 Fix CHANGELOG link for v0.15.0 2021-12-29 13:17:11 -08:00
Alekos Filini
fdb272e039 Merge bitcoindevkit/bdk#511: Fix nightly_docs.yml publish_docs 'Commit' step
947a9c29db Fix nightly_docs.yml publish_docs 'Commit' step (Steve Myers)

Pull request description:

  ### Description

  I forgot to fix in #503 the `nightly_docs.yaml` `publish_docs` `Commit` step to add new files for the path `./docs/.vuepress/public/docs-rs`.

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)

ACKs for top commit:
  afilini:
    ACK 947a9c2

Tree-SHA512: d2bdbcb6cea46ec1949eba6f334acd5dbbe9b4b1323bb4713dc5d7f749666260ab05c29247c35f08c587b46d6bfb765c6a612c6522fd15211c84f7590f8c4748
2021-12-29 10:59:32 +01:00
Steve Myers
d2b6b5545e Bump version to 0.15.1-dev 2021-12-23 10:40:42 -08:00
Steve Myers
db6ffb90f0 Merge commit 'refs/pull/510/head' of github.com:bitcoindevkit/bdk 2021-12-23 10:34:58 -08:00
Steve Myers
947a9c29db Fix nightly_docs.yml publish_docs 'Commit' step 2021-12-23 10:23:11 -08:00
Alekos Filini
61ee2a9c1c Merge commit 'refs/pull/504/head' of github.com:bitcoindevkit/bdk 2021-12-23 12:24:52 +01:00
Alekos Filini
44e4c5dac5 Merge commit 'refs/pull/509/head' of github.com:bitcoindevkit/bdk 2021-12-23 12:22:09 +01:00
Alekos Filini
e09aaf055a Add a custom logo to our docs
As suggested in #497, add our logo to the docs as well
2021-12-23 11:37:41 +01:00
Alekos Filini
c40898ba08 Merge commit 'refs/pull/503/head' of github.com:bitcoindevkit/bdk 2021-12-23 11:34:34 +01:00
Steve Myers
2f98db8549 Add back old logo to static/bdk.svg
This is required so that old releases of bdk on crates.io won't show a
broken image link. Should be replaced with SVG version of new logo.
2021-12-22 21:38:54 -08:00
James Taylor
a0c140bb29 add doc comment for IsDust trait 2021-12-22 01:50:17 -05:00
James Taylor
bf5994b14a fixed fee in test, removed unnecessary comment 2021-12-19 18:37:05 -05:00
James Taylor
ca682819b3 using dust value from rust-bitcoin 2021-12-19 02:55:24 -05:00
mcroad
ee41d88f25 Test WIF from BIP39 words has correct network 2021-12-18 15:34:18 -06:00
mcroad
beb1e4114d Add fix to changelog 2021-12-18 15:13:09 -06:00
mcroad
af047f90db Set the correct inner private_key network 2021-12-18 15:10:25 -06:00
mcroad
d01ec6d259 Add test to ensure WIF uses the correct network 2021-12-18 15:08:16 -06:00
Alekos Filini
77bce06caf Update logo 2021-12-18 15:17:45 +01:00
Steve Myers
1a907f8a53 [ci] Fix publish_docs job 2021-12-17 21:28:39 -08:00
27 changed files with 605 additions and 565 deletions

View File

@@ -44,18 +44,18 @@ jobs:
repository: bitcoindevkit/bitcoindevkit.org
ref: master
- name: Create directories
run: mkdir -p ./static/docs-rs/bdk/nightly
run: mkdir -p ./docs/.vuepress/public/docs-rs/bdk/nightly
- name: Remove old latest
run: rm -rf ./static/docs-rs/bdk/nightly/latest
run: rm -rf ./docs/.vuepress/public/docs-rs/bdk/nightly/latest
- name: Download built docs
uses: actions/download-artifact@v1
with:
name: built-docs
path: ./static/docs-rs/bdk/nightly/latest
path: ./docs/.vuepress/public/docs-rs/bdk/nightly/latest
- name: Configure git
run: git config user.email "github-actions@github.com" && git config user.name "github-actions"
- name: Commit
continue-on-error: true # If there's nothing to commit this step fails, but it's fine
run: git add ./static && git commit -m "Publish autogenerated nightly docs"
run: git add ./docs/.vuepress/public/docs-rs && git commit -m "Publish autogenerated nightly docs"
- name: Push
run: git push origin master

View File

@@ -6,11 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Sync API change
To decouple the `Wallet` from the `Blockchain` we've made major changes:
- Removed `Blockchain` from Wallet.
- Removed `Wallet::broadcast` (just use `Blockchain::broadcast`)
- Depreciated `Wallet::new_offline` (all wallets are offline now)
- Changed `Wallet::sync` to take a `Blockchain`.
- Stop making a request for the block height when calling `Wallet:new`.
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
## [v0.15.0] - [v0.14.0]
- Overhauled sync logic for electrum and esplora.
- Unify ureq and reqwest esplora backends to have the same configuration parameters. This means reqwest now has a timeout parameter and ureq has a concurrency parameter.
- Fixed esplora fee estimation.
- Fixed generating WIF in the correct network format.
- Disable `reqwest` default features.
- Added `reqwest-default-tls` feature: Use this to restore the TLS defaults of reqwest if you don't want to add a dependency to it in your own manifest.
## [v0.14.0] - [v0.13.0]
@@ -403,3 +417,4 @@ final transaction is created by calling `finish` on the builder.
[v0.12.0]: https://github.com/bitcoindevkit/bdk/compare/v0.11.0...v0.12.0
[v0.13.0]: https://github.com/bitcoindevkit/bdk/compare/v0.12.0...v0.13.0
[v0.14.0]: https://github.com/bitcoindevkit/bdk/compare/v0.13.0...v0.14.0
[v0.15.0]: https://github.com/bitcoindevkit/bdk/compare/v0.14.0...v0.15.0

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk"
version = "0.15.0"
version = "0.15.1-dev"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
@@ -25,7 +25,7 @@ sled = { version = "0.34", optional = true }
electrum-client = { version = "0.8", optional = true }
rusqlite = { version = "0.25.3", optional = true }
ahash = { version = "=0.7.4", optional = true }
reqwest = { version = "0.11", optional = true, features = ["json"] }
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
ureq = { version = "~2.2.0", features = ["json"], optional = true }
futures = { version = "0.3", optional = true }
async-trait = { version = "0.1", optional = true }
@@ -82,6 +82,8 @@ use-esplora-ureq = ["esplora", "ureq", "ureq/socks"]
# Typical configurations will not need to use `esplora` feature directly.
esplora = []
# Use below feature with `use-esplora-reqwest` to enable reqwest default TLS support
reqwest-default-tls = ["reqwest/default-tls"]
# Debug/Test features
test-blockchains = ["bitcoincore-rpc", "electrum-client"]

View File

@@ -1,7 +1,7 @@
<div align="center">
<h1>BDK</h1>
<img src="./static/bdk.svg" width="220" />
<img src="./static/bdk.png" width="220" />
<p>
<strong>A modern, lightweight, descriptor-based wallet library written in Rust!</strong>
@@ -41,21 +41,21 @@ The `bdk` library aims to be the core building block for Bitcoin wallets of any
```rust,no_run
use bdk::Wallet;
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::blockchain::ElectrumBlockchain;
use bdk::SyncOptions;
use bdk::electrum_client::Client;
fn main() -> Result<(), bdk::Error> {
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
wallet.sync(noop_progress(), None)?;
wallet.sync(&blockchain, SyncOptions::default())?;
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
@@ -70,7 +70,7 @@ use bdk::{Wallet, database::MemoryDatabase};
use bdk::wallet::AddressIndex::New;
fn main() -> Result<(), bdk::Error> {
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet,
@@ -88,9 +88,9 @@ fn main() -> Result<(), bdk::Error> {
### Create a transaction
```rust,no_run
use bdk::{FeeRate, Wallet};
use bdk::{FeeRate, Wallet, SyncOptions};
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
use bdk::wallet::AddressIndex::New;
@@ -98,16 +98,15 @@ use bdk::wallet::AddressIndex::New;
use bitcoin::consensus::serialize;
fn main() -> Result<(), bdk::Error> {
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
wallet.sync(noop_progress(), None)?;
wallet.sync(&blockchain, SyncOptions::default())?;
let send_to = wallet.get_address(New)?;
let (psbt, details) = {
@@ -135,7 +134,7 @@ use bdk::{Wallet, SignOptions, database::MemoryDatabase};
use bitcoin::consensus::deserialize;
fn main() -> Result<(), bdk::Error> {
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
bitcoin::Network::Testnet,

View File

@@ -48,8 +48,7 @@ impl AddressValidator for DummyValidator {
fn main() -> Result<(), bdk::Error> {
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
let mut wallet =
Wallet::new_offline(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
wallet.add_address_validator(Arc::new(DummyValidator));

View File

@@ -10,7 +10,6 @@
// licenses.
use bdk::blockchain::compact_filters::*;
use bdk::blockchain::noop_progress;
use bdk::database::MemoryDatabase;
use bdk::*;
use bitcoin::*;
@@ -35,9 +34,8 @@ fn main() -> Result<(), CompactFiltersError> {
let descriptor = "wpkh(tpubD6NzVbkrYhZ4X2yy78HWrr1M9NT8dKeWfzNiQqDdMqqa9UmmGztGGz6TaLFGsLfdft5iu32gxq1T4eMNxExNNWzVCpf9Y6JZi5TnqoC9wJq/*)";
let database = MemoryDatabase::default();
let wallet =
Arc::new(Wallet::new(descriptor, None, Network::Testnet, database, blockchain).unwrap());
wallet.sync(noop_progress(), None).unwrap();
let wallet = Arc::new(Wallet::new(descriptor, None, Network::Testnet, database).unwrap());
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
info!("balance: {}", wallet.get_balance()?);
Ok(())
}

View File

@@ -89,7 +89,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.transpose()
.unwrap()
.unwrap_or(Network::Testnet);
let wallet = Wallet::new_offline(&format!("{}", descriptor), None, network, database)?;
let wallet = Wallet::new(&format!("{}", descriptor), None, network, database)?;
info!("... First address: {}", wallet.get_address(New)?);

View File

@@ -16,61 +16,17 @@
//!
//! ## Example
//!
//! In this example both `wallet_electrum` and `wallet_esplora` have the same type of
//! `Wallet<AnyBlockchain, MemoryDatabase>`. This means that they could both, for instance, be
//! assigned to a struct member.
//!
//! ```no_run
//! # use bitcoin::Network;
//! # use bdk::blockchain::*;
//! # use bdk::database::MemoryDatabase;
//! # use bdk::Wallet;
//! # #[cfg(feature = "electrum")]
//! # {
//! let electrum_blockchain = ElectrumBlockchain::from(electrum_client::Client::new("...")?);
//! let wallet_electrum: Wallet<AnyBlockchain, _> = Wallet::new(
//! "...",
//! None,
//! Network::Testnet,
//! MemoryDatabase::default(),
//! electrum_blockchain.into(),
//! )?;
//! # }
//!
//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
//! # {
//! let esplora_blockchain = EsploraBlockchain::new("...", 20);
//! let wallet_esplora: Wallet<AnyBlockchain, _> = Wallet::new(
//! "...",
//! None,
//! Network::Testnet,
//! MemoryDatabase::default(),
//! esplora_blockchain.into(),
//! )?;
//! # }
//!
//! # Ok::<(), bdk::Error>(())
//! ```
//!
//! When paired with the use of [`ConfigurableBlockchain`], it allows creating wallets with any
//! blockchain type supported using a single line of code:
//!
//! ```no_run
//! # use bitcoin::Network;
//! # use bdk::blockchain::*;
//! # use bdk::database::MemoryDatabase;
//! # use bdk::Wallet;
//! # #[cfg(all(feature = "esplora", feature = "ureq"))]
//! # {
//! let config = serde_json::from_str("...")?;
//! let blockchain = AnyBlockchain::from_config(&config)?;
//! let wallet = Wallet::new(
//! "...",
//! None,
//! Network::Testnet,
//! MemoryDatabase::default(),
//! blockchain,
//! )?;
//! let height = blockchain.get_height();
//! # }
//! # Ok::<(), bdk::Error>(())
//! ```
@@ -133,21 +89,6 @@ impl Blockchain for AnyBlockchain {
maybe_await!(impl_inner_method!(self, get_capabilities))
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
&self,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(self, setup, database, progress_update))
}
fn sync<D: BatchDatabase, P: 'static + Progress>(
&self,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(self, sync, database, progress_update))
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(impl_inner_method!(self, get_tx, txid))
}
@@ -155,11 +96,42 @@ impl Blockchain for AnyBlockchain {
maybe_await!(impl_inner_method!(self, broadcast, tx))
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
maybe_await!(impl_inner_method!(self, estimate_fee, target))
}
}
impl GetHeight for AnyBlockchain {
fn get_height(&self) -> Result<u32, Error> {
maybe_await!(impl_inner_method!(self, get_height))
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
maybe_await!(impl_inner_method!(self, estimate_fee, target))
}
impl WalletSync for AnyBlockchain {
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(
self,
wallet_sync,
database,
progress_update
))
}
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(impl_inner_method!(
self,
wallet_setup,
database,
progress_update
))
}
}

View File

@@ -67,7 +67,7 @@ mod peer;
mod store;
mod sync;
use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
use super::{Blockchain, Capability, ConfigurableBlockchain, GetHeight, Progress, WalletSync};
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::error::Error;
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
@@ -226,11 +226,36 @@ impl Blockchain for CompactFiltersBlockchain {
vec![Capability::FullHistory].into_iter().collect()
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.peers[0]
.get_mempool()
.get_tx(&Inventory::Transaction(*txid)))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
self.peers[0].broadcast_tx(tx.clone())?;
Ok(())
}
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
// TODO
Ok(FeeRate::default())
}
}
impl GetHeight for CompactFiltersBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.headers.get_height()? as u32)
}
}
impl WalletSync for CompactFiltersBlockchain {
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
fn setup<D: BatchDatabase, P: 'static + Progress>(
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: P,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let first_peer = &self.peers[0];
@@ -431,27 +456,6 @@ impl Blockchain for CompactFiltersBlockchain {
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.peers[0]
.get_mempool()
.get_tx(&Inventory::Transaction(*txid)))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
self.peers[0].broadcast_tx(tx.clone())?;
Ok(())
}
fn get_height(&self) -> Result<u32, Error> {
Ok(self.headers.get_height()? as u32)
}
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
// TODO
Ok(FeeRate::default())
}
}
/// Data to connect to a Bitcoin P2P peer

View File

@@ -68,10 +68,37 @@ impl Blockchain for ElectrumBlockchain {
.collect()
}
fn setup<D: BatchDatabase, P: Progress>(
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.client.transaction_get(txid).map(Option::Some)?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
Ok(FeeRate::from_btc_per_kvb(
self.client.estimate_fee(target)? as f32
))
}
}
impl GetHeight for ElectrumBlockchain {
fn get_height(&self) -> Result<u32, Error> {
// TODO: unsubscribe when added to the client, or is there a better call to use here?
Ok(self
.client
.block_headers_subscribe()
.map(|data| data.height as u32)?)
}
}
impl WalletSync for ElectrumBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
_progress_update: P,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let mut request = script_sync::start(database, self.stop_gap)?;
let mut block_times = HashMap::<u32, u32>::new();
@@ -205,29 +232,6 @@ impl Blockchain for ElectrumBlockchain {
database.commit_batch(batch_update)?;
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.client.transaction_get(txid).map(Option::Some)?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
}
fn get_height(&self) -> Result<u32, Error> {
// TODO: unsubscribe when added to the client, or is there a better call to use here?
Ok(self
.client
.block_headers_subscribe()
.map(|data| data.height as u32)?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
Ok(FeeRate::from_btc_per_kvb(
self.client.estimate_fee(target)? as f32
))
}
}
struct TxCache<'a, 'b, D> {

View File

@@ -91,7 +91,30 @@ impl Blockchain for EsploraBlockchain {
.collect()
}
fn setup<D: BatchDatabase, P: Progress>(
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(await_or_block!(self.url_client._get_tx(txid))?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(await_or_block!(self.url_client._broadcast(tx))?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = await_or_block!(self.url_client._get_fee_estimates())?;
super::into_fee_rate(target, estimates)
}
}
#[maybe_async]
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(await_or_block!(self.url_client._get_height())?)
}
}
#[maybe_async]
impl WalletSync for EsploraBlockchain {
fn wallet_setup<D: BatchDatabase, P: Progress>(
&self,
database: &mut D,
_progress_update: P,
@@ -180,23 +203,6 @@ impl Blockchain for EsploraBlockchain {
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(await_or_block!(self.url_client._get_tx(txid))?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(await_or_block!(self.url_client._broadcast(tx))?)
}
fn get_height(&self) -> Result<u32, Error> {
Ok(await_or_block!(self.url_client._get_height())?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = await_or_block!(self.url_client._get_fee_estimates())?;
super::into_fee_rate(target, estimates)
}
}
impl UrlClient {

View File

@@ -87,10 +87,32 @@ impl Blockchain for EsploraBlockchain {
.collect()
}
fn setup<D: BatchDatabase, P: Progress>(
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.url_client._get_tx(txid)?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
let _txid = self.url_client._broadcast(tx)?;
Ok(())
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = self.url_client._get_fee_estimates()?;
super::into_fee_rate(target, estimates)
}
}
impl GetHeight for EsploraBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.url_client._get_height()?)
}
}
impl WalletSync for EsploraBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
_progress_update: P,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
use crate::blockchain::script_sync::Request;
let mut request = script_sync::start(database, self.stop_gap)?;
@@ -179,24 +201,6 @@ impl Blockchain for EsploraBlockchain {
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.url_client._get_tx(txid)?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
let _txid = self.url_client._broadcast(tx)?;
Ok(())
}
fn get_height(&self) -> Result<u32, Error> {
Ok(self.url_client._get_height()?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let estimates = self.url_client._get_fee_estimates()?;
super::into_fee_rate(target, estimates)
}
}
impl UrlClient {

View File

@@ -86,28 +86,45 @@ pub enum Capability {
/// Trait that defines the actions that must be supported by a blockchain backend
#[maybe_async]
pub trait Blockchain {
pub trait Blockchain: WalletSync + GetHeight {
/// Return the set of [`Capability`] supported by this backend
fn get_capabilities(&self) -> HashSet<Capability>;
/// Fetch a transaction from the blockchain given its txid
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
/// Broadcast a transaction
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error>;
}
/// Trait for getting the current height of the blockchain.
#[maybe_async]
pub trait GetHeight {
/// Return the current height
fn get_height(&self) -> Result<u32, Error>;
}
/// Trait for blockchains that can sync by updating the database directly.
#[maybe_async]
pub trait WalletSync {
/// Setup the backend and populate the internal database for the first time
///
/// This method is the equivalent of [`Blockchain::sync`], but it's guaranteed to only be
/// This method is the equivalent of [`Self::wallet_sync`], but it's guaranteed to only be
/// called once, at the first [`Wallet::sync`](crate::wallet::Wallet::sync).
///
/// The rationale behind the distinction between `sync` and `setup` is that some custom backends
/// might need to perform specific actions only the first time they are synced.
///
/// For types that do not have that distinction, only this method can be implemented, since
/// [`Blockchain::sync`] defaults to calling this internally if not overridden.
fn setup<D: BatchDatabase, P: 'static + Progress>(
/// [`WalletSync::wallet_sync`] defaults to calling this internally if not overridden.
/// Populate the internal database with transactions and UTXOs
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: P,
progress_update: Box<dyn Progress>,
) -> Result<(), Error>;
/// Populate the internal database with transactions and UTXOs
///
/// If not overridden, it defaults to calling [`Blockchain::setup`] internally.
/// If not overridden, it defaults to calling [`Self::wallet_setup`] internally.
///
/// This method should implement the logic required to iterate over the list of the wallet's
/// script_pubkeys using [`Database::iter_script_pubkeys`] and look for relevant transactions
@@ -124,23 +141,13 @@ pub trait Blockchain {
/// [`BatchOperations::set_tx`]: crate::database::BatchOperations::set_tx
/// [`BatchOperations::set_utxo`]: crate::database::BatchOperations::set_utxo
/// [`BatchOperations::del_utxo`]: crate::database::BatchOperations::del_utxo
fn sync<D: BatchDatabase, P: 'static + Progress>(
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: P,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(self.setup(database, progress_update))
maybe_await!(self.wallet_setup(database, progress_update))
}
/// Fetch a transaction from the blockchain given its txid
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
/// Broadcast a transaction
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
/// Return the current height
fn get_height(&self) -> Result<u32, Error>;
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error>;
}
/// Trait for [`Blockchain`] types that can be created given a configuration
@@ -155,9 +162,9 @@ pub trait ConfigurableBlockchain: Blockchain + Sized {
/// Data sent with a progress update over a [`channel`]
pub type ProgressData = (f32, Option<String>);
/// Trait for types that can receive and process progress updates during [`Blockchain::sync`] and
/// [`Blockchain::setup`]
pub trait Progress: Send {
/// Trait for types that can receive and process progress updates during [`WalletSync::wallet_sync`] and
/// [`WalletSync::wallet_setup`]
pub trait Progress: Send + 'static + core::fmt::Debug {
/// Send a new progress update
///
/// The `progress` value should be in the range 0.0 - 100.0, and the `message` value is an
@@ -182,7 +189,7 @@ impl Progress for Sender<ProgressData> {
}
/// Type that implements [`Progress`] and drops every update received
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Default, Debug)]
pub struct NoopProgress;
/// Create a new instance of [`NoopProgress`]
@@ -197,7 +204,7 @@ impl Progress for NoopProgress {
}
/// Type that implements [`Progress`] and logs at level `INFO` every update received
#[derive(Clone, Copy)]
#[derive(Clone, Copy, Default, Debug)]
pub struct LogProgress;
/// Create a new instance of [`LogProgress`]
@@ -223,22 +230,6 @@ impl<T: Blockchain> Blockchain for Arc<T> {
maybe_await!(self.deref().get_capabilities())
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
&self,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
maybe_await!(self.deref().setup(database, progress_update))
}
fn sync<D: BatchDatabase, P: 'static + Progress>(
&self,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
maybe_await!(self.deref().sync(database, progress_update))
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(self.deref().get_tx(txid))
}
@@ -246,10 +237,33 @@ impl<T: Blockchain> Blockchain for Arc<T> {
maybe_await!(self.deref().broadcast(tx))
}
fn get_height(&self) -> Result<u32, Error> {
maybe_await!(self.deref().get_height())
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
maybe_await!(self.deref().estimate_fee(target))
}
}
#[maybe_async]
impl<T: GetHeight> GetHeight for Arc<T> {
fn get_height(&self) -> Result<u32, Error> {
maybe_await!(self.deref().get_height())
}
}
#[maybe_async]
impl<T: WalletSync> WalletSync for Arc<T> {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(self.deref().wallet_setup(database, progress_update))
}
fn wallet_sync<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
maybe_await!(self.deref().wallet_sync(database, progress_update))
}
}

View File

@@ -33,7 +33,9 @@
use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress};
use crate::blockchain::{
Blockchain, Capability, ConfigurableBlockchain, GetHeight, Progress, WalletSync,
};
use crate::database::{BatchDatabase, DatabaseUtils};
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
use bitcoincore_rpc::json::{
@@ -139,10 +141,37 @@ impl Blockchain for RpcBlockchain {
self.capabilities.clone()
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(Some(self.client.get_raw_transaction(txid, None)?))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let sat_per_kb = self
.client
.estimate_smart_fee(target as u16, None)?
.fee_rate
.ok_or(Error::FeeRateUnavailable)?
.as_sat() as f64;
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
}
}
impl GetHeight for RpcBlockchain {
fn get_height(&self) -> Result<u32, Error> {
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
}
}
impl WalletSync for RpcBlockchain {
fn wallet_setup<D: BatchDatabase>(
&self,
database: &mut D,
progress_update: P,
progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let mut scripts_pubkeys = database.iter_script_pubkeys(Some(KeychainKind::External))?;
scripts_pubkeys.extend(database.iter_script_pubkeys(Some(KeychainKind::Internal))?);
@@ -187,13 +216,13 @@ impl Blockchain for RpcBlockchain {
}
}
self.sync(database, progress_update)
self.wallet_sync(database, progress_update)
}
fn sync<D: BatchDatabase, P: 'static + Progress>(
fn wallet_sync<D: BatchDatabase>(
&self,
db: &mut D,
_progress_update: P,
_progress_update: Box<dyn Progress>,
) -> Result<(), Error> {
let mut indexes = HashMap::new();
for keykind in &[KeychainKind::External, KeychainKind::Internal] {
@@ -323,29 +352,6 @@ impl Blockchain for RpcBlockchain {
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(Some(self.client.get_raw_transaction(txid, None)?))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
}
fn get_height(&self) -> Result<u32, Error> {
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let sat_per_kb = self
.client
.estimate_smart_fee(target as u16, None)?
.fee_rate
.ok_or(Error::FeeRateUnavailable)?
.as_sat() as f64;
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
}
}
impl ConfigurableBlockchain for RpcBlockchain {

View File

@@ -23,12 +23,12 @@
//! # use bdk::database::{AnyDatabase, MemoryDatabase};
//! # use bdk::{Wallet};
//! let memory = MemoryDatabase::default();
//! let wallet_memory = Wallet::new_offline("...", None, Network::Testnet, memory)?;
//! let wallet_memory = Wallet::new("...", None, Network::Testnet, memory)?;
//!
//! # #[cfg(feature = "key-value-db")]
//! # {
//! let sled = sled::open("my-database")?.open_tree("default_tree")?;
//! let wallet_sled = Wallet::new_offline("...", None, Network::Testnet, sled)?;
//! let wallet_sled = Wallet::new("...", None, Network::Testnet, sled)?;
//! # }
//! # Ok::<(), bdk::Error>(())
//! ```
@@ -42,7 +42,7 @@
//! # use bdk::{Wallet};
//! let config = serde_json::from_str("...")?;
//! let database = AnyDatabase::from_config(&config)?;
//! let wallet = Wallet::new_offline("...", None, Network::Testnet, database)?;
//! let wallet = Wallet::new("...", None, Network::Testnet, database)?;
//! # Ok::<(), bdk::Error>(())
//! ```

View File

@@ -555,7 +555,7 @@ macro_rules! doctest_wallet {
Some(100),
);
$crate::Wallet::new_offline(
$crate::Wallet::new(
&descriptors.0,
descriptors.1.as_ref(),
Network::Regtest,

View File

@@ -79,7 +79,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// P2Pkh(key),
/// None,
/// Network::Testnet,
@@ -113,7 +113,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// P2Wpkh_P2Sh(key),
/// None,
/// Network::Testnet,
@@ -148,7 +148,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
///
/// let key =
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// P2Wpkh(key),
/// None,
/// Network::Testnet,
@@ -186,7 +186,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// use bdk::template::Bip44;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip44(key.clone(), KeychainKind::External),
/// Some(Bip44(key, KeychainKind::Internal)),
/// Network::Testnet,
@@ -226,7 +226,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
@@ -262,7 +262,7 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
/// use bdk::template::Bip49;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip49(key.clone(), KeychainKind::External),
/// Some(Bip49(key, KeychainKind::Internal)),
/// Network::Testnet,
@@ -302,7 +302,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,
@@ -338,7 +338,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
/// use bdk::template::Bip84;
///
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip84(key.clone(), KeychainKind::External),
/// Some(Bip84(key, KeychainKind::Internal)),
/// Network::Testnet,
@@ -378,7 +378,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
///
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
/// let wallet = Wallet::new_offline(
/// let wallet = Wallet::new(
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
/// Network::Testnet,

View File

@@ -319,6 +319,7 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
match self {
ExtendedKey::Private((mut xprv, _)) => {
xprv.network = network;
xprv.private_key.network = network;
Some(xprv)
}
ExtendedKey::Public(_) => None,
@@ -931,4 +932,43 @@ pub mod test {
"L2wTu6hQrnDMiFNWA5na6jB12ErGQqtXwqpSL7aWquJaZG8Ai3ch"
);
}
#[test]
fn test_keys_wif_network() {
// test mainnet wif
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
let xkey = generated_xprv.into_extended_key().unwrap();
let network = Network::Bitcoin;
let xprv = xkey.into_xprv(network).unwrap();
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
assert_eq!(wif.network, network);
// test testnet wif
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
let xkey = generated_xprv.into_extended_key().unwrap();
let network = Network::Testnet;
let xprv = xkey.into_xprv(network).unwrap();
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
assert_eq!(wif.network, network);
}
#[cfg(feature = "keys-bip39")]
#[test]
fn test_keys_wif_network_bip39() {
let xkey: ExtendedKey = bip39::Mnemonic::parse_in(
bip39::Language::English,
"jelly crash boy whisper mouse ecology tuna soccer memory million news short",
)
.unwrap()
.into_extended_key()
.unwrap();
let xprv = xkey.into_xprv(Network::Testnet).unwrap();
let wif = PrivateKey::from_wif(&xprv.private_key.to_wif()).unwrap();
assert_eq!(wif.network, Network::Testnet);
}
}

View File

@@ -14,6 +14,10 @@
// 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")
)]
//! A modern, lightweight, descriptor-based wallet library written in Rust.
//!
@@ -49,22 +53,22 @@
### Example
```no_run
use bdk::Wallet;
use bdk::{Wallet, SyncOptions};
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
fn main() -> Result<(), bdk::Error> {
let client = Client::new("ssl://electrum.blockstream.info:60002")?;
let blockchain = ElectrumBlockchain::from(client);
let wallet = Wallet::new(
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
wallet.sync(noop_progress(), None)?;
wallet.sync(&blockchain, SyncOptions::default())?;
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
@@ -83,7 +87,7 @@ fn main() -> Result<(), bdk::Error> {
//! use bdk::wallet::AddressIndex::New;
//!
//! fn main() -> Result<(), bdk::Error> {
//! let wallet = Wallet::new_offline(
//! let wallet = Wallet::new(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
//! bitcoin::Network::Testnet,
@@ -104,9 +108,9 @@ fn main() -> Result<(), bdk::Error> {
### Example
```no_run
use bdk::{FeeRate, Wallet};
use bdk::{FeeRate, Wallet, SyncOptions};
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
use bdk::blockchain::ElectrumBlockchain;
use bdk::electrum_client::Client;
use bitcoin::consensus::serialize;
@@ -119,10 +123,10 @@ fn main() -> Result<(), bdk::Error> {
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
bitcoin::Network::Testnet,
MemoryDatabase::default(),
ElectrumBlockchain::from(client)
)?;
let blockchain = ElectrumBlockchain::from(client);
wallet.sync(noop_progress(), None)?;
wallet.sync(&blockchain, SyncOptions::default())?;
let send_to = wallet.get_address(New)?;
let (psbt, details) = {
@@ -156,7 +160,7 @@ fn main() -> Result<(), bdk::Error> {
//! use bdk::database::MemoryDatabase;
//!
//! fn main() -> Result<(), bdk::Error> {
//! let wallet = Wallet::new_offline(
//! let wallet = Wallet::new(
//! "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
//! bitcoin::Network::Testnet,
@@ -268,6 +272,7 @@ pub use wallet::address_validator;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder;
pub use wallet::SyncOptions;
pub use wallet::Wallet;
/// Get the version of BDK at runtime

View File

@@ -355,10 +355,10 @@ macro_rules! bdk_blockchain_tests {
mod bdk_blockchain_tests {
use $crate::bitcoin::Network;
use $crate::testutils::blockchain_tests::TestClient;
use $crate::blockchain::noop_progress;
use $crate::blockchain::Blockchain;
use $crate::database::MemoryDatabase;
use $crate::types::KeychainKind;
use $crate::{Wallet, FeeRate};
use $crate::{Wallet, FeeRate, SyncOptions};
use $crate::testutils;
use super::*;
@@ -369,11 +369,11 @@ macro_rules! bdk_blockchain_tests {
$block
}
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>), test_client: &TestClient) -> Wallet<$blockchain, MemoryDatabase> {
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new(), get_blockchain(test_client)).unwrap()
fn get_wallet_from_descriptors(descriptors: &(String, Option<String>)) -> Wallet<MemoryDatabase> {
Wallet::new(&descriptors.0.to_string(), descriptors.1.as_ref(), Network::Regtest, MemoryDatabase::new()).unwrap()
}
fn init_single_sig() -> (Wallet<$blockchain, MemoryDatabase>, (String, Option<String>), TestClient) {
fn init_single_sig() -> (Wallet<MemoryDatabase>, $blockchain, (String, Option<String>), TestClient) {
let _ = env_logger::try_init();
let descriptors = testutils! {
@@ -381,13 +381,14 @@ macro_rules! bdk_blockchain_tests {
};
let test_client = TestClient::default();
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
let blockchain = get_blockchain(&test_client);
let wallet = get_wallet_from_descriptors(&descriptors);
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
#[cfg(feature = "test-rpc")]
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
(wallet, descriptors, test_client)
(wallet, blockchain, descriptors, test_client)
}
#[test]
@@ -395,7 +396,7 @@ macro_rules! bdk_blockchain_tests {
use std::ops::Deref;
use crate::database::Database;
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let tx = testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
@@ -408,7 +409,7 @@ macro_rules! bdk_blockchain_tests {
#[cfg(not(feature = "test-rpc"))]
assert!(wallet.database().deref().get_sync_time().unwrap().is_none(), "initial sync_time not none");
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
@@ -423,7 +424,7 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_stop_gap_20() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 5) => 50_000 )
@@ -432,7 +433,7 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 25) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
@@ -440,16 +441,16 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_before_and_after_receive() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
@@ -457,13 +458,13 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_multiple_outputs_same_tx() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000, (@external descriptors, 5) => 30_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
@@ -478,7 +479,7 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_receive_multi() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
@@ -487,7 +488,7 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 5) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
@@ -496,32 +497,32 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_address_reuse() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 25_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
}
#[test]
fn test_sync_receive_rbf_replaced() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
@@ -535,7 +536,7 @@ macro_rules! bdk_blockchain_tests {
let new_txid = test_client.bump_fee(&txid);
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
@@ -553,13 +554,13 @@ macro_rules! bdk_blockchain_tests {
#[cfg(not(feature = "esplora"))]
#[test]
fn test_sync_reorg_block() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 1 ) ( @replaceable true )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
@@ -572,7 +573,7 @@ macro_rules! bdk_blockchain_tests {
// Invalidate 1 block
test_client.invalidate(1);
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
@@ -583,15 +584,15 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_after_send() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -601,8 +602,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
let tx = psbt.extract_tx();
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
wallet.broadcast(&tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
@@ -613,16 +614,16 @@ macro_rules! bdk_blockchain_tests {
/// The coins should only be received once!
#[test]
fn test_sync_double_receive() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let receiver_wallet = get_wallet_from_descriptors(&("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None), &test_client);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let receiver_wallet = get_wallet_from_descriptors(&("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None));
// need to sync so rpc can start watching
receiver_wallet.sync(noop_progress(), None).unwrap();
receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
@@ -644,16 +645,16 @@ macro_rules! bdk_blockchain_tests {
psbt.extract_tx()
};
wallet.broadcast(&tx1).unwrap();
wallet.broadcast(&tx2).unwrap();
blockchain.broadcast(&tx1).unwrap();
blockchain.broadcast(&tx2).unwrap();
receiver_wallet.sync(noop_progress(), None).unwrap();
receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(receiver_wallet.get_balance().unwrap(), 49_000, "should have received coins once and only once");
}
#[test]
fn test_sync_many_sends_to_a_single_address() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
for _ in 0..4 {
// split this up into multiple blocks so rpc doesn't get angry
@@ -672,22 +673,22 @@ macro_rules! bdk_blockchain_tests {
});
}
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
}
#[test]
fn test_update_confirmation_time_after_generate() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
@@ -695,7 +696,7 @@ macro_rules! bdk_blockchain_tests {
assert!(details.confirmation_time.is_none());
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let details = tx_map.get(&received_txid).unwrap();
@@ -705,13 +706,13 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_outgoing_from_scratch() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -720,25 +721,26 @@ macro_rules! bdk_blockchain_tests {
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let sent_txid = wallet.broadcast(&psbt.extract_tx()).unwrap();
let sent_tx = psbt.extract_tx();
blockchain.broadcast(&sent_tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let received = tx_map.get(&received_txid).unwrap();
assert_eq!(received.received, 50_000, "incorrect received from receiver");
assert_eq!(received.sent, 0, "incorrect sent from receiver");
let sent = tx_map.get(&sent_txid).unwrap();
let sent = tx_map.get(&sent_tx.txid()).unwrap();
assert_eq!(sent.received, details.received, "incorrect received from sender");
assert_eq!(sent.sent, details.sent, "incorrect sent from sender");
assert_eq!(sent.fee.unwrap_or(0), details.fee.unwrap_or(0), "incorrect fees from sender");
@@ -746,14 +748,14 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_long_change_chain() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut total_sent = 0;
@@ -763,38 +765,38 @@ macro_rules! bdk_blockchain_tests {
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&psbt.extract_tx()).unwrap();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
total_sent += 5_000 + details.fee.unwrap_or(0);
}
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors, &test_client);
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
}
#[test]
fn test_sync_bump_fee_basic() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -802,8 +804,8 @@ macro_rules! bdk_blockchain_tests {
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
@@ -812,8 +814,8 @@ macro_rules! bdk_blockchain_tests {
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
@@ -822,14 +824,14 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_bump_fee_remove_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -837,18 +839,18 @@ macro_rules! bdk_blockchain_tests {
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
builder.fee_rate(FeeRate::from_sat_per_vb(5.1));
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
assert_eq!(new_details.received, 0, "incorrect received after change removal");
@@ -857,14 +859,14 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_sync_bump_fee_add_input_simple() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -872,8 +874,8 @@ macro_rules! bdk_blockchain_tests {
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
@@ -882,22 +884,22 @@ macro_rules! bdk_blockchain_tests {
let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(new_details.sent, 75_000, "incorrect sent");
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
}
#[test]
fn test_sync_bump_fee_add_input_no_change() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -905,8 +907,8 @@ macro_rules! bdk_blockchain_tests {
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
@@ -917,8 +919,8 @@ macro_rules! bdk_blockchain_tests {
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(new_details.sent, 75_000, "incorrect sent");
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
assert_eq!(new_details.received, 0, "incorrect received after add input");
@@ -927,13 +929,13 @@ macro_rules! bdk_blockchain_tests {
#[test]
fn test_add_data() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let _ = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
@@ -946,22 +948,22 @@ macro_rules! bdk_blockchain_tests {
let tx = psbt.extract_tx();
let serialized_tx = bitcoin::consensus::encode::serialize(&tx);
assert!(serialized_tx.windows(data.len()).any(|e| e==data), "cannot find op_return data in transaction");
let sent_txid = wallet.broadcast(&tx).unwrap();
blockchain.broadcast(&tx).unwrap();
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let _ = tx_map.get(&sent_txid).unwrap();
let _ = tx_map.get(&tx.txid()).unwrap();
}
#[test]
fn test_sync_receive_coinbase() {
let (wallet, _, mut test_client) = init_single_sig();
let (wallet, blockchain, _, mut test_client) = init_single_sig();
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
test_client.generate(1, Some(wallet_addr));
@@ -974,7 +976,7 @@ macro_rules! bdk_blockchain_tests {
}
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
}
@@ -987,7 +989,7 @@ macro_rules! bdk_blockchain_tests {
use bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoincore_rpc::{Auth, Client, RpcApi};
let (wallet, descriptors, mut test_client) = init_single_sig();
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
// TODO remove once rust-bitcoincore-rpc with PR 199 released
// https://github.com/rust-bitcoin/rust-bitcoincore-rpc/pull/199
@@ -1050,7 +1052,7 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance");
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
@@ -1061,8 +1063,8 @@ macro_rules! bdk_blockchain_tests {
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "wallet cannot finalize transaction");
let tx = psbt.extract_tx();
wallet.broadcast(&tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
blockchain.broadcast(&tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");

View File

@@ -55,7 +55,7 @@
//! }
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! wallet.add_address_validator(Arc::new(PrintAddressAndContinue));
//!
//! let address = wallet.get_address(New)?;

View File

@@ -30,7 +30,7 @@
//! }"#;
//!
//! let import = WalletExport::from_str(import)?;
//! let wallet = Wallet::new_offline(
//! let wallet = Wallet::new(
//! &import.descriptor(),
//! import.change_descriptor().as_ref(),
//! Network::Testnet,
@@ -45,7 +45,7 @@
//! # use bdk::database::*;
//! # use bdk::wallet::export::*;
//! # use bdk::*;
//! let wallet = Wallet::new_offline(
//! let wallet = Wallet::new(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
//! Network::Testnet,
@@ -111,8 +111,8 @@ impl WalletExport {
///
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
/// returned will be `0`.
pub fn export_wallet<B, D: BatchDatabase>(
wallet: &Wallet<B, D>,
pub fn export_wallet<D: BatchDatabase>(
wallet: &Wallet<D>,
label: &str,
include_blockheight: bool,
) -> Result<Self, &'static str> {
@@ -242,7 +242,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,
@@ -266,8 +266,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let wallet =
Wallet::new_offline(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
let wallet = Wallet::new(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
}
@@ -280,7 +279,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,
@@ -303,7 +302,7 @@ mod test {
[c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
))";
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Testnet,
@@ -323,7 +322,7 @@ mod test {
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
descriptor,
Some(change_descriptor),
Network::Bitcoin,

View File

@@ -53,9 +53,9 @@ use address_validator::AddressValidator;
use coin_selection::DefaultCoinSelectionAlgorithm;
use signer::{SignOptions, Signer, SignerOrdering, SignersContainer};
use tx_builder::{BumpFee, CreateTx, FeePolicy, TxBuilder, TxParams};
use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx, DUST_LIMIT_SATOSHI};
use utils::{check_nlocktime, check_nsequence_rbf, After, Older, SecpCtx};
use crate::blockchain::{Blockchain, Progress};
use crate::blockchain::{GetHeight, NoopProgress, Progress, WalletSync};
use crate::database::memory::MemoryDatabase;
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils, SyncTime};
use crate::descriptor::derived::AsDerived;
@@ -75,16 +75,17 @@ const CACHE_ADDR_BATCH_SIZE: u32 = 100;
/// A Bitcoin wallet
///
/// A wallet takes descriptors, a [`database`](trait@crate::database::Database) and a
/// [`blockchain`](trait@crate::blockchain::Blockchain) and implements the basic functions that a Bitcoin wallets
/// needs to operate, like [generating addresses](Wallet::get_address), [returning the balance](Wallet::get_balance),
/// [creating transactions](Wallet::build_tx), etc.
/// The `Wallet` struct acts as a way of coherently interfacing with output descriptors and related transactions.
/// Its main components are:
///
/// A wallet can be either "online" if the [`blockchain`](crate::blockchain) type provided
/// implements [`Blockchain`], or "offline" if it is the unit type `()`. Offline wallets only expose
/// methods that don't need any interaction with the blockchain to work.
/// 1. output *descriptors* from which it can derive addresses.
/// 2. A [`Database`] where it tracks transactions and utxos related to the descriptors.
/// 3. [`Signer`]s that can contribute signatures to addresses instantiated from the descriptors.
///
/// [`Database`]: crate::database::Database
/// [`Signer`]: crate::signer::Signer
#[derive(Debug)]
pub struct Wallet<B, D> {
pub struct Wallet<D> {
descriptor: ExtendedDescriptor,
change_descriptor: Option<ExtendedDescriptor>,
@@ -95,88 +96,11 @@ pub struct Wallet<B, D> {
network: Network,
current_height: Option<u32>,
client: B,
database: RefCell<D>,
secp: SecpCtx,
}
impl<D> Wallet<(), D>
where
D: BatchDatabase,
{
/// Create a new "offline" wallet
pub fn new_offline<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
network: Network,
database: D,
) -> Result<Self, Error> {
Self::_new(descriptor, change_descriptor, network, database, (), None)
}
}
impl<B, D> Wallet<B, D>
where
D: BatchDatabase,
{
fn _new<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
network: Network,
mut database: D,
client: B,
current_height: Option<u32>,
) -> Result<Self, Error> {
let secp = Secp256k1::new();
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)?;
database.check_descriptor_checksum(
KeychainKind::External,
get_checksum(&descriptor.to_string())?.as_bytes(),
)?;
let signers = Arc::new(SignersContainer::from(keymap));
let (change_descriptor, change_signers) = match change_descriptor {
Some(desc) => {
let (change_descriptor, change_keymap) =
into_wallet_descriptor_checked(desc, &secp, network)?;
database.check_descriptor_checksum(
KeychainKind::Internal,
get_checksum(&change_descriptor.to_string())?.as_bytes(),
)?;
let change_signers = Arc::new(SignersContainer::from(change_keymap));
// if !parsed.same_structure(descriptor.as_ref()) {
// return Err(Error::DifferentDescriptorStructure);
// }
(Some(change_descriptor), change_signers)
}
None => (None, Arc::new(SignersContainer::new())),
};
Ok(Wallet {
descriptor,
change_descriptor,
signers,
change_signers,
address_validators: Vec::new(),
network,
current_height,
client,
database: RefCell::new(database),
secp,
})
}
/// Get the Bitcoin network the wallet is using.
pub fn network(&self) -> Network {
self.network
}
}
/// The address index selection strategy to use to derived an address from the wallet's external
/// descriptor. See [`Wallet::get_address`]. If you're unsure which one to use use `WalletIndex::New`.
#[derive(Debug)]
@@ -232,11 +156,72 @@ impl fmt::Display for AddressInfo {
}
}
// offline actions, always available
impl<B, D> Wallet<B, D>
#[derive(Debug, Default)]
/// Options to a [`Wallet::sync`]
pub struct SyncOptions {
/// The progress tracker which may be informated when progress is made.
pub progress: Option<Box<dyn Progress>>,
/// The maximum number of addresses sync on.
pub max_addresses: Option<u32>,
}
impl<D> Wallet<D>
where
D: BatchDatabase,
{
/// Create a wallet.
///
/// The only way this can fail is if the descriptors passed in do not match the checksums in `database`.
pub fn new<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
network: Network,
mut database: D,
) -> Result<Self, Error> {
let secp = Secp256k1::new();
let (descriptor, keymap) = into_wallet_descriptor_checked(descriptor, &secp, network)?;
database.check_descriptor_checksum(
KeychainKind::External,
get_checksum(&descriptor.to_string())?.as_bytes(),
)?;
let signers = Arc::new(SignersContainer::from(keymap));
let (change_descriptor, change_signers) = match change_descriptor {
Some(desc) => {
let (change_descriptor, change_keymap) =
into_wallet_descriptor_checked(desc, &secp, network)?;
database.check_descriptor_checksum(
KeychainKind::Internal,
get_checksum(&change_descriptor.to_string())?.as_bytes(),
)?;
let change_signers = Arc::new(SignersContainer::from(change_keymap));
// if !parsed.same_structure(descriptor.as_ref()) {
// return Err(Error::DifferentDescriptorStructure);
// }
(Some(change_descriptor), change_signers)
}
None => (None, Arc::new(SignersContainer::new())),
};
Ok(Wallet {
descriptor,
change_descriptor,
signers,
change_signers,
address_validators: Vec::new(),
network,
database: RefCell::new(database),
secp,
})
}
/// Get the Bitcoin network the wallet is using.
pub fn network(&self) -> Network {
self.network
}
// Return a newly derived address using the external descriptor
fn get_new_address(&self) -> Result<AddressInfo, Error> {
let incremented_index = self.fetch_and_increment_index(KeychainKind::External)?;
@@ -422,7 +407,7 @@ where
/// ```
///
/// [`TxBuilder`]: crate::TxBuilder
pub fn build_tx(&self) -> TxBuilder<'_, B, D, DefaultCoinSelectionAlgorithm, CreateTx> {
pub fn build_tx(&self) -> TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, CreateTx> {
TxBuilder {
wallet: self,
params: TxParams::default(),
@@ -601,7 +586,7 @@ where
let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
for (index, (script_pubkey, value)) in recipients.enumerate() {
if value.is_dust() && !script_pubkey.is_provably_unspendable() {
if value.is_dust(script_pubkey) && !script_pubkey.is_provably_unspendable() {
return Err(Error::OutputBelowDustLimit(index));
}
@@ -677,9 +662,9 @@ where
if tx.output.is_empty() {
if params.drain_to.is_some() {
if drain_val.is_dust() {
if drain_val.is_dust(&drain_output.script_pubkey) {
return Err(Error::InsufficientFunds {
needed: DUST_LIMIT_SATOSHI,
needed: drain_output.script_pubkey.dust_value().as_sat(),
available: drain_val,
});
}
@@ -688,7 +673,7 @@ where
}
}
if drain_val.is_dust() {
if drain_val.is_dust(&drain_output.script_pubkey) {
fee_amount += drain_val;
} else {
drain_output.value = drain_val;
@@ -763,7 +748,7 @@ where
pub fn build_fee_bump(
&self,
txid: Txid,
) -> Result<TxBuilder<'_, B, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
) -> Result<TxBuilder<'_, D, DefaultCoinSelectionAlgorithm, BumpFee>, Error> {
let mut details = match self.database.borrow().get_tx(&txid, true)? {
None => return Err(Error::TransactionNotFound),
Some(tx) if tx.transaction.is_none() => return Err(Error::TransactionNotFound),
@@ -994,7 +979,11 @@ where
.borrow()
.get_tx(&input.previous_output.txid, false)?
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(u32::MAX));
let current_height = sign_options.assume_height.or(self.current_height);
let last_sync_height = self
.database()
.get_sync_time()?
.map(|sync_time| sync_time.block_time.height);
let current_height = sign_options.assume_height.or(last_sync_height);
debug!(
"Input #{} - {}, using `create_height` = {:?}, `current_height` = {:?}",
@@ -1452,47 +1441,26 @@ where
pub fn database(&self) -> impl std::ops::Deref<Target = D> + '_ {
self.database.borrow()
}
}
impl<B, D> Wallet<B, D>
where
B: Blockchain,
D: BatchDatabase,
{
/// Create a new "online" wallet
#[maybe_async]
pub fn new<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
network: Network,
database: D,
client: B,
) -> Result<Self, Error> {
let current_height = Some(maybe_await!(client.get_height())? as u32);
Self::_new(
descriptor,
change_descriptor,
network,
database,
client,
current_height,
)
}
/// Sync the internal database with the blockchain
#[maybe_async]
pub fn sync<P: 'static + Progress>(
pub fn sync<B: WalletSync + GetHeight>(
&self,
progress_update: P,
max_address_param: Option<u32>,
blockchain: &B,
sync_opts: SyncOptions,
) -> Result<(), Error> {
debug!("Begin sync...");
let mut run_setup = false;
let SyncOptions {
max_addresses,
progress,
} = sync_opts;
let progress = progress.unwrap_or_else(|| Box::new(NoopProgress));
let max_address = match self.descriptor.is_deriveable() {
false => 0,
true => max_address_param.unwrap_or(CACHE_ADDR_BATCH_SIZE),
true => max_addresses.unwrap_or(CACHE_ADDR_BATCH_SIZE),
};
debug!("max_address {}", max_address);
if self
@@ -1509,7 +1477,7 @@ where
if let Some(change_descriptor) = &self.change_descriptor {
let max_address = match change_descriptor.is_deriveable() {
false => 0,
true => max_address_param.unwrap_or(CACHE_ADDR_BATCH_SIZE),
true => max_addresses.unwrap_or(CACHE_ADDR_BATCH_SIZE),
};
if self
@@ -1528,13 +1496,11 @@ where
// TODO: what if i generate an address first and cache some addresses?
// TODO: we should sync if generating an address triggers a new batch to be stored
if run_setup {
maybe_await!(self
.client
.setup(self.database.borrow_mut().deref_mut(), progress_update,))?;
maybe_await!(
blockchain.wallet_setup(self.database.borrow_mut().deref_mut(), progress,)
)?;
} else {
maybe_await!(self
.client
.sync(self.database.borrow_mut().deref_mut(), progress_update,))?;
maybe_await!(blockchain.wallet_sync(self.database.borrow_mut().deref_mut(), progress,))?;
}
#[cfg(feature = "verify")]
@@ -1545,7 +1511,7 @@ where
verify::verify_tx(
tx.transaction.as_ref().ok_or(Error::TransactionNotFound)?,
self.database.borrow().deref(),
&self.client,
blockchain,
)?;
tx.verified = true;
@@ -1556,7 +1522,7 @@ where
let sync_time = SyncTime {
block_time: BlockTime {
height: maybe_await!(self.client.get_height())?,
height: maybe_await!(blockchain.get_height())?,
timestamp: time::get_timestamp(),
},
};
@@ -1565,31 +1531,18 @@ where
Ok(())
}
/// Return a reference to the internal blockchain client
pub fn client(&self) -> &B {
&self.client
}
/// Broadcast a transaction to the network
#[maybe_async]
pub fn broadcast(&self, tx: &Transaction) -> Result<Txid, Error> {
maybe_await!(self.client.broadcast(tx))?;
Ok(tx.txid())
}
}
/// Return a fake wallet that appears to be funded for testing.
pub fn get_funded_wallet(
descriptor: &str,
) -> (
Wallet<(), MemoryDatabase>,
Wallet<MemoryDatabase>,
(String, Option<String>),
bitcoin::Txid,
) {
let descriptors = testutils!(@descriptors (descriptor));
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Regtest,
@@ -1639,7 +1592,7 @@ pub(crate) mod test {
#[test]
fn test_cache_addresses_fixed() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
"wpkh(L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6)",
None,
Network::Testnet,
@@ -1673,7 +1626,7 @@ pub(crate) mod test {
#[test]
fn test_cache_addresses() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
@@ -1701,7 +1654,7 @@ pub(crate) mod test {
#[test]
fn test_cache_addresses_refill() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap();
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)", None, Network::Testnet, db).unwrap();
assert_eq!(
wallet.get_address(New).unwrap().to_string(),
@@ -3424,7 +3377,7 @@ pub(crate) mod test {
.unwrap();
let mut builder = wallet.build_fee_bump(txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(140.0));
builder.fee_rate(FeeRate::from_sat_per_vb(141.0));
let (psbt, details) = builder.finish().unwrap();
assert_eq!(
@@ -3799,7 +3752,7 @@ pub(crate) mod test {
#[test]
fn test_unused_address() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
assert_eq!(
@@ -3816,7 +3769,7 @@ pub(crate) mod test {
fn test_next_unused_address() {
let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let descriptors = testutils!(@descriptors (descriptor));
let wallet = Wallet::new_offline(
let wallet = Wallet::new(
&descriptors.0,
None,
Network::Testnet,
@@ -3845,7 +3798,7 @@ pub(crate) mod test {
#[test]
fn test_peek_address_at_index() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
assert_eq!(
@@ -3878,7 +3831,7 @@ pub(crate) mod test {
#[test]
fn test_peek_address_at_index_not_derivable() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
None, Network::Testnet, db).unwrap();
assert_eq!(
@@ -3900,7 +3853,7 @@ pub(crate) mod test {
#[test]
fn test_reset_address_index() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
// new index 0
@@ -3937,7 +3890,7 @@ pub(crate) mod test {
#[test]
fn test_returns_index_and_address() {
let db = MemoryDatabase::new();
let wallet = Wallet::new_offline("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
None, Network::Testnet, db).unwrap();
// new index 0

View File

@@ -72,7 +72,7 @@
//! let custom_signer = CustomSigner::connect();
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
//! let mut wallet = Wallet::new_offline(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),

View File

@@ -120,8 +120,8 @@ impl TxBuilderContext for BumpFee {}
/// [`finish`]: Self::finish
/// [`coin_selection`]: Self::coin_selection
#[derive(Debug)]
pub struct TxBuilder<'a, B, D, Cs, Ctx> {
pub(crate) wallet: &'a Wallet<B, D>,
pub struct TxBuilder<'a, D, Cs, Ctx> {
pub(crate) wallet: &'a Wallet<D>,
pub(crate) params: TxParams,
pub(crate) coin_selection: Cs,
pub(crate) phantom: PhantomData<Ctx>,
@@ -170,7 +170,7 @@ impl std::default::Default for FeePolicy {
}
}
impl<'a, Cs: Clone, Ctx, B, D> Clone for TxBuilder<'a, B, D, Cs, Ctx> {
impl<'a, Cs: Clone, Ctx, D> Clone for TxBuilder<'a, D, Cs, Ctx> {
fn clone(&self) -> Self {
TxBuilder {
wallet: self.wallet,
@@ -182,8 +182,8 @@ impl<'a, Cs: Clone, Ctx, B, D> Clone for TxBuilder<'a, B, D, Cs, Ctx> {
}
// methods supported by both contexts, for any CoinSelectionAlgorithm
impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
TxBuilder<'a, B, D, Cs, Ctx>
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
TxBuilder<'a, D, Cs, Ctx>
{
/// Set a custom fee rate
pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
@@ -508,7 +508,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
self,
coin_selection: P,
) -> TxBuilder<'a, B, D, P, Ctx> {
) -> TxBuilder<'a, D, P, Ctx> {
TxBuilder {
wallet: self.wallet,
params: self.params,
@@ -547,7 +547,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderConte
}
}
impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D, Cs, CreateTx> {
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
/// Replace the recipients already added with a new list
pub fn set_recipients(&mut self, recipients: Vec<(Script, u64)>) -> &mut Self {
self.params.recipients = recipients;
@@ -614,7 +614,7 @@ impl<'a, B, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, B, D,
}
// methods supported only by bump_fee
impl<'a, B, D: BatchDatabase> TxBuilder<'a, B, D, DefaultCoinSelectionAlgorithm, BumpFee> {
impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
/// Explicitly tells the wallet that it is allowed to reduce the fee of the output matching this
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
/// will attempt to find a change output to shrink instead.

View File

@@ -9,13 +9,11 @@
// You may not use this file except in accordance with one or both of these
// licenses.
use bitcoin::blockdata::script::Script;
use bitcoin::secp256k1::{All, Secp256k1};
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
// De-facto standard "dust limit" (even though it should change based on the output type)
pub const DUST_LIMIT_SATOSHI: u64 = 546;
// MSB of the nSequence. If set there's no consensus-constraint, so it must be disabled when
// spending using CSV in order to enforce CSV rules
pub(crate) const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 1 << 31;
@@ -28,18 +26,19 @@ pub(crate) const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000FFFF;
// Threshold for nLockTime to be considered a block-height-based timelock rather than time-based
pub(crate) const BLOCKS_TIMELOCK_THRESHOLD: u32 = 500000000;
/// Trait to check if a value is below the dust limit
/// Trait to check if a value is below the dust limit.
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
/// keep it compatible with network dust rate
// we implement this trait to make sure we don't mess up the comparison with off-by-one like a <
// instead of a <= etc. The constant value for the dust limit is not public on purpose, to
// encourage the usage of this trait.
// instead of a <= etc.
pub trait IsDust {
/// Check whether or not a value is below dust limit
fn is_dust(&self) -> bool;
fn is_dust(&self, script: &Script) -> bool;
}
impl IsDust for u64 {
fn is_dust(&self) -> bool {
*self <= DUST_LIMIT_SATOSHI
fn is_dust(&self, script: &Script) -> bool {
*self < script.dust_value().as_sat()
}
}
@@ -141,10 +140,29 @@ pub(crate) type SecpCtx = Secp256k1<All>;
#[cfg(test)]
mod test {
use super::{
check_nlocktime, check_nsequence_rbf, BLOCKS_TIMELOCK_THRESHOLD,
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD,
SEQUENCE_LOCKTIME_TYPE_FLAG,
};
use crate::bitcoin::Address;
use crate::types::FeeRate;
use std::str::FromStr;
#[test]
fn test_is_dust() {
let script_p2pkh = Address::from_str("1GNgwA8JfG7Kc8akJ8opdNWJUihqUztfPe")
.unwrap()
.script_pubkey();
assert!(script_p2pkh.is_p2pkh());
assert!(545.is_dust(&script_p2pkh));
assert!(!546.is_dust(&script_p2pkh));
let script_p2wpkh = Address::from_str("bc1qxlh2mnc0yqwas76gqq665qkggee5m98t8yskd8")
.unwrap()
.script_pubkey();
assert!(script_p2wpkh.is_v0_p2wpkh());
assert!(293.is_dust(&script_p2wpkh));
assert!(!294.is_dust(&script_p2wpkh));
}
#[test]
fn test_fee_from_btc_per_kb() {

BIN
static/bdk.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB