feat!: Rework sqlite, changesets, persistence and wallet-construction

Rework sqlite: Instead of only supported one schema (defined in
`bdk_sqlite`), we have a schema per changeset type for more flexiblity.

* rm `bdk_sqlite` crate (as we don't need `bdk_sqlite::Store` anymore).
* add `sqlite` feature on `bdk_chain` which adds methods on each
  changeset type for initializing tables, loading the changeset and
  writing.

Rework changesets: Some callers may want to use `KeychainTxOutIndex`
where `K` may change per descriptor on every run. So we only want to
persist the last revealed indices by `DescriptorId` (which uniquely-ish
identifies the descriptor).

* rm `keychain_added` field from `keychain_txout`'s changeset.
* Add `keychain_added` to `CombinedChangeSet` (which is renamed to
  `WalletChangeSet`).

Rework persistence: add back some safety and convenience when persisting
our types. Working with changeset directly (as we were doing before) can
be cumbersome.

* Intoduce `struct Persisted<T>` which wraps a type `T` which stores
  staged changes to it. This adds safety when creating and or loading
  `T` from db.
* `struct Persisted<T>` methods, `create`, `load` and `persist`, are
  avaliable if `trait PersistWith<Db>` is implemented for `T`. `Db`
  represents the database connection and `PersistWith` should be
  implemented per database-type.
* For async, we have `trait PersistedAsyncWith<Db>`.
* `Wallet` has impls of `PersistedWith<rusqlite::Connection>`,
  `PersistedWith<rusqlite::Transaction>` and
  `PersistedWith<bdk_file_store::Store>` by default.

Rework wallet-construction: Before, we had multiple methods for loading
and creating with different input-counts so it would be unwieldly to add
more parameters in the future. This also makes it difficult to impl
`PersistWith` (which has a single method for `load` that takes in
`PersistWith::LoadParams` and a single method for `create` that takes in
`PersistWith::CreateParams`).

* Introduce a builder pattern when constructing a `Wallet`. For loading
  from persistence or `ChangeSet`, we have `LoadParams`. For creating a
  new wallet, we have `CreateParams`.
This commit is contained in:
志宇
2024-07-11 04:49:01 +00:00
parent d99b3ef4b4
commit 6b43001951
49 changed files with 2217 additions and 2058 deletions

View File

@@ -19,22 +19,26 @@ bitcoin = { version = "0.32.0", features = ["serde", "base64"], default-features
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false }
bdk_file_store = { path = "../file_store", version = "0.13.0", optional = true }
# Optional dependencies
bip39 = { version = "2.0", optional = true }
[features]
default = ["std"]
default = ["std", "file_store"]
std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
compiler = ["miniscript/compiler"]
all-keys = ["keys-bip39"]
keys-bip39 = ["bip39"]
sqlite = ["bdk_chain/sqlite"]
file_store = ["bdk_file_store"]
[dev-dependencies]
lazy_static = "1.4"
assert_matches = "1.5.0"
tempfile = "3"
bdk_sqlite = { path = "../sqlite" }
bdk_chain = { path = "../chain", features = ["sqlite"] }
bdk_wallet = { path = ".", features = ["sqlite", "file_store"] }
bdk_file_store = { path = "../file_store" }
anyhow = "1"
rand = "^0.8"

View File

@@ -57,18 +57,17 @@ that the `Wallet` can use to update its view of the chain.
## Persistence
To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::CombinedChangeSet`].
To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::WalletChangeSet`].
**Implementations**
* [`bdk_file_store`]: Stores wallet changes in a simple flat file.
* [`bdk_sqlite`]: Stores wallet changes in a SQLite relational database file.
**Example**
<!-- compile_fail because outpoint and txout are fake variables -->
```rust,no_run
use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
use bdk_wallet::{bitcoin::Network, CreateParams, LoadParams, KeychainKind, ChangeSet};
// Open or create a new file store for wallet data.
let mut db =
@@ -76,21 +75,22 @@ let mut db =
.expect("create store");
// Create a wallet with initial wallet data read from the file store.
let network = Network::Testnet;
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
let changeset = db.aggregate_changesets().expect("changeset loaded");
let mut wallet =
Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet)
.expect("create or load wallet");
let load_params = LoadParams::with_descriptors(descriptor, change_descriptor, network)
.expect("must parse descriptors");
let create_params = CreateParams::new(descriptor, change_descriptor, network)
.expect("must parse descriptors");
let mut wallet = match load_params.load_wallet(&mut db).expect("wallet") {
Some(wallet) => wallet,
None => create_params.create_wallet(&mut db).expect("wallet"),
};
// Get a new address to receive bitcoin.
let receive_address = wallet.reveal_next_address(KeychainKind::External);
// Persist staged wallet data changes to the file store.
let staged_changeset = wallet.take_staged();
if let Some(changeset) = staged_changeset {
db.append_changeset(&changeset)
.expect("must commit changes to database");
}
wallet.persist(&mut db).expect("persist");
println!("Your new receive address is: {}", receive_address.address);
```
@@ -233,7 +233,6 @@ conditions.
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
[`bdk_sqlite`]: https://docs.rs/bdk_sqlite/latest
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest

View File

@@ -21,7 +21,7 @@ use bitcoin::Network;
use miniscript::policy::Concrete;
use miniscript::Descriptor;
use bdk_wallet::{KeychainKind, Wallet};
use bdk_wallet::{CreateParams, KeychainKind};
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
@@ -77,7 +77,8 @@ fn main() -> Result<(), Box<dyn Error>> {
);
// Create a new wallet from descriptors
let mut wallet = Wallet::new(&descriptor, &internal_descriptor, Network::Regtest)?;
let mut wallet = CreateParams::new(&descriptor, &internal_descriptor, Network::Regtest)?
.create_wallet_no_persist()?;
println!(
"First derived address from the descriptor: \n{}",

View File

@@ -281,15 +281,10 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
}
}
/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the
/// descriptor
pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
inner: T,
secp: &SecpCtx,
network: Network,
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
/// Extra checks for [`ExtendedDescriptor`].
pub(crate) fn check_wallet_descriptor(
descriptor: &Descriptor<DescriptorPublicKey>,
) -> Result<(), DescriptorError> {
// Ensure the keys don't contain any hardened derivation steps or hardened wildcards
let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
if let DescriptorPublicKey::XPub(DescriptorXKey {
@@ -316,7 +311,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
// issues
descriptor.sanity_check()?;
Ok((descriptor, keymap))
Ok(())
}
#[doc(hidden)]
@@ -855,22 +850,31 @@ mod test {
}
#[test]
fn test_into_wallet_descriptor_checked() {
fn test_check_wallet_descriptor() {
let secp = Secp256k1::new();
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.expect("must parse");
let result = check_wallet_descriptor(&descriptor);
assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.expect("must parse");
let result = check_wallet_descriptor(&descriptor);
assert_matches!(result, Err(DescriptorError::MultiPath));
// repeated pubkeys
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.expect("must parse");
let result = check_wallet_descriptor(&descriptor);
assert!(result.is_err());
}
@@ -882,8 +886,10 @@ mod test {
let secp = Secp256k1::new();
let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
let (descriptor, _) =
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
let (descriptor, _) = descriptor
.into_wallet_descriptor(&secp, Network::Testnet)
.unwrap();
check_wallet_descriptor(&descriptor).expect("descriptor");
let descriptor = descriptor.at_derivation_index(0).unwrap();

View File

@@ -73,7 +73,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Pkh;
///
@@ -81,7 +81,8 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?;
/// let mut wallet = CreateParams::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(
/// wallet
@@ -105,7 +106,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh_P2Sh;
///
@@ -113,11 +114,12 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// P2Wpkh_P2Sh(key_external),
/// P2Wpkh_P2Sh(key_internal),
/// Network::Testnet,
/// )?;
/// )?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(
/// wallet
@@ -142,7 +144,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet};
/// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2Wpkh;
///
@@ -150,7 +152,9 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?;
/// let mut wallet =
/// CreateParams::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(
/// wallet
@@ -174,7 +178,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
///
/// ```
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::CreateParams;
/// # use bdk_wallet::KeychainKind;
/// use bdk_wallet::template::P2TR;
///
@@ -182,7 +186,8 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
/// let key_internal =
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
/// let mut wallet = Wallet::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?;
/// let mut wallet = CreateParams::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(
/// wallet
@@ -211,15 +216,16 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip44;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// Bip44(key.clone(), KeychainKind::External),
/// Bip44(key, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
@@ -247,16 +253,17 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip44Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip44Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
@@ -284,15 +291,16 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip49;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// Bip49(key.clone(), KeychainKind::External),
/// Bip49(key, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
@@ -320,16 +328,17 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip49Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip49Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
@@ -357,15 +366,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip84;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// Bip84(key.clone(), KeychainKind::External),
/// Bip84(key, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
@@ -393,16 +403,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip84Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip84Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )?.create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
@@ -430,15 +440,16 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip86;
///
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// Bip86(key.clone(), KeychainKind::External),
/// Bip86(key, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
@@ -466,16 +477,17 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
/// ```
/// # use std::str::FromStr;
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// use bdk_wallet::template::Bip86Public;
///
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
/// let mut wallet = Wallet::new(
/// let mut wallet = CreateParams::new(
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
/// Bip86Public(key, fingerprint, KeychainKind::Internal),
/// Network::Testnet,
/// )?;
/// )?
/// .create_wallet_no_persist()?;
///
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");

View File

@@ -36,12 +36,22 @@ pub use types::*;
pub use wallet::signer;
pub use wallet::signer::SignOptions;
pub use wallet::tx_builder::TxBuilder;
pub use wallet::ChangeSet;
pub use wallet::CreateParams;
pub use wallet::LoadParams;
pub use wallet::PersistedWallet;
pub use wallet::Wallet;
/// Get the version of BDK at runtime
/// Get the version of [`bdk_wallet`](crate) at runtime.
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION", "unknown")
}
pub use bdk_chain as chain;
pub(crate) use bdk_chain::collections;
#[cfg(feature = "sqlite")]
pub use bdk_chain::rusqlite;
#[cfg(feature = "sqlite")]
pub use bdk_chain::sqlite;
pub use chain::WalletChangeSet;

View File

@@ -29,11 +29,12 @@
//! }"#;
//!
//! let import = FullyNodedExport::from_str(import)?;
//! let wallet = Wallet::new(
//! let wallet = CreateParams::new(
//! &import.descriptor(),
//! &import.change_descriptor().expect("change descriptor"),
//! Network::Testnet,
//! )?;
//! )?
//! .create_wallet_no_persist()?;
//! # Ok::<_, Box<dyn std::error::Error>>(())
//! ```
//!
@@ -42,11 +43,12 @@
//! # use bitcoin::*;
//! # use bdk_wallet::wallet::export::*;
//! # use bdk_wallet::*;
//! let wallet = Wallet::new(
//! let wallet = CreateParams::new(
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)",
//! Network::Testnet,
//! )?;
//! )?
//! .create_wallet_no_persist()?;
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
//!
//! println!("Exported: {}", export.to_string());
@@ -219,12 +221,15 @@ mod test {
use bitcoin::{transaction, BlockHash, Network, Transaction};
use super::*;
use crate::wallet::Wallet;
use crate::wallet::{CreateParams, Wallet};
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
use crate::wallet::Update;
use bdk_chain::TxGraph;
let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
let mut wallet = CreateParams::new(descriptor, change_descriptor, network)
.expect("must parse descriptors")
.create_wallet_no_persist()
.expect("must create wallet");
let transaction = Transaction {
input: vec![],
output: vec![],

View File

@@ -18,7 +18,7 @@
//! # use bdk_wallet::signer::SignerOrdering;
//! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
//! # use bdk_wallet::wallet::AddressIndex::New;
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
//! # use bdk_wallet::{CreateParams, KeychainKind, SignOptions};
//! # use hwi::HWIClient;
//! # use std::sync::Arc;
//! #
@@ -30,11 +30,7 @@
//! let first_device = devices.remove(0)?;
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
//!
//! # let mut wallet = Wallet::new(
//! # "",
//! # None,
//! # Network::Testnet,
//! # )?;
//! # let mut wallet = CreateParams::new("", "", Network::Testnet)?.create_wallet_no_persist()?;
//! #
//! // Adding the hardware signer to the BDK wallet
//! wallet.add_signer(

View File

@@ -12,7 +12,10 @@
//! Wallet
//!
//! This module defines the [`Wallet`].
use crate::collections::{BTreeMap, HashMap};
use crate::{
collections::{BTreeMap, HashMap},
descriptor::check_wallet_descriptor,
};
use alloc::{
boxed::Box,
string::{String, ToString},
@@ -28,8 +31,8 @@ use bdk_chain::{
},
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
tx_graph::{CanonicalTx, TxGraph, TxNode},
BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, FullTxOut, Indexed,
IndexedTxGraph, Merge,
BlockId, ChainPosition, ConfirmationBlockTime, ConfirmationTime, DescriptorExt, FullTxOut,
Indexed, IndexedTxGraph, Merge,
};
use bitcoin::sighash::{EcdsaSighashType, TapSighashType};
use bitcoin::{
@@ -38,24 +41,28 @@ use bitcoin::{
};
use bitcoin::{consensus::encode::serialize, transaction, BlockHash, Psbt};
use bitcoin::{constants::genesis_block, Amount};
use bitcoin::{
secp256k1::{All, Secp256k1},
Weight,
};
use bitcoin::{secp256k1::Secp256k1, Weight};
use core::fmt;
use core::mem;
use core::ops::Deref;
use rand_core::RngCore;
use descriptor::error::Error as DescriptorError;
use miniscript::psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier};
use miniscript::{
descriptor::KeyMap,
psbt::{PsbtExt, PsbtInputExt, PsbtInputSatisfier},
};
use bdk_chain::tx_graph::CalculateFeeError;
pub mod coin_selection;
pub mod export;
mod params;
pub mod signer;
pub mod tx_builder;
pub use params::*;
mod persisted;
pub use persisted::*;
pub(crate) mod utils;
pub mod error;
@@ -69,8 +76,8 @@ use utils::{check_nsequence_rbf, After, Older, SecpCtx};
use crate::descriptor::policy::BuildSatisfaction;
use crate::descriptor::{
self, calc_checksum, into_wallet_descriptor_checked, DerivedDescriptor, DescriptorMeta,
ExtendedDescriptor, ExtractPolicy, IntoWalletDescriptor, Policy, XKeyUtils,
self, calc_checksum, DerivedDescriptor, DescriptorMeta, ExtendedDescriptor, ExtractPolicy,
IntoWalletDescriptor, Policy, XKeyUtils,
};
use crate::psbt::PsbtUtils;
use crate::signer::SignerError;
@@ -149,7 +156,7 @@ impl From<SyncResult> for Update {
}
/// The changes made to a wallet by applying an [`Update`].
pub type ChangeSet = bdk_chain::CombinedChangeSet<KeychainKind, ConfirmationBlockTime>;
pub type ChangeSet = bdk_chain::WalletChangeSet;
/// A derived address and the index it was found at.
/// For convenience this automatically derefs to `Address`
@@ -177,34 +184,7 @@ impl fmt::Display for AddressInfo {
}
}
/// The error type when constructing a fresh [`Wallet`].
///
/// Methods [`new`] and [`new_with_genesis_hash`] may return this error.
///
/// [`new`]: Wallet::new
/// [`new_with_genesis_hash`]: Wallet::new_with_genesis_hash
#[derive(Debug)]
pub enum NewError {
/// There was problem with the passed-in descriptor(s).
Descriptor(crate::descriptor::DescriptorError),
}
impl fmt::Display for NewError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NewError::Descriptor(e) => e.fmt(f),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for NewError {}
/// The error type when loading a [`Wallet`] from a [`ChangeSet`].
///
/// Method [`load_from_changeset`] may return this error.
///
/// [`load_from_changeset`]: Wallet::load_from_changeset
#[derive(Debug)]
pub enum LoadError {
/// There was a problem with the passed-in descriptor(s).
@@ -215,6 +195,8 @@ pub enum LoadError {
MissingGenesis,
/// Data loaded from persistence is missing descriptor.
MissingDescriptor(KeychainKind),
/// Data loaded is unexpected.
Mismatch(LoadMismatch),
}
impl fmt::Display for LoadError {
@@ -226,6 +208,7 @@ impl fmt::Display for LoadError {
LoadError::MissingDescriptor(k) => {
write!(f, "loaded data is missing descriptor for keychain {k:?}")
}
LoadError::Mismatch(mismatch) => write!(f, "data mismatch: {mismatch:?}"),
}
}
}
@@ -233,63 +216,34 @@ impl fmt::Display for LoadError {
#[cfg(feature = "std")]
impl std::error::Error for LoadError {}
/// Error type for when we try load a [`Wallet`] from persistence and creating it if non-existent.
///
/// Methods [`new_or_load`] and [`new_or_load_with_genesis_hash`] may return this error.
///
/// [`new_or_load`]: Wallet::new_or_load
/// [`new_or_load_with_genesis_hash`]: Wallet::new_or_load_with_genesis_hash
/// Represents a mismatch with what is loaded and what is expected from [`LoadParams`].
#[derive(Debug)]
pub enum NewOrLoadError {
/// There is a problem with the passed-in descriptor.
Descriptor(crate::descriptor::DescriptorError),
/// The loaded genesis hash does not match what was provided.
LoadedGenesisDoesNotMatch {
/// The expected genesis block hash.
expected: BlockHash,
/// The block hash loaded from persistence.
got: Option<BlockHash>,
},
/// The loaded network type does not match what was provided.
LoadedNetworkDoesNotMatch {
/// The expected network type.
pub enum LoadMismatch {
/// Network does not match.
Network {
/// The network that is loaded.
loaded: Network,
/// The expected network.
expected: Network,
/// The network type loaded from persistence.
got: Option<Network>,
},
/// The loaded desccriptor does not match what was provided.
LoadedDescriptorDoesNotMatch {
/// The descriptor loaded from persistence.
got: Option<ExtendedDescriptor>,
/// The keychain of the descriptor not matching
/// Genesis hash does not match.
Genesis {
/// The genesis hash that is loaded.
loaded: BlockHash,
/// The expected genesis hash.
expected: BlockHash,
},
/// Descriptor's [`DescriptorId`](bdk_chain::DescriptorId) does not match.
Descriptor {
/// Keychain identifying the descriptor.
keychain: KeychainKind,
/// The loaded descriptor.
loaded: ExtendedDescriptor,
/// The expected descriptor.
expected: ExtendedDescriptor,
},
}
impl fmt::Display for NewOrLoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
NewOrLoadError::Descriptor(e) => e.fmt(f),
NewOrLoadError::LoadedGenesisDoesNotMatch { expected, got } => {
write!(f, "loaded genesis hash is not {}, got {:?}", expected, got)
}
NewOrLoadError::LoadedNetworkDoesNotMatch { expected, got } => {
write!(f, "loaded network type is not {}, got {:?}", expected, got)
}
NewOrLoadError::LoadedDescriptorDoesNotMatch { got, keychain } => {
write!(
f,
"loaded descriptor is different from what was provided, got {:?} for keychain {:?}",
got, keychain
)
}
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for NewOrLoadError {}
/// An error that may occur when applying a block to [`Wallet`].
#[derive(Debug)]
pub enum ApplyBlockError {
@@ -324,39 +278,81 @@ impl fmt::Display for ApplyBlockError {
impl std::error::Error for ApplyBlockError {}
impl Wallet {
/// Initialize an empty [`Wallet`].
pub fn new<E: IntoWalletDescriptor>(
/// Build a new [`Wallet`].
///
/// If you have previously created a wallet, use [`load`](Self::load) instead.
///
/// # Synopsis
///
/// ```rust
/// # use bdk_wallet::Wallet;
/// # use bitcoin::Network;
/// # fn main() -> anyhow::Result<()> {
/// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
/// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
/// // Create a non-persisted wallet.
/// let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
/// .create_wallet_no_persist()?;
///
/// // Create a wallet that is persisted to SQLite database.
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
/// # let file_path = temp_dir.path().join("store.db");
/// use bdk_wallet::rusqlite::Connection;
/// let mut conn = Connection::open(file_path)?;
/// let wallet = Wallet::create(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
/// .create_wallet(&mut conn)?;
/// # Ok(())
/// # }
/// ```
pub fn create<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
network: Network,
) -> Result<Self, NewError> {
let genesis_hash = genesis_block(network).block_hash();
Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash)
) -> Result<CreateParams, DescriptorError> {
CreateParams::new(descriptor, change_descriptor, network)
}
/// Initialize an empty [`Wallet`] with a custom genesis hash.
/// Create a new [`Wallet`] with given `params`.
///
/// This is like [`Wallet::new`] with an additional `genesis_hash` parameter. This is useful
/// for syncing from alternative networks.
pub fn new_with_genesis_hash<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
network: Network,
genesis_hash: BlockHash,
) -> Result<Self, NewError> {
let secp = Secp256k1::new();
/// If you have previously created a wallet, use [`load`](Self::load) instead.
pub fn create_with_params(params: CreateParams) -> Result<Self, DescriptorError> {
let secp = params.secp;
let network = params.network;
let genesis_hash = params
.genesis_hash
.unwrap_or(genesis_block(network).block_hash());
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
let (signers, change_signers) =
create_signers(&mut index, &secp, descriptor, change_descriptor, network)
.map_err(NewError::Descriptor)?;
check_wallet_descriptor(&params.descriptor)?;
check_wallet_descriptor(&params.change_descriptor)?;
let signers = Arc::new(SignersContainer::build(
params.descriptor_keymap,
&params.descriptor,
&secp,
));
let change_signers = Arc::new(SignersContainer::build(
params.change_descriptor_keymap,
&params.change_descriptor,
&secp,
));
let index = create_indexer(
params.descriptor,
params.change_descriptor,
params.lookahead,
)?;
let descriptor = index.get_descriptor(&KeychainKind::External).cloned();
let change_descriptor = index.get_descriptor(&KeychainKind::Internal).cloned();
let indexed_graph = IndexedTxGraph::new(index);
let indexed_graph_changeset = indexed_graph.initial_changeset();
let staged = ChangeSet {
chain: chain_changeset,
indexed_tx_graph: indexed_graph.initial_changeset(),
let stage = ChangeSet {
descriptor,
change_descriptor,
local_chain: chain_changeset,
tx_graph: indexed_graph_changeset.tx_graph,
indexer: indexed_graph_changeset.indexer,
network: Some(network),
};
@@ -366,11 +362,79 @@ impl Wallet {
network,
chain,
indexed_graph,
stage: staged,
stage,
secp,
})
}
/// Build [`Wallet`] by loading from persistence or [`ChangeSet`].
///
/// Note that the descriptor secret keys are not persisted to the db. You can either add
/// signers after-the-fact with [`Wallet::add_signer`] or [`Wallet::set_keymap`]. Or you can
/// construct wallet using [`Wallet::load_with_descriptors`].
///
/// # Synopsis
///
/// ```rust,no_run
/// # use bdk_wallet::{Wallet, ChangeSet, KeychainKind};
/// # use bitcoin::{BlockHash, Network, hashes::Hash};
/// # fn main() -> anyhow::Result<()> {
/// # const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
/// # const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
/// # let changeset = ChangeSet::default();
/// // Load a wallet from changeset (no persistence).
/// let wallet = Wallet::load()
/// .load_wallet_no_persist(changeset)?
/// .expect("must have data to load wallet");
///
/// // Load a wallet that is persisted to SQLite database.
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
/// # let file_path = temp_dir.path().join("store.db");
/// # let external_keymap = Default::default();
/// # let internal_keymap = Default::default();
/// # let genesis_hash = BlockHash::all_zeros();
/// let mut conn = bdk_wallet::rusqlite::Connection::open(file_path)?;
/// let mut wallet = Wallet::load()
/// // manually include private keys
/// // the alternative is to use `Wallet::load_with_descriptors`
/// .keymap(KeychainKind::External, external_keymap)
/// .keymap(KeychainKind::Internal, internal_keymap)
/// // set a lookahead for our indexer
/// .lookahead(101)
/// // ensure loaded wallet's genesis hash matches this value
/// .genesis_hash(genesis_hash)
/// .load_wallet(&mut conn)?
/// .expect("must have data to load wallet");
/// # Ok(())
/// # }
/// ```
pub fn load() -> LoadParams {
LoadParams::new()
}
/// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. This fails if the loaded
/// wallet has a different `network`.
///
/// Note that the descriptor secret keys are not persisted to the db. You can either add
/// signers after-the-fact with [`Wallet::add_signer`] or [`Wallet::set_keymap`]. Or you can
/// construct wallet using [`Wallet::load_with_descriptors`].
pub fn load_with_network(network: Network) -> LoadParams {
LoadParams::with_network(network)
}
/// Build [`Wallet`] by loading from persistence or [`ChangeSet`]. This fails if the loaded
/// wallet has a different `network`, `descriptor` or `change_descriptor`.
///
/// If the passed-in descriptors contains secret keys, the keys will be included in the
/// constructed wallet (which means you can sign transactions).
pub fn load_with_descriptors<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
network: Network,
) -> Result<LoadParams, DescriptorError> {
LoadParams::with_descriptors(descriptor, change_descriptor, network)
}
/// Load [`Wallet`] from the given previously persisted [`ChangeSet`].
///
/// Note that the descriptor secret keys are not persisted to the db; this means that after
@@ -382,68 +446,102 @@ impl Wallet {
///
/// ```rust,no_run
/// # use bdk_wallet::Wallet;
/// # use bdk_wallet::signer::{SignersContainer, SignerOrdering};
/// # use bdk_wallet::descriptor::Descriptor;
/// # use bitcoin::key::Secp256k1;
/// # use bdk_wallet::KeychainKind;
/// use bdk_sqlite::{Store, rusqlite::Connection};
/// # use bitcoin::Network;
/// # use bdk_wallet::{LoadParams, KeychainKind, PersistedWallet};
/// use bdk_chain::sqlite::Connection;
/// #
/// # fn main() -> Result<(), anyhow::Error> {
/// # fn main() -> anyhow::Result<()> {
/// # let temp_dir = tempfile::tempdir().expect("must create tempdir");
/// # let file_path = temp_dir.path().join("store.db");
/// let conn = Connection::open(file_path).expect("must open connection");
/// let mut db = Store::new(conn).expect("must create db");
/// let secp = Secp256k1::new();
/// const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
/// const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
///
/// let (external_descriptor, external_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)").unwrap();
/// let (internal_descriptor, internal_keymap) = Descriptor::parse_descriptor(&secp, "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)").unwrap();
/// let mut conn = Connection::open(file_path)?;
/// let mut wallet: PersistedWallet =
/// LoadParams::with_descriptors(EXTERNAL_DESC, INTERNAL_DESC, Network::Testnet)?
/// .load_wallet(&mut conn)?
/// .expect("db should have data to load wallet");
///
/// let external_signer_container = SignersContainer::build(external_keymap, &external_descriptor, &secp);
/// let internal_signer_container = SignersContainer::build(internal_keymap, &internal_descriptor, &secp);
/// let changeset = db.read()?.expect("there must be an existing changeset");
/// let mut wallet = Wallet::load_from_changeset(changeset)?;
///
/// external_signer_container.signers().into_iter()
/// .for_each(|s| wallet.add_signer(KeychainKind::External, SignerOrdering::default(), s.clone()));
/// internal_signer_container.signers().into_iter()
/// .for_each(|s| wallet.add_signer(KeychainKind::Internal, SignerOrdering::default(), s.clone()));
/// # Ok(())
/// # }
/// ```
///
/// Alternatively, you can call [`Wallet::new_or_load`], which will add the private keys of the
/// passed-in descriptors to the [`Wallet`].
pub fn load_from_changeset(changeset: ChangeSet) -> Result<Self, LoadError> {
pub fn load_with_params(
changeset: ChangeSet,
params: LoadParams,
) -> Result<Option<Self>, LoadError> {
if changeset.is_empty() {
return Ok(None);
}
let secp = Secp256k1::new();
let network = changeset.network.ok_or(LoadError::MissingNetwork)?;
let chain =
LocalChain::from_changeset(changeset.chain).map_err(|_| LoadError::MissingGenesis)?;
let mut index = KeychainTxOutIndex::<KeychainKind>::default();
let descriptor = changeset
.indexed_tx_graph
.indexer
.keychains_added
.get(&KeychainKind::External)
.ok_or(LoadError::MissingDescriptor(KeychainKind::External))?
.clone();
let change_descriptor = changeset
.indexed_tx_graph
.indexer
.keychains_added
.get(&KeychainKind::Internal)
.ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))?
.clone();
let chain = LocalChain::from_changeset(changeset.local_chain)
.map_err(|_| LoadError::MissingGenesis)?;
let (signers, change_signers) =
create_signers(&mut index, &secp, descriptor, change_descriptor, network)
.expect("Can't fail: we passed in valid descriptors, recovered from the changeset");
let descriptor = changeset
.descriptor
.ok_or(LoadError::MissingDescriptor(KeychainKind::External))?;
let change_descriptor = changeset
.change_descriptor
.ok_or(LoadError::MissingDescriptor(KeychainKind::Internal))?;
check_wallet_descriptor(&descriptor).map_err(LoadError::Descriptor)?;
check_wallet_descriptor(&change_descriptor).map_err(LoadError::Descriptor)?;
// checks
if let Some(exp_network) = params.check_network {
if network != exp_network {
return Err(LoadError::Mismatch(LoadMismatch::Network {
loaded: network,
expected: exp_network,
}));
}
}
if let Some(exp_genesis_hash) = params.check_genesis_hash {
if chain.genesis_hash() != exp_genesis_hash {
return Err(LoadError::Mismatch(LoadMismatch::Genesis {
loaded: chain.genesis_hash(),
expected: exp_genesis_hash,
}));
}
}
if let Some(exp_descriptor) = params.check_descriptor {
if descriptor.descriptor_id() != exp_descriptor.descriptor_id() {
return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
keychain: KeychainKind::External,
loaded: descriptor,
expected: exp_descriptor,
}));
}
}
if let Some(exp_change_descriptor) = params.check_change_descriptor {
if change_descriptor.descriptor_id() != exp_change_descriptor.descriptor_id() {
return Err(LoadError::Mismatch(LoadMismatch::Descriptor {
keychain: KeychainKind::External,
loaded: change_descriptor,
expected: exp_change_descriptor,
}));
}
}
let signers = Arc::new(SignersContainer::build(
params.descriptor_keymap,
&descriptor,
&secp,
));
let change_signers = Arc::new(SignersContainer::build(
params.change_descriptor_keymap,
&change_descriptor,
&secp,
));
let index = create_indexer(descriptor, change_descriptor, params.lookahead)
.map_err(LoadError::Descriptor)?;
let mut indexed_graph = IndexedTxGraph::new(index);
indexed_graph.apply_changeset(changeset.indexed_tx_graph);
indexed_graph.apply_changeset(changeset.indexer.into());
indexed_graph.apply_changeset(changeset.tx_graph.into());
let stage = ChangeSet::default();
Ok(Wallet {
Ok(Some(Wallet {
signers,
change_signers,
chain,
@@ -451,146 +549,7 @@ impl Wallet {
stage,
network,
secp,
})
}
/// Either loads [`Wallet`] from the given [`ChangeSet`] or initializes it if one does not exist.
///
/// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided.
///
/// ```rust,no_run
/// # use bdk_wallet::Wallet;
/// use bdk_sqlite::{Store, rusqlite::Connection};
/// # use bitcoin::Network::Testnet;
/// let conn = Connection::open_in_memory().expect("must open connection");
/// let mut db = Store::new(conn).expect("must create db");
/// let changeset = db.read()?;
///
/// let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
/// let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
///
/// let mut wallet = Wallet::new_or_load(external_descriptor, internal_descriptor, changeset, Testnet)?;
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn new_or_load<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
changeset: Option<ChangeSet>,
network: Network,
) -> Result<Self, NewOrLoadError> {
let genesis_hash = genesis_block(network).block_hash();
Self::new_or_load_with_genesis_hash(
descriptor,
change_descriptor,
changeset,
network,
genesis_hash,
)
}
/// Either loads [`Wallet`] from a [`ChangeSet`] or initializes it if one does not exist, using the
/// provided descriptor, change descriptor, network, and custom genesis hash.
///
/// This method will fail if the loaded [`ChangeSet`] has different parameters to those provided.
/// This is like [`Wallet::new_or_load`] with an additional `genesis_hash` parameter. This is
/// useful for syncing from alternative networks.
pub fn new_or_load_with_genesis_hash<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
changeset: Option<ChangeSet>,
network: Network,
genesis_hash: BlockHash,
) -> Result<Self, NewOrLoadError> {
if let Some(changeset) = changeset {
let mut wallet = Self::load_from_changeset(changeset).map_err(|e| match e {
LoadError::Descriptor(e) => NewOrLoadError::Descriptor(e),
LoadError::MissingNetwork => NewOrLoadError::LoadedNetworkDoesNotMatch {
expected: network,
got: None,
},
LoadError::MissingGenesis => NewOrLoadError::LoadedGenesisDoesNotMatch {
expected: genesis_hash,
got: None,
},
LoadError::MissingDescriptor(keychain) => {
NewOrLoadError::LoadedDescriptorDoesNotMatch {
got: None,
keychain,
}
}
})?;
if wallet.network != network {
return Err(NewOrLoadError::LoadedNetworkDoesNotMatch {
expected: network,
got: Some(wallet.network),
});
}
if wallet.chain.genesis_hash() != genesis_hash {
return Err(NewOrLoadError::LoadedGenesisDoesNotMatch {
expected: genesis_hash,
got: Some(wallet.chain.genesis_hash()),
});
}
let (expected_descriptor, expected_descriptor_keymap) = descriptor
.into_wallet_descriptor(&wallet.secp, network)
.map_err(NewOrLoadError::Descriptor)?;
let wallet_descriptor = wallet.public_descriptor(KeychainKind::External);
if wallet_descriptor != &expected_descriptor {
return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
got: Some(wallet_descriptor.clone()),
keychain: KeychainKind::External,
});
}
// if expected descriptor has private keys add them as new signers
if !expected_descriptor_keymap.is_empty() {
let signer_container = SignersContainer::build(
expected_descriptor_keymap,
&expected_descriptor,
&wallet.secp,
);
signer_container.signers().into_iter().for_each(|signer| {
wallet.add_signer(
KeychainKind::External,
SignerOrdering::default(),
signer.clone(),
)
});
}
let (expected_change_descriptor, expected_change_descriptor_keymap) = change_descriptor
.into_wallet_descriptor(&wallet.secp, network)
.map_err(NewOrLoadError::Descriptor)?;
let wallet_change_descriptor = wallet.public_descriptor(KeychainKind::Internal);
if wallet_change_descriptor != &expected_change_descriptor {
return Err(NewOrLoadError::LoadedDescriptorDoesNotMatch {
got: Some(wallet_change_descriptor.clone()),
keychain: KeychainKind::Internal,
});
}
// if expected change descriptor has private keys add them as new signers
if !expected_change_descriptor_keymap.is_empty() {
let signer_container = SignersContainer::build(
expected_change_descriptor_keymap,
&expected_change_descriptor,
&wallet.secp,
);
signer_container.signers().into_iter().for_each(|signer| {
wallet.add_signer(
KeychainKind::Internal,
SignerOrdering::default(),
signer.clone(),
)
});
}
Ok(wallet)
} else {
Self::new_with_genesis_hash(descriptor, change_descriptor, network, genesis_hash)
.map_err(|e| match e {
NewError::Descriptor(e) => NewOrLoadError::Descriptor(e),
})
}
}))
}
/// Get the Bitcoin network the wallet is using.
@@ -642,17 +601,15 @@ impl Wallet {
/// calls to this method before closing the wallet. For example:
///
/// ```rust,no_run
/// # use bdk_wallet::wallet::{Wallet, ChangeSet};
/// # use bdk_wallet::KeychainKind;
/// use bdk_sqlite::{rusqlite::Connection, Store};
/// let conn = Connection::open_in_memory().expect("must open connection");
/// let mut db = Store::new(conn).expect("must create store");
/// # let changeset = ChangeSet::default();
/// # let mut wallet = Wallet::load_from_changeset(changeset).expect("load wallet");
/// # use bdk_wallet::{LoadParams, ChangeSet, KeychainKind};
/// use bdk_chain::sqlite::Connection;
/// let mut conn = Connection::open_in_memory().expect("must open connection");
/// let mut wallet = LoadParams::new()
/// .load_wallet(&mut conn)
/// .expect("database is okay")
/// .expect("database has data");
/// let next_address = wallet.reveal_next_address(KeychainKind::External);
/// if let Some(changeset) = wallet.take_staged() {
/// db.write(&changeset)?;
/// }
/// wallet.persist(&mut conn).expect("write is okay");
///
/// // Now it's safe to show the user their next address!
/// println!("Next address: {}", next_address.address);
@@ -666,7 +623,7 @@ impl Wallet {
.reveal_next_spk(&keychain)
.expect("keychain must exist");
stage.merge(indexed_tx_graph::ChangeSet::from(index_changeset).into());
stage.merge(index_changeset.into());
AddressInfo {
index,
@@ -1110,16 +1067,38 @@ impl Wallet {
signers.add_external(signer.id(&self.secp), ordering, signer);
}
/// Set the keymap for a given keychain.
pub fn set_keymap(&mut self, keychain: KeychainKind, keymap: KeyMap) {
let wallet_signers = match keychain {
KeychainKind::External => Arc::make_mut(&mut self.signers),
KeychainKind::Internal => Arc::make_mut(&mut self.change_signers),
};
let descriptor = self
.indexed_graph
.index
.get_descriptor(&keychain)
.expect("keychain must exist");
*wallet_signers = SignersContainer::build(keymap, descriptor, &self.secp);
}
/// Set the keymap for each keychain.
pub fn set_keymaps(&mut self, keymaps: impl IntoIterator<Item = (KeychainKind, KeyMap)>) {
for (keychain, keymap) in keymaps {
self.set_keymap(keychain, keymap);
}
}
/// Get the signers
///
/// ## Example
///
/// ```
/// # use bdk_wallet::{Wallet, KeychainKind};
/// # use bdk_wallet::{CreateParams, KeychainKind};
/// # use bdk_wallet::bitcoin::Network;
/// let descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/0/*)";
/// let change_descriptor = "wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/1'/0'/1/*)";
/// let wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
/// let wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)?
/// .create_wallet_no_persist()?;
/// for secret_key in wallet.get_signers(KeychainKind::External).signers().iter().filter_map(|s| s.descriptor_secret_key()) {
/// // secret_key: tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*
/// println!("secret_key: {}", secret_key);
@@ -2424,25 +2403,23 @@ fn new_local_utxo(
}
}
fn create_signers<E: IntoWalletDescriptor>(
index: &mut KeychainTxOutIndex<KeychainKind>,
secp: &Secp256k1<All>,
descriptor: E,
change_descriptor: E,
network: Network,
) -> Result<(Arc<SignersContainer>, Arc<SignersContainer>), DescriptorError> {
let descriptor = into_wallet_descriptor_checked(descriptor, secp, network)?;
let change_descriptor = into_wallet_descriptor_checked(change_descriptor, secp, network)?;
let (descriptor, keymap) = descriptor;
let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
let _ = index
.insert_descriptor(KeychainKind::External, descriptor)
.expect("this is the first descriptor we're inserting");
fn create_indexer(
descriptor: ExtendedDescriptor,
change_descriptor: ExtendedDescriptor,
lookahead: u32,
) -> Result<KeychainTxOutIndex<KeychainKind>, DescriptorError> {
let mut indexer = KeychainTxOutIndex::<KeychainKind>::new(lookahead);
let (descriptor, keymap) = change_descriptor;
let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
let _ = index
.insert_descriptor(KeychainKind::Internal, descriptor)
// let (descriptor, keymap) = descriptor;
// let signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
assert!(indexer
.insert_descriptor(KeychainKind::External, descriptor)
.expect("first descriptor introduced must succeed"));
// let (descriptor, keymap) = change_descriptor;
// let change_signers = Arc::new(SignersContainer::build(keymap, &descriptor, secp));
assert!(indexer
.insert_descriptor(KeychainKind::Internal, change_descriptor)
.map_err(|e| {
use bdk_chain::indexer::keychain_txout::InsertDescriptorError;
match e {
@@ -2453,9 +2430,9 @@ fn create_signers<E: IntoWalletDescriptor>(
unreachable!("this is the first time we're assigning internal")
}
}
})?;
})?);
Ok((signers, change_signers))
Ok(indexer)
}
/// Transforms a [`FeeRate`] to `f64` with unit as sat/vb.
@@ -2476,16 +2453,18 @@ macro_rules! doctest_wallet {
() => {{
use $crate::bitcoin::{BlockHash, Transaction, absolute, TxOut, Network, hashes::Hash};
use $crate::chain::{ConfirmationBlockTime, BlockId, TxGraph};
use $crate::wallet::{Update, Wallet};
use $crate::wallet::{Update, CreateParams};
use $crate::KeychainKind;
let descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/0/*)";
let change_descriptor = "tr([73c5da0a/86'/0'/0']tprv8fMn4hSKPRC1oaCPqxDb1JWtgkpeiQvZhsr8W2xuy3GEMkzoArcAWTfJxYb6Wj8XNNDWEjfYKK4wGQXh3ZUXhDF2NcnsALpWTeSwarJt7Vc/1/*)";
let mut wallet = Wallet::new(
let mut wallet = CreateParams::new(
descriptor,
change_descriptor,
Network::Regtest,
)
.unwrap()
.create_wallet_no_persist()
.unwrap();
let address = wallet.peek_address(KeychainKind::External, 0).address;
let tx = Transaction {

View File

@@ -0,0 +1,217 @@
use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith};
use bitcoin::{BlockHash, Network};
use miniscript::descriptor::KeyMap;
use crate::{
descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor},
KeychainKind, Wallet,
};
use super::{utils::SecpCtx, ChangeSet, LoadError, PersistedWallet};
/// Parameters for [`Wallet::create`] or [`PersistedWallet::create`].
#[derive(Debug, Clone)]
#[must_use]
pub struct CreateParams {
pub(crate) descriptor: ExtendedDescriptor,
pub(crate) descriptor_keymap: KeyMap,
pub(crate) change_descriptor: ExtendedDescriptor,
pub(crate) change_descriptor_keymap: KeyMap,
pub(crate) network: Network,
pub(crate) genesis_hash: Option<BlockHash>,
pub(crate) lookahead: u32,
pub(crate) secp: SecpCtx,
}
impl CreateParams {
/// Construct parameters with provided `descriptor`, `change_descriptor` and `network`.
///
/// Default values: `genesis_hash` = `None`, `lookahead` = [`DEFAULT_LOOKAHEAD`]
pub fn new<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
network: Network,
) -> Result<Self, DescriptorError> {
let secp = SecpCtx::default();
let (descriptor, descriptor_keymap) = descriptor.into_wallet_descriptor(&secp, network)?;
let (change_descriptor, change_descriptor_keymap) =
change_descriptor.into_wallet_descriptor(&secp, network)?;
Ok(Self {
descriptor,
descriptor_keymap,
change_descriptor,
change_descriptor_keymap,
network,
genesis_hash: None,
lookahead: DEFAULT_LOOKAHEAD,
secp,
})
}
/// Extend the given `keychain`'s `keymap`.
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
match keychain {
KeychainKind::External => &mut self.descriptor_keymap,
KeychainKind::Internal => &mut self.change_descriptor_keymap,
}
.extend(keymap);
self
}
/// Use a custom `genesis_hash`.
pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
self.genesis_hash = Some(genesis_hash);
self
}
/// Use custom lookahead value.
pub fn lookahead(mut self, lookahead: u32) -> Self {
self.lookahead = lookahead;
self
}
/// Create [`PersistedWallet`] with the given `Db`.
pub fn create_wallet<Db>(
self,
db: &mut Db,
) -> Result<PersistedWallet, <Wallet as PersistWith<Db>>::CreateError>
where
Wallet: PersistWith<Db, CreateParams = Self>,
{
PersistedWallet::create(db, self)
}
/// Create [`PersistedWallet`] with the given async `Db`.
pub async fn create_wallet_async<Db>(
self,
db: &mut Db,
) -> Result<PersistedWallet, <Wallet as PersistAsyncWith<Db>>::CreateError>
where
Wallet: PersistAsyncWith<Db, CreateParams = Self>,
{
PersistedWallet::create_async(db, self).await
}
/// Create [`Wallet`] without persistence.
pub fn create_wallet_no_persist(self) -> Result<Wallet, DescriptorError> {
Wallet::create_with_params(self)
}
}
/// Parameters for [`Wallet::load`] or [`PersistedWallet::load`].
#[must_use]
#[derive(Debug, Clone)]
pub struct LoadParams {
pub(crate) descriptor_keymap: KeyMap,
pub(crate) change_descriptor_keymap: KeyMap,
pub(crate) lookahead: u32,
pub(crate) check_network: Option<Network>,
pub(crate) check_genesis_hash: Option<BlockHash>,
pub(crate) check_descriptor: Option<ExtendedDescriptor>,
pub(crate) check_change_descriptor: Option<ExtendedDescriptor>,
pub(crate) secp: SecpCtx,
}
impl LoadParams {
/// Construct parameters with default values.
///
/// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`]
pub fn new() -> Self {
Self {
descriptor_keymap: KeyMap::default(),
change_descriptor_keymap: KeyMap::default(),
lookahead: DEFAULT_LOOKAHEAD,
check_network: None,
check_genesis_hash: None,
check_descriptor: None,
check_change_descriptor: None,
secp: SecpCtx::new(),
}
}
/// Construct parameters with `network` check.
pub fn with_network(network: Network) -> Self {
Self {
check_network: Some(network),
..Default::default()
}
}
/// Construct parameters with descriptor checks.
pub fn with_descriptors<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: E,
network: Network,
) -> Result<Self, DescriptorError> {
let mut params = Self::with_network(network);
let secp = &params.secp;
let (descriptor, descriptor_keymap) = descriptor.into_wallet_descriptor(secp, network)?;
params.check_descriptor = Some(descriptor);
params.descriptor_keymap = descriptor_keymap;
let (change_descriptor, change_descriptor_keymap) =
change_descriptor.into_wallet_descriptor(secp, network)?;
params.check_change_descriptor = Some(change_descriptor);
params.change_descriptor_keymap = change_descriptor_keymap;
Ok(params)
}
/// Extend the given `keychain`'s `keymap`.
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
match keychain {
KeychainKind::External => &mut self.descriptor_keymap,
KeychainKind::Internal => &mut self.change_descriptor_keymap,
}
.extend(keymap);
self
}
/// Check for a `genesis_hash`.
pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
self.check_genesis_hash = Some(genesis_hash);
self
}
/// Use custom lookahead value.
pub fn lookahead(mut self, lookahead: u32) -> Self {
self.lookahead = lookahead;
self
}
/// Load [`PersistedWallet`] with the given `Db`.
pub fn load_wallet<Db>(
self,
db: &mut Db,
) -> Result<Option<PersistedWallet>, <Wallet as PersistWith<Db>>::LoadError>
where
Wallet: PersistWith<Db, LoadParams = Self>,
{
PersistedWallet::load(db, self)
}
/// Load [`PersistedWallet`] with the given async `Db`.
pub async fn load_wallet_async<Db>(
self,
db: &mut Db,
) -> Result<Option<PersistedWallet>, <Wallet as PersistAsyncWith<Db>>::LoadError>
where
Wallet: PersistAsyncWith<Db, LoadParams = Self>,
{
PersistedWallet::load_async(db, self).await
}
/// Load [`Wallet`] without persistence.
pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result<Option<Wallet>, LoadError> {
Wallet::load_with_params(changeset, self)
}
}
impl Default for LoadParams {
fn default() -> Self {
Self::new()
}
}

View File

@@ -0,0 +1,180 @@
use core::fmt;
use crate::wallet::{ChangeSet, CreateParams, LoadError, LoadParams};
use crate::{descriptor::DescriptorError, Wallet};
use bdk_chain::{Merge, PersistWith};
/// Represents a persisted wallet.
pub type PersistedWallet = bdk_chain::Persisted<Wallet>;
#[cfg(feature = "sqlite")]
impl<'c> PersistWith<bdk_chain::sqlite::Transaction<'c>> for Wallet {
type CreateParams = CreateParams;
type LoadParams = LoadParams;
type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
type PersistError = bdk_chain::rusqlite::Error;
fn create(
db: &mut bdk_chain::sqlite::Transaction<'c>,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut wallet =
Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
if let Some(changeset) = wallet.take_staged() {
changeset
.persist_to_sqlite(db)
.map_err(CreateWithPersistError::Persist)?;
}
Ok(wallet)
}
fn load(
conn: &mut bdk_chain::sqlite::Transaction<'c>,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let changeset = ChangeSet::from_sqlite(conn).map_err(LoadWithPersistError::Persist)?;
if changeset.is_empty() {
return Ok(None);
}
Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
}
fn persist(
&mut self,
conn: &mut bdk_chain::sqlite::Transaction,
) -> Result<bool, Self::PersistError> {
if let Some(changeset) = self.take_staged() {
changeset.persist_to_sqlite(conn)?;
return Ok(true);
}
Ok(false)
}
}
#[cfg(feature = "sqlite")]
impl PersistWith<bdk_chain::sqlite::Connection> for Wallet {
type CreateParams = CreateParams;
type LoadParams = LoadParams;
type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
type PersistError = bdk_chain::rusqlite::Error;
fn create(
db: &mut bdk_chain::sqlite::Connection,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut db_tx = db.transaction().map_err(CreateWithPersistError::Persist)?;
let wallet = PersistWith::create(&mut db_tx, params)?;
db_tx.commit().map_err(CreateWithPersistError::Persist)?;
Ok(wallet)
}
fn load(
db: &mut bdk_chain::sqlite::Connection,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let mut db_tx = db.transaction().map_err(LoadWithPersistError::Persist)?;
let wallet_opt = PersistWith::load(&mut db_tx, params)?;
db_tx.commit().map_err(LoadWithPersistError::Persist)?;
Ok(wallet_opt)
}
fn persist(
&mut self,
db: &mut bdk_chain::sqlite::Connection,
) -> Result<bool, Self::PersistError> {
let mut db_tx = db.transaction()?;
let has_changes = PersistWith::persist(self, &mut db_tx)?;
db_tx.commit()?;
Ok(has_changes)
}
}
#[cfg(feature = "file_store")]
impl PersistWith<bdk_file_store::Store<ChangeSet>> for Wallet {
type CreateParams = CreateParams;
type LoadParams = LoadParams;
type CreateError = CreateWithPersistError<std::io::Error>;
type LoadError = LoadWithPersistError<bdk_file_store::AggregateChangesetsError<ChangeSet>>;
type PersistError = std::io::Error;
fn create(
db: &mut bdk_file_store::Store<ChangeSet>,
params: Self::CreateParams,
) -> Result<Self, Self::CreateError> {
let mut wallet =
Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
if let Some(changeset) = wallet.take_staged() {
db.append_changeset(&changeset)
.map_err(CreateWithPersistError::Persist)?;
}
Ok(wallet)
}
fn load(
db: &mut bdk_file_store::Store<ChangeSet>,
params: Self::LoadParams,
) -> Result<Option<Self>, Self::LoadError> {
let changeset = db
.aggregate_changesets()
.map_err(LoadWithPersistError::Persist)?
.unwrap_or_default();
Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
}
fn persist(
&mut self,
db: &mut bdk_file_store::Store<ChangeSet>,
) -> Result<bool, Self::PersistError> {
if let Some(changeset) = self.take_staged() {
db.append_changeset(&changeset)?;
return Ok(true);
}
Ok(false)
}
}
/// Error type for [`PersistedWallet::load`].
#[derive(Debug)]
pub enum LoadWithPersistError<E> {
/// Error from persistence.
Persist(E),
/// Occurs when the loaded changeset cannot construct [`Wallet`].
InvalidChangeSet(LoadError),
}
impl<E: fmt::Display> fmt::Display for LoadWithPersistError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Persist(err) => fmt::Display::fmt(err, f),
Self::InvalidChangeSet(err) => fmt::Display::fmt(&err, f),
}
}
}
#[cfg(feature = "std")]
impl<E: fmt::Debug + fmt::Display> std::error::Error for LoadWithPersistError<E> {}
/// Error type for [`PersistedWallet::create`].
#[derive(Debug)]
pub enum CreateWithPersistError<E> {
/// Error from persistence.
Persist(E),
/// Occurs when the loaded changeset cannot contruct [`Wallet`].
Descriptor(DescriptorError),
}
impl<E: fmt::Display> fmt::Display for CreateWithPersistError<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Persist(err) => fmt::Display::fmt(err, f),
Self::Descriptor(err) => fmt::Display::fmt(&err, f),
}
}
}
#[cfg(feature = "std")]
impl<E: fmt::Debug + fmt::Display> std::error::Error for CreateWithPersistError<E> {}

View File

@@ -69,7 +69,8 @@
//!
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)";
//! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)";
//! let mut wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
//! let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)?
//! .create_wallet_no_persist()?;
//! wallet.add_signer(
//! KeychainKind::External,
//! SignerOrdering(200),

View File

@@ -1,7 +1,7 @@
#![allow(unused)]
use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
use bdk_wallet::{
wallet::{Update, Wallet},
wallet::{CreateParams, Update, Wallet},
KeychainKind, LocalOutput,
};
use bitcoin::{
@@ -16,7 +16,11 @@ use std::str::FromStr;
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
/// sats are the transaction fee.
pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, bitcoin::Txid) {
let mut wallet = Wallet::new(descriptor, change, Network::Regtest).unwrap();
let mut wallet = CreateParams::new(descriptor, change, Network::Regtest)
.expect("must parse descriptors")
.create_wallet_no_persist()
.expect("descriptors must be valid");
let receive_address = wallet.peek_address(KeychainKind::External, 0).address;
let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
.expect("address")

View File

@@ -3,18 +3,17 @@ extern crate alloc;
use std::path::Path;
use std::str::FromStr;
use anyhow::Context;
use assert_matches::assert_matches;
use bdk_chain::collections::BTreeMap;
use bdk_chain::COINBASE_MATURITY;
use bdk_chain::{BlockId, ConfirmationTime};
use bdk_sqlite::rusqlite::Connection;
use bdk_chain::{PersistWith, COINBASE_MATURITY};
use bdk_wallet::descriptor::{calc_checksum, DescriptorError, IntoWalletDescriptor};
use bdk_wallet::psbt::PsbtUtils;
use bdk_wallet::signer::{SignOptions, SignerError};
use bdk_wallet::wallet::coin_selection::{self, LargestFirstCoinSelection};
use bdk_wallet::wallet::error::CreateTxError;
use bdk_wallet::wallet::tx_builder::AddForeignUtxoError;
use bdk_wallet::wallet::{AddressInfo, Balance, ChangeSet, NewError, Wallet};
use bdk_wallet::wallet::{AddressInfo, Balance, CreateParams, LoadParams, Wallet};
use bdk_wallet::KeychainKind;
use bitcoin::hashes::Hash;
use bitcoin::key::Secp256k1;
@@ -102,46 +101,44 @@ const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
const DB_MAGIC: &[u8] = &[0x21, 0x24, 0x48];
#[test]
fn load_recovers_wallet() -> anyhow::Result<()> {
fn run<Db, New, Recover, Read, Write>(
fn wallet_is_persisted() -> anyhow::Result<()> {
fn run<Db, CreateDb, OpenDb>(
filename: &str,
create_new: New,
recover: Recover,
read: Read,
write: Write,
create_db: CreateDb,
open_db: OpenDb,
) -> anyhow::Result<()>
where
New: Fn(&Path) -> anyhow::Result<Db>,
Recover: Fn(&Path) -> anyhow::Result<Db>,
Read: Fn(&mut Db) -> anyhow::Result<Option<ChangeSet>>,
Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>,
CreateDb: Fn(&Path) -> anyhow::Result<Db>,
OpenDb: Fn(&Path) -> anyhow::Result<Db>,
Wallet: PersistWith<Db, CreateParams = CreateParams, LoadParams = LoadParams>,
<Wallet as PersistWith<Db>>::CreateError: std::error::Error + Send + Sync + 'static,
<Wallet as PersistWith<Db>>::LoadError: std::error::Error + Send + Sync + 'static,
<Wallet as PersistWith<Db>>::PersistError: std::error::Error + Send + Sync + 'static,
{
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc();
let (external_desc, internal_desc) = get_test_tr_single_sig_xprv_with_change_desc();
// create new wallet
let wallet_spk_index = {
let mut wallet =
Wallet::new(desc, change_desc, Network::Testnet).expect("must init wallet");
let mut db = create_db(&file_path)?;
let mut wallet = CreateParams::new(external_desc, internal_desc, Network::Testnet)?
.create_wallet(&mut db)?;
wallet.reveal_next_address(KeychainKind::External);
// persist new wallet changes
let mut db = create_new(&file_path).expect("must create db");
if let Some(changeset) = wallet.take_staged() {
write(&mut db, &changeset)?;
}
assert!(wallet.persist(&mut db)?, "must write");
wallet.spk_index().clone()
};
// recover wallet
{
// load persisted wallet changes
let db = &mut recover(&file_path).expect("must recover db");
let changeset = read(db).expect("must recover wallet").expect("changeset");
let mut db = open_db(&file_path).context("failed to recover db")?;
let wallet =
LoadParams::with_descriptors(external_desc, internal_desc, Network::Testnet)?
.load_wallet(&mut db)?
.expect("wallet must exist");
let wallet = Wallet::load_from_changeset(changeset).expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert_eq!(
wallet.spk_index().keychains().collect::<Vec<_>>(),
@@ -154,7 +151,8 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
let secp = Secp256k1::new();
assert_eq!(
*wallet.public_descriptor(KeychainKind::External),
desc.into_wallet_descriptor(&secp, wallet.network())
external_desc
.into_wallet_descriptor(&secp, wallet.network())
.unwrap()
.0
);
@@ -167,166 +165,11 @@ fn load_recovers_wallet() -> anyhow::Result<()> {
"store.db",
|path| Ok(bdk_file_store::Store::create_new(DB_MAGIC, path)?),
|path| Ok(bdk_file_store::Store::open(DB_MAGIC, path)?),
|db| Ok(bdk_file_store::Store::aggregate_changesets(db)?),
|db, changeset| Ok(bdk_file_store::Store::append_changeset(db, changeset)?),
)?;
run(
run::<bdk_chain::sqlite::Connection, _, _>(
"store.sqlite",
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|db| Ok(bdk_sqlite::Store::read(db)?),
|db, changeset| Ok(bdk_sqlite::Store::write(db, changeset)?),
)?;
Ok(())
}
#[test]
fn new_or_load() -> anyhow::Result<()> {
fn run<Db, NewOrRecover, Read, Write>(
filename: &str,
new_or_load: NewOrRecover,
read: Read,
write: Write,
) -> anyhow::Result<()>
where
NewOrRecover: Fn(&Path) -> anyhow::Result<Db>,
Read: Fn(&mut Db) -> anyhow::Result<Option<ChangeSet>>,
Write: Fn(&mut Db, &ChangeSet) -> anyhow::Result<()>,
{
let temp_dir = tempfile::tempdir().expect("must create tempdir");
let file_path = temp_dir.path().join(filename);
let (desc, change_desc) = get_test_wpkh_with_change_desc();
// init wallet when non-existent
let wallet_keychains: BTreeMap<_, _> = {
let wallet = &mut Wallet::new_or_load(desc, change_desc, None, Network::Testnet)
.expect("must init wallet");
let mut db = new_or_load(&file_path).expect("must create db");
if let Some(changeset) = wallet.take_staged() {
write(&mut db, &changeset)?;
}
wallet.keychains().map(|(k, v)| (*k, v.clone())).collect()
};
// wrong network
{
let mut db = new_or_load(&file_path).expect("must create db");
let changeset = read(&mut db)?;
let err = Wallet::new_or_load(desc, change_desc, changeset, Network::Bitcoin)
.expect_err("wrong network");
assert!(
matches!(
err,
bdk_wallet::wallet::NewOrLoadError::LoadedNetworkDoesNotMatch {
got: Some(Network::Testnet),
expected: Network::Bitcoin
}
),
"err: {}",
err,
);
}
// wrong genesis hash
{
let exp_blockhash = BlockHash::all_zeros();
let got_blockhash = bitcoin::constants::genesis_block(Network::Testnet).block_hash();
let db = &mut new_or_load(&file_path).expect("must open db");
let changeset = read(db)?;
let err = Wallet::new_or_load_with_genesis_hash(
desc,
change_desc,
changeset,
Network::Testnet,
exp_blockhash,
)
.expect_err("wrong genesis hash");
assert!(
matches!(
err,
bdk_wallet::wallet::NewOrLoadError::LoadedGenesisDoesNotMatch { got, expected }
if got == Some(got_blockhash) && expected == exp_blockhash
),
"err: {}",
err,
);
}
// wrong external descriptor
{
let (exp_descriptor, exp_change_desc) = get_test_tr_single_sig_xprv_with_change_desc();
let got_descriptor = desc
.into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
.unwrap()
.0;
let db = &mut new_or_load(&file_path).expect("must open db");
let changeset = read(db)?;
let err =
Wallet::new_or_load(exp_descriptor, exp_change_desc, changeset, Network::Testnet)
.expect_err("wrong external descriptor");
assert!(
matches!(
err,
bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
if got == &Some(got_descriptor) && keychain == KeychainKind::External
),
"err: {}",
err,
);
}
// wrong internal descriptor
{
let exp_descriptor = get_test_tr_single_sig();
let got_descriptor = change_desc
.into_wallet_descriptor(&Secp256k1::new(), Network::Testnet)
.unwrap()
.0;
let db = &mut new_or_load(&file_path).expect("must open db");
let changeset = read(db)?;
let err = Wallet::new_or_load(desc, exp_descriptor, changeset, Network::Testnet)
.expect_err("wrong internal descriptor");
assert!(
matches!(
err,
bdk_wallet::wallet::NewOrLoadError::LoadedDescriptorDoesNotMatch { ref got, keychain }
if got == &Some(got_descriptor) && keychain == KeychainKind::Internal
),
"err: {}",
err,
);
}
// all parameters match
{
let db = &mut new_or_load(&file_path).expect("must open db");
let changeset = read(db)?;
let wallet = Wallet::new_or_load(desc, change_desc, changeset, Network::Testnet)
.expect("must recover wallet");
assert_eq!(wallet.network(), Network::Testnet);
assert!(wallet
.keychains()
.map(|(k, v)| (*k, v.clone()))
.eq(wallet_keychains));
}
Ok(())
}
run(
"store.db",
|path| Ok(bdk_file_store::Store::open_or_create_new(DB_MAGIC, path)?),
|db| Ok(bdk_file_store::Store::aggregate_changesets(db)?),
|db, changeset| Ok(bdk_file_store::Store::append_changeset(db, changeset)?),
)?;
run(
"store.sqlite",
|path| Ok(bdk_sqlite::Store::new(Connection::open(path)?)?),
|db| Ok(bdk_sqlite::Store::read(db)?),
|db, changeset| Ok(bdk_sqlite::Store::write(db, changeset)?),
|path| Ok(bdk_chain::sqlite::Connection::open(path)?),
|path| Ok(bdk_chain::sqlite::Connection::open(path)?),
)?;
Ok(())
@@ -336,14 +179,11 @@ fn new_or_load() -> anyhow::Result<()> {
fn test_error_external_and_internal_are_the_same() {
// identical descriptors should fail to create wallet
let desc = get_test_wpkh();
let err = Wallet::new(desc, desc, Network::Testnet);
let err = CreateParams::new(desc, desc, Network::Testnet)
.unwrap()
.create_wallet_no_persist();
assert!(
matches!(
&err,
Err(NewError::Descriptor(
DescriptorError::ExternalAndInternalAreTheSame
))
),
matches!(&err, Err(DescriptorError::ExternalAndInternalAreTheSame)),
"expected same descriptors error, got {:?}",
err,
);
@@ -351,14 +191,11 @@ fn test_error_external_and_internal_are_the_same() {
// public + private of same descriptor should fail to create wallet
let desc = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
let change_desc = "wpkh([3c31d632/84'/1'/0']tpubDCYwFkks2cg78N7eoYbBatsFEGje8vW8arSKW4rLwD1AU1s9KJMDRHE32JkvYERuiFjArrsH7qpWSpJATed5ShZbG9KsskA5Rmi6NSYgYN2/0/*)";
let err = Wallet::new(desc, change_desc, Network::Testnet);
let err = CreateParams::new(desc, change_desc, Network::Testnet)
.unwrap()
.create_wallet_no_persist();
assert!(
matches!(
err,
Err(NewError::Descriptor(
DescriptorError::ExternalAndInternalAreTheSame
))
),
matches!(err, Err(DescriptorError::ExternalAndInternalAreTheSame)),
"expected same descriptors error, got {:?}",
err,
);
@@ -1316,8 +1153,11 @@ fn test_create_tx_policy_path_required() {
#[test]
fn test_create_tx_policy_path_no_csv() {
let (desc, change_desc) = get_test_wpkh_with_change_desc();
let mut wallet = Wallet::new(desc, change_desc, Network::Regtest).expect("wallet");
let (descriptor, change_descriptor) = get_test_wpkh_with_change_desc();
let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Regtest)
.expect("must parse")
.create_wallet_no_persist()
.expect("wallet");
let tx = Transaction {
version: transaction::Version::non_standard(0),
@@ -2927,9 +2767,12 @@ fn test_sign_nonstandard_sighash() {
#[test]
fn test_unused_address() {
let desc = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let change_desc = get_test_wpkh();
let mut wallet = Wallet::new(desc, change_desc, Network::Testnet).expect("wallet");
let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let change_descriptor = get_test_wpkh();
let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)
.expect("must parse descriptors")
.create_wallet_no_persist()
.expect("wallet");
// `list_unused_addresses` should be empty if we haven't revealed any
assert!(wallet
@@ -2956,8 +2799,11 @@ fn test_unused_address() {
#[test]
fn test_next_unused_address() {
let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let change = get_test_wpkh();
let mut wallet = Wallet::new(descriptor, change, Network::Testnet).expect("wallet");
let change_descriptor = get_test_wpkh();
let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)
.expect("must parse descriptors")
.create_wallet_no_persist()
.expect("wallet");
assert_eq!(wallet.derivation_index(KeychainKind::External), None);
assert_eq!(
@@ -3002,9 +2848,12 @@ fn test_next_unused_address() {
#[test]
fn test_peek_address_at_index() {
let desc = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let change_desc = get_test_wpkh();
let mut wallet = Wallet::new(desc, change_desc, Network::Testnet).unwrap();
let descriptor = "wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)";
let change_descriptor = get_test_wpkh();
let mut wallet = CreateParams::new(descriptor, change_descriptor, Network::Testnet)
.expect("must parse descriptors")
.create_wallet_no_persist()
.expect("wallet");
assert_eq!(
wallet.peek_address(KeychainKind::External, 1).to_string(),
@@ -3039,8 +2888,11 @@ fn test_peek_address_at_index() {
#[test]
fn test_peek_address_at_index_not_derivable() {
let wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
get_test_wpkh(), Network::Testnet).unwrap();
let wallet = CreateParams::new(
"wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/1)",
get_test_wpkh(),
Network::Testnet,
).unwrap().create_wallet_no_persist().unwrap();
assert_eq!(
wallet.peek_address(KeychainKind::External, 1).to_string(),
@@ -3060,8 +2912,11 @@ fn test_peek_address_at_index_not_derivable() {
#[test]
fn test_returns_index_and_address() {
let mut wallet = Wallet::new("wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
get_test_wpkh(), Network::Testnet).unwrap();
let mut wallet = CreateParams::new(
"wpkh(tpubEBr4i6yk5nf5DAaJpsi9N2pPYBeJ7fZ5Z9rmN4977iYLCGco1VyjB9tvvuvYtfZzjD5A8igzgw3HeWeeKFmanHYqksqZXYXGsw5zjnj7KM9/*)",
get_test_wpkh(),
Network::Testnet,
).unwrap().create_wallet_no_persist().unwrap();
// new index 0
assert_eq!(
@@ -3127,11 +2982,13 @@ fn test_sending_to_bip350_bech32m_address() {
fn test_get_address() {
use bdk_wallet::descriptor::template::Bip84;
let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let wallet = Wallet::new(
let wallet = CreateParams::new(
Bip84(key, KeychainKind::External),
Bip84(key, KeychainKind::Internal),
Network::Regtest,
)
.unwrap()
.create_wallet_no_persist()
.unwrap();
assert_eq!(
@@ -3160,7 +3017,10 @@ fn test_get_address() {
#[test]
fn test_reveal_addresses() {
let (desc, change_desc) = get_test_tr_single_sig_xprv_with_change_desc();
let mut wallet = Wallet::new(desc, change_desc, Network::Signet).unwrap();
let mut wallet = CreateParams::new(desc, change_desc, Network::Signet)
.expect("must parse")
.create_wallet_no_persist()
.unwrap();
let keychain = KeychainKind::External;
let last_revealed_addr = wallet.reveal_addresses_to(keychain, 9).last().unwrap();
@@ -3181,11 +3041,13 @@ fn test_get_address_no_reuse() {
use std::collections::HashSet;
let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let mut wallet = Wallet::new(
let mut wallet = CreateParams::new(
Bip84(key, KeychainKind::External),
Bip84(key, KeychainKind::Internal),
Network::Regtest,
)
.unwrap()
.create_wallet_no_persist()
.unwrap();
let mut used_set = HashSet::new();
@@ -3655,11 +3517,13 @@ fn test_taproot_sign_derive_index_from_psbt() {
let mut psbt = builder.finish().unwrap();
// re-create the wallet with an empty db
let wallet_empty = Wallet::new(
let wallet_empty = CreateParams::new(
get_test_tr_single_sig_xprv(),
get_test_tr_single_sig(),
Network::Regtest,
)
.unwrap()
.create_wallet_no_persist()
.unwrap();
// signing with an empty db means that we will only look at the psbt to infer the
@@ -3760,7 +3624,10 @@ fn test_taproot_sign_non_default_sighash() {
#[test]
fn test_spend_coinbase() {
let (desc, change_desc) = get_test_wpkh_with_change_desc();
let mut wallet = Wallet::new(desc, change_desc, Network::Regtest).unwrap();
let mut wallet = CreateParams::new(desc, change_desc, Network::Regtest)
.unwrap()
.create_wallet_no_persist()
.unwrap();
let confirmation_height = 5;
wallet
@@ -4014,6 +3881,7 @@ fn test_taproot_load_descriptor_duplicated_keys() {
/// [#1483]: https://github.com/bitcoindevkit/bdk/issues/1483
/// [#1486]: https://github.com/bitcoindevkit/bdk/pull/1486
#[test]
#[cfg(debug_assertions)]
#[should_panic(
expected = "replenish lookahead: must not have existing spk: keychain=Internal, lookahead=25, next_store_index=0, next_reveal_index=0"
)]