Compare commits
2 Commits
release/0.
...
remove-blo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8de422dcfd | ||
|
|
733300623e |
9
.github/workflows/cont_integration.yml
vendored
9
.github/workflows/cont_integration.yml
vendored
@@ -28,7 +28,6 @@ jobs:
|
|||||||
- async-interface
|
- async-interface
|
||||||
- use-esplora-reqwest
|
- use-esplora-reqwest
|
||||||
- sqlite
|
- sqlite
|
||||||
- sqlite-bundled
|
|
||||||
steps:
|
steps:
|
||||||
- name: checkout
|
- name: checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -90,13 +89,13 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
blockchain:
|
blockchain:
|
||||||
- name: electrum
|
- name: electrum
|
||||||
features: test-electrum,verify
|
features: test-electrum
|
||||||
- name: rpc
|
- name: rpc
|
||||||
features: test-rpc
|
features: test-rpc
|
||||||
- name: esplora
|
- name: esplora
|
||||||
features: test-esplora,use-esplora-reqwest,verify
|
features: test-esplora,use-esplora-reqwest
|
||||||
- name: esplora
|
- name: esplora
|
||||||
features: test-esplora,use-esplora-ureq,verify
|
features: test-esplora,use-esplora-ureq
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@@ -115,7 +114,7 @@ jobs:
|
|||||||
override: true
|
override: true
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.name }}::bdk_blockchain_tests
|
||||||
|
|
||||||
check-wasm:
|
check-wasm:
|
||||||
name: Check WASM
|
name: Check WASM
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-20.04
|
||||||
|
|||||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
target
|
target
|
||||||
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||||
- name: Set default toolchain
|
- name: Set default toolchain
|
||||||
run: rustup default nightly-2022-01-25
|
run: rustup default nightly
|
||||||
- name: Set profile
|
- name: Set profile
|
||||||
run: rustup set profile minimal
|
run: rustup set profile minimal
|
||||||
- name: Update toolchain
|
- name: Update toolchain
|
||||||
|
|||||||
43
CHANGELOG.md
43
CHANGELOG.md
@@ -6,54 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v0.18.0] - [v0.17.0]
|
|
||||||
|
|
||||||
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, ie. for mobile platforms.
|
|
||||||
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
|
|
||||||
- Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
|
|
||||||
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
|
|
||||||
- Improve key generation traits
|
|
||||||
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
|
|
||||||
- Bump `miniscript` dependency version to `^6.1`.
|
|
||||||
|
|
||||||
## [v0.17.0] - [v0.16.1]
|
|
||||||
|
|
||||||
- Removed default verification from `wallet::sync`. sync-time verification is added in `script_sync` and is activated by `verify` feature flag.
|
|
||||||
- `verify` flag removed from `TransactionDetails`.
|
|
||||||
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
|
|
||||||
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
|
|
||||||
- Add `is_spent` field to `LocalUtxo`; when we notice that a utxo has been spent we set `is_spent` field to true instead of deleting it from the db.
|
|
||||||
|
|
||||||
### Sync API change
|
### Sync API change
|
||||||
|
|
||||||
To decouple the `Wallet` from the `Blockchain` we've made major changes:
|
To decouple the `Wallet` from the `Blockchain` we've made major changes:
|
||||||
|
|
||||||
- Removed `Blockchain` from Wallet.
|
- Removed `Blockchain` from Wallet.
|
||||||
- Removed `Wallet::broadcast` (just use `Blockchain::broadcast`)
|
- Removed `Wallet::broadcast` (just use `Blockchain::broadcast`)
|
||||||
- Deprecated `Wallet::new_offline` (all wallets are offline now)
|
- Depreciated `Wallet::new_offline` (all wallets are offline now)
|
||||||
- Changed `Wallet::sync` to take a `Blockchain`.
|
- Changed `Wallet::sync` to take a `Blockchain`.
|
||||||
- Stop making a request for the block height when calling `Wallet:new`.
|
- Stop making a request for the block height when calling `Wallet:new`.
|
||||||
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
|
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
|
||||||
- Removed `max_addresses` sync parameter which determined how many addresses to cache before syncing since this can just be done with `ensure_addresses_cached`.
|
|
||||||
|
|
||||||
## [v0.16.1] - [v0.16.0]
|
|
||||||
|
|
||||||
- Pin tokio dependency version to ~1.14 to prevent errors due to their new MSRV 1.49.0
|
|
||||||
|
|
||||||
## [v0.16.0] - [v0.15.0]
|
|
||||||
|
|
||||||
- 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.
|
|
||||||
- Use dust_value from rust-bitcoin
|
|
||||||
- Fixed generating WIF in the correct network format.
|
|
||||||
|
|
||||||
## [v0.15.0] - [v0.14.0]
|
## [v0.15.0] - [v0.14.0]
|
||||||
|
|
||||||
- Overhauled sync logic for electrum and esplora.
|
- 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.
|
- 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 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]
|
## [v0.14.0] - [v0.13.0]
|
||||||
|
|
||||||
@@ -430,6 +401,7 @@ final transaction is created by calling `finish` on the builder.
|
|||||||
- Use `MemoryDatabase` in the compiler example
|
- Use `MemoryDatabase` in the compiler example
|
||||||
- Make the REPL return JSON
|
- Make the REPL return JSON
|
||||||
|
|
||||||
|
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.11.0...HEAD
|
||||||
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
|
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
|
||||||
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
|
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
|
||||||
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
|
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
|
||||||
@@ -446,8 +418,3 @@ final transaction is created by calling `finish` on the builder.
|
|||||||
[v0.13.0]: https://github.com/bitcoindevkit/bdk/compare/v0.12.0...v0.13.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.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
|
[v0.15.0]: https://github.com/bitcoindevkit/bdk/compare/v0.14.0...v0.15.0
|
||||||
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0
|
|
||||||
[v0.16.1]: https://github.com/bitcoindevkit/bdk/compare/v0.16.0...v0.16.1
|
|
||||||
[v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
|
|
||||||
[v0.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0
|
|
||||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...HEAD
|
|
||||||
|
|||||||
14
Cargo.toml
14
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
version = "0.19.0-dev"
|
version = "0.15.1-dev"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||||
homepage = "https://bitcoindevkit.org"
|
homepage = "https://bitcoindevkit.org"
|
||||||
@@ -14,7 +14,7 @@ license = "MIT OR Apache-2.0"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
bdk-macros = "^0.6"
|
bdk-macros = "^0.6"
|
||||||
log = "^0.4"
|
log = "^0.4"
|
||||||
miniscript = { version = "^6.1", features = ["use-serde"] }
|
miniscript = { version = "^6.0", features = ["use-serde"] }
|
||||||
bitcoin = { version = "^0.27", features = ["use-serde", "base64"] }
|
bitcoin = { version = "^0.27", features = ["use-serde", "base64"] }
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde_json = { version = "^1.0" }
|
serde_json = { version = "^1.0" }
|
||||||
@@ -42,7 +42,7 @@ bitcoincore-rpc = { version = "0.14", optional = true }
|
|||||||
|
|
||||||
# Platform-specific dependencies
|
# Platform-specific dependencies
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
tokio = { version = "~1.14", features = ["rt"] }
|
tokio = { version = "1", features = ["rt"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
@@ -55,7 +55,6 @@ compiler = ["miniscript/compiler"]
|
|||||||
verify = ["bitcoinconsensus"]
|
verify = ["bitcoinconsensus"]
|
||||||
default = ["key-value-db", "electrum"]
|
default = ["key-value-db", "electrum"]
|
||||||
sqlite = ["rusqlite", "ahash"]
|
sqlite = ["rusqlite", "ahash"]
|
||||||
sqlite-bundled = ["sqlite", "rusqlite/bundled"]
|
|
||||||
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
||||||
key-value-db = ["sled"]
|
key-value-db = ["sled"]
|
||||||
all-keys = ["keys-bip39"]
|
all-keys = ["keys-bip39"]
|
||||||
@@ -97,7 +96,7 @@ test-md-docs = ["electrum"]
|
|||||||
lazy_static = "1.4"
|
lazy_static = "1.4"
|
||||||
env_logger = "0.7"
|
env_logger = "0.7"
|
||||||
clap = "2.33"
|
clap = "2.33"
|
||||||
electrsd = { version= "0.15", features = ["trigger", "bitcoind_22_0"] }
|
electrsd = { version= "0.13", features = ["trigger", "bitcoind_22_0"] }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "address_validator"
|
name = "address_validator"
|
||||||
@@ -110,11 +109,6 @@ name = "miniscriptc"
|
|||||||
path = "examples/compiler.rs"
|
path = "examples/compiler.rs"
|
||||||
required-features = ["compiler"]
|
required-features = ["compiler"]
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "rpcwallet"
|
|
||||||
path = "examples/rpcwallet.rs"
|
|
||||||
required-features = ["keys-bip39", "key-value-db", "rpc"]
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["macros"]
|
members = ["macros"]
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|||||||
@@ -1,229 +0,0 @@
|
|||||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
|
||||||
//
|
|
||||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
|
||||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
|
||||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
|
||||||
// You may not use this file except in accordance with one or both of these
|
|
||||||
// licenses.
|
|
||||||
|
|
||||||
use bdk::bitcoin::secp256k1::Secp256k1;
|
|
||||||
use bdk::bitcoin::Amount;
|
|
||||||
use bdk::bitcoin::Network;
|
|
||||||
use bdk::bitcoincore_rpc::RpcApi;
|
|
||||||
|
|
||||||
use bdk::blockchain::rpc::{Auth, RpcBlockchain, RpcConfig};
|
|
||||||
use bdk::blockchain::ConfigurableBlockchain;
|
|
||||||
|
|
||||||
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
|
|
||||||
use bdk::keys::{DerivableKey, GeneratableKey, GeneratedKey};
|
|
||||||
|
|
||||||
use bdk::miniscript::miniscript::Segwitv0;
|
|
||||||
|
|
||||||
use bdk::sled;
|
|
||||||
use bdk::template::Bip84;
|
|
||||||
use bdk::wallet::{signer::SignOptions, wallet_name_from_descriptor, AddressIndex, SyncOptions};
|
|
||||||
use bdk::KeychainKind;
|
|
||||||
use bdk::Wallet;
|
|
||||||
|
|
||||||
use bdk::blockchain::Blockchain;
|
|
||||||
|
|
||||||
use electrsd;
|
|
||||||
|
|
||||||
use std::error::Error;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::str::FromStr;
|
|
||||||
|
|
||||||
/// This example demonstrates a typical way to create a wallet and work with bdk.
|
|
||||||
///
|
|
||||||
/// This example bdk wallet is connected to a bitcoin core rpc regtest node,
|
|
||||||
/// and will attempt to receive, create and broadcast transactions.
|
|
||||||
///
|
|
||||||
/// To start a bitcoind regtest node programmatically, this example uses
|
|
||||||
/// `electrsd` library, which is also a bdk dev-dependency.
|
|
||||||
///
|
|
||||||
/// But you can start your own bitcoind backend, and the rest of the example should work fine.
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
// -- Setting up background bitcoind process
|
|
||||||
|
|
||||||
println!(">> Setting up bitcoind");
|
|
||||||
|
|
||||||
// Start the bitcoind process
|
|
||||||
let bitcoind_conf = electrsd::bitcoind::Conf::default();
|
|
||||||
|
|
||||||
// electrsd will automatically download the bitcoin core binaries
|
|
||||||
let bitcoind_exe =
|
|
||||||
electrsd::bitcoind::downloaded_exe_path().expect("We should always have downloaded path");
|
|
||||||
|
|
||||||
// Launch bitcoind and gather authentication access
|
|
||||||
let bitcoind = electrsd::bitcoind::BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap();
|
|
||||||
let bitcoind_auth = Auth::Cookie {
|
|
||||||
file: bitcoind.params.cookie_file.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get a new core address
|
|
||||||
let core_address = bitcoind.client.get_new_address(None, None)?;
|
|
||||||
|
|
||||||
// Generate 101 blocks and use the above address as coinbase
|
|
||||||
bitcoind.client.generate_to_address(101, &core_address)?;
|
|
||||||
|
|
||||||
println!(">> bitcoind setup complete");
|
|
||||||
println!(
|
|
||||||
"Available coins in Core wallet : {}",
|
|
||||||
bitcoind.client.get_balance(None, None)?
|
|
||||||
);
|
|
||||||
|
|
||||||
// -- Setting up the Wallet
|
|
||||||
|
|
||||||
println!("\n>> Setting up BDK wallet");
|
|
||||||
|
|
||||||
// Get a random private key
|
|
||||||
let xprv = generate_random_ext_privkey()?;
|
|
||||||
|
|
||||||
// Use the derived descriptors from the privatekey to
|
|
||||||
// create unique wallet name.
|
|
||||||
// This is a special utility function exposed via `bdk::wallet_name_from_descriptor()`
|
|
||||||
let wallet_name = wallet_name_from_descriptor(
|
|
||||||
Bip84(xprv.clone(), KeychainKind::External),
|
|
||||||
Some(Bip84(xprv.clone(), KeychainKind::Internal)),
|
|
||||||
Network::Regtest,
|
|
||||||
&Secp256k1::new(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Create a database (using default sled type) to store wallet data
|
|
||||||
let mut datadir = PathBuf::from_str("/tmp/")?;
|
|
||||||
datadir.push(".bdk-example");
|
|
||||||
let database = sled::open(datadir)?;
|
|
||||||
let database = database.open_tree(wallet_name.clone())?;
|
|
||||||
|
|
||||||
// Create a RPC configuration of the running bitcoind backend we created in last step
|
|
||||||
// Note: If you are using custom regtest node, use the appropriate url and auth
|
|
||||||
let rpc_config = RpcConfig {
|
|
||||||
url: bitcoind.params.rpc_socket.to_string(),
|
|
||||||
auth: bitcoind_auth,
|
|
||||||
network: Network::Regtest,
|
|
||||||
wallet_name,
|
|
||||||
skip_blocks: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use the above configuration to create a RPC blockchain backend
|
|
||||||
let blockchain = RpcBlockchain::from_config(&rpc_config)?;
|
|
||||||
|
|
||||||
// Combine Database + Descriptor to create the final wallet
|
|
||||||
let wallet = Wallet::new(
|
|
||||||
Bip84(xprv.clone(), KeychainKind::External),
|
|
||||||
Some(Bip84(xprv.clone(), KeychainKind::Internal)),
|
|
||||||
Network::Regtest,
|
|
||||||
database,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// The `wallet` and the `blockchain` are independent structs.
|
|
||||||
// The wallet will be used to do all wallet level actions
|
|
||||||
// The blockchain can be used to do all blockchain level actions.
|
|
||||||
// For certain actions (like sync) the wallet will ask for a blockchain.
|
|
||||||
|
|
||||||
// Sync the wallet
|
|
||||||
// The first sync is important as this will instantiate the
|
|
||||||
// wallet files.
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
|
||||||
|
|
||||||
println!(">> BDK wallet setup complete.");
|
|
||||||
println!(
|
|
||||||
"Available initial coins in BDK wallet : {} sats",
|
|
||||||
wallet.get_balance()?
|
|
||||||
);
|
|
||||||
|
|
||||||
// -- Wallet transaction demonstration
|
|
||||||
|
|
||||||
println!("\n>> Sending coins: Core --> BDK, 10 BTC");
|
|
||||||
// Get a new address to receive coins
|
|
||||||
let bdk_new_addr = wallet.get_address(AddressIndex::New)?.address;
|
|
||||||
|
|
||||||
// Send 10 BTC from core wallet to bdk wallet
|
|
||||||
bitcoind.client.send_to_address(
|
|
||||||
&bdk_new_addr,
|
|
||||||
Amount::from_btc(10.0)?,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Confirm transaction by generating 1 block
|
|
||||||
bitcoind.client.generate_to_address(1, &core_address)?;
|
|
||||||
|
|
||||||
// Sync the BDK wallet
|
|
||||||
// This time the sync will fetch the new transaction and update it in
|
|
||||||
// wallet database
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
|
||||||
|
|
||||||
println!(">> Received coins in BDK wallet");
|
|
||||||
println!(
|
|
||||||
"Available balance in BDK wallet: {} sats",
|
|
||||||
wallet.get_balance()?
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("\n>> Sending coins: BDK --> Core, 5 BTC");
|
|
||||||
// Attempt to send back 5.0 BTC to core address by creating a transaction
|
|
||||||
//
|
|
||||||
// Transactions are created using a `TxBuilder`.
|
|
||||||
// This helps us to systematically build a transaction with all
|
|
||||||
// required customization.
|
|
||||||
// A full list of APIs offered by `TxBuilder` can be found at
|
|
||||||
// https://docs.rs/bdk/latest/bdk/wallet/tx_builder/struct.TxBuilder.html
|
|
||||||
let mut tx_builder = wallet.build_tx();
|
|
||||||
|
|
||||||
// For a regular transaction, just set the recipient and amount
|
|
||||||
tx_builder.set_recipients(vec![(core_address.script_pubkey(), 500000000)]);
|
|
||||||
|
|
||||||
// Finalize the transaction and extract the PSBT
|
|
||||||
let (mut psbt, _) = tx_builder.finish()?;
|
|
||||||
|
|
||||||
// Set signing option
|
|
||||||
let signopt = SignOptions {
|
|
||||||
assume_height: None,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sign the psbt
|
|
||||||
wallet.sign(&mut psbt, signopt)?;
|
|
||||||
|
|
||||||
// Extract the signed transaction
|
|
||||||
let tx = psbt.extract_tx();
|
|
||||||
|
|
||||||
// Broadcast the transaction
|
|
||||||
blockchain.broadcast(&tx)?;
|
|
||||||
|
|
||||||
// Confirm transaction by generating some blocks
|
|
||||||
bitcoind.client.generate_to_address(1, &core_address)?;
|
|
||||||
|
|
||||||
// Sync the BDK wallet
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
|
||||||
|
|
||||||
println!(">> Coins sent to Core wallet");
|
|
||||||
println!(
|
|
||||||
"Remaining BDK wallet balance: {} sats",
|
|
||||||
wallet.get_balance()?
|
|
||||||
);
|
|
||||||
println!("\nCongrats!! you made your first test transaction with bdk and bitcoin core.");
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function demonstrating privatekey extraction using bip39 mnemonic
|
|
||||||
// The mnemonic can be shown to user to safekeeping and the same wallet
|
|
||||||
// private descriptors can be recreated from it.
|
|
||||||
fn generate_random_ext_privkey() -> Result<impl DerivableKey<Segwitv0> + Clone, Box<dyn Error>> {
|
|
||||||
// a Bip39 passphrase can be set optionally
|
|
||||||
let password = Some("random password".to_string());
|
|
||||||
|
|
||||||
// Generate a random mnemonic, and use that to create a "DerivableKey"
|
|
||||||
let mnemonic: GeneratedKey<_, _> = Mnemonic::generate((WordCount::Words12, Language::English))
|
|
||||||
.map_err(|e| e.expect("Unknown Error"))?;
|
|
||||||
|
|
||||||
// `Ok(mnemonic)` would also work if there's no passphrase and it would
|
|
||||||
// yield the same result as this construct with `password` = `None`.
|
|
||||||
Ok((mnemonic, password))
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
//!
|
//!
|
||||||
//! ## Example
|
//! ## Example
|
||||||
//!
|
//!
|
||||||
//! When paired with the use of [`ConfigurableBlockchain`], it allows creating any
|
//! When paired with the use of [`ConfigurableBlockchain`], it allows creating wallets with any
|
||||||
//! blockchain type supported using a single line of code:
|
//! blockchain type supported using a single line of code:
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
@@ -89,6 +89,9 @@ impl Blockchain for AnyBlockchain {
|
|||||||
maybe_await!(impl_inner_method!(self, get_capabilities))
|
maybe_await!(impl_inner_method!(self, get_capabilities))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
|
maybe_await!(impl_inner_method!(self, get_tx, txid))
|
||||||
|
}
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
maybe_await!(impl_inner_method!(self, broadcast, tx))
|
maybe_await!(impl_inner_method!(self, broadcast, tx))
|
||||||
}
|
}
|
||||||
@@ -98,21 +101,12 @@ impl Blockchain for AnyBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
|
||||||
impl GetHeight for AnyBlockchain {
|
impl GetHeight for AnyBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
maybe_await!(impl_inner_method!(self, get_height))
|
maybe_await!(impl_inner_method!(self, get_height))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
|
||||||
impl GetTx for AnyBlockchain {
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
|
||||||
maybe_await!(impl_inner_method!(self, get_tx, txid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[maybe_async]
|
|
||||||
impl WalletSync for AnyBlockchain {
|
impl WalletSync for AnyBlockchain {
|
||||||
fn wallet_sync<D: BatchDatabase>(
|
fn wallet_sync<D: BatchDatabase>(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ mod peer;
|
|||||||
mod store;
|
mod store;
|
||||||
mod sync;
|
mod sync;
|
||||||
|
|
||||||
use crate::blockchain::*;
|
use super::{Blockchain, Capability, ConfigurableBlockchain, GetHeight, Progress, WalletSync};
|
||||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
|
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
|
||||||
@@ -163,19 +163,11 @@ impl CompactFiltersBlockchain {
|
|||||||
if let Some(previous_output) = database.get_previous_output(&input.previous_output)? {
|
if let Some(previous_output) = database.get_previous_output(&input.previous_output)? {
|
||||||
inputs_sum += previous_output.value;
|
inputs_sum += previous_output.value;
|
||||||
|
|
||||||
// this output is ours, we have a path to derive it
|
if database.is_mine(&previous_output.script_pubkey)? {
|
||||||
if let Some((keychain, _)) =
|
|
||||||
database.get_path_from_script_pubkey(&previous_output.script_pubkey)?
|
|
||||||
{
|
|
||||||
outgoing += previous_output.value;
|
outgoing += previous_output.value;
|
||||||
|
|
||||||
debug!("{} input #{} is mine, setting utxo as spent", tx.txid(), i);
|
debug!("{} input #{} is mine, removing from utxo", tx.txid(), i);
|
||||||
updates.set_utxo(&LocalUtxo {
|
updates.del_utxo(&input.previous_output)?;
|
||||||
outpoint: input.previous_output,
|
|
||||||
txout: previous_output.clone(),
|
|
||||||
keychain,
|
|
||||||
is_spent: true,
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +185,6 @@ impl CompactFiltersBlockchain {
|
|||||||
outpoint: OutPoint::new(tx.txid(), i as u32),
|
outpoint: OutPoint::new(tx.txid(), i as u32),
|
||||||
txout: output.clone(),
|
txout: output.clone(),
|
||||||
keychain,
|
keychain,
|
||||||
is_spent: false,
|
|
||||||
})?;
|
})?;
|
||||||
incoming += output.value;
|
incoming += output.value;
|
||||||
|
|
||||||
@@ -216,6 +207,7 @@ impl CompactFiltersBlockchain {
|
|||||||
received: incoming,
|
received: incoming,
|
||||||
sent: outgoing,
|
sent: outgoing,
|
||||||
confirmation_time: BlockTime::new(height, timestamp),
|
confirmation_time: BlockTime::new(height, timestamp),
|
||||||
|
verified: height.is_some(),
|
||||||
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
|
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -234,6 +226,12 @@ impl Blockchain for CompactFiltersBlockchain {
|
|||||||
vec![Capability::FullHistory].into_iter().collect()
|
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> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
self.peers[0].broadcast_tx(tx.clone())?;
|
self.peers[0].broadcast_tx(tx.clone())?;
|
||||||
|
|
||||||
@@ -252,14 +250,6 @@ impl GetHeight for CompactFiltersBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetTx for CompactFiltersBlockchain {
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
|
||||||
Ok(self.peers[0]
|
|
||||||
.get_mempool()
|
|
||||||
.get_tx(&Inventory::Transaction(*txid)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletSync for CompactFiltersBlockchain {
|
impl WalletSync for CompactFiltersBlockchain {
|
||||||
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase>(
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ impl Blockchain for ElectrumBlockchain {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
|
Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
|
||||||
}
|
}
|
||||||
@@ -90,12 +94,6 @@ impl GetHeight for ElectrumBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetTx for ElectrumBlockchain {
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
|
||||||
Ok(self.client.transaction_get(txid).map(Option::Some)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletSync for ElectrumBlockchain {
|
impl WalletSync for ElectrumBlockchain {
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase>(
|
||||||
&self,
|
&self,
|
||||||
@@ -204,7 +202,6 @@ impl WalletSync for ElectrumBlockchain {
|
|||||||
let full_details = full_transactions
|
let full_details = full_transactions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|tx| {
|
.map(|tx| {
|
||||||
let mut input_index = 0usize;
|
|
||||||
let prev_outputs = tx
|
let prev_outputs = tx
|
||||||
.input
|
.input
|
||||||
.iter()
|
.iter()
|
||||||
@@ -219,7 +216,6 @@ impl WalletSync for ElectrumBlockchain {
|
|||||||
.output
|
.output
|
||||||
.get(input.previous_output.vout as usize)
|
.get(input.previous_output.vout as usize)
|
||||||
.ok_or_else(electrum_goof)?;
|
.ok_or_else(electrum_goof)?;
|
||||||
input_index += 1;
|
|
||||||
Ok(Some(txout.clone()))
|
Ok(Some(txout.clone()))
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, Error>>()?;
|
.collect::<Result<Vec<_>, Error>>()?;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub struct Vin {
|
|||||||
// None if coinbase
|
// None if coinbase
|
||||||
pub prevout: Option<PrevOut>,
|
pub prevout: Option<PrevOut>,
|
||||||
pub scriptsig: Script,
|
pub scriptsig: Script,
|
||||||
#[serde(deserialize_with = "deserialize_witness", default)]
|
#[serde(deserialize_with = "deserialize_witness")]
|
||||||
pub witness: Vec<Vec<u8>>,
|
pub witness: Vec<Vec<u8>>,
|
||||||
pub sequence: u32,
|
pub sequence: u32,
|
||||||
pub is_coinbase: bool,
|
pub is_coinbase: bool,
|
||||||
|
|||||||
@@ -91,6 +91,10 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
Ok(await_or_block!(self.url_client._broadcast(tx))?)
|
Ok(await_or_block!(self.url_client._broadcast(tx))?)
|
||||||
}
|
}
|
||||||
@@ -108,19 +112,12 @@ impl GetHeight for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
|
||||||
impl GetTx for EsploraBlockchain {
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
|
||||||
Ok(await_or_block!(self.url_client._get_tx(txid))?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl WalletSync for EsploraBlockchain {
|
impl WalletSync for EsploraBlockchain {
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase, P: Progress>(
|
||||||
&self,
|
&self,
|
||||||
database: &mut D,
|
database: &mut D,
|
||||||
_progress_update: Box<dyn Progress>,
|
_progress_update: P,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
use crate::blockchain::script_sync::Request;
|
use crate::blockchain::script_sync::Request;
|
||||||
let mut request = script_sync::start(database, self.stop_gap)?;
|
let mut request = script_sync::start(database, self.stop_gap)?;
|
||||||
@@ -193,9 +190,9 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
.request()
|
.request()
|
||||||
.map(|txid| {
|
.map(|txid| {
|
||||||
let tx = tx_index.get(txid).expect("must be in index");
|
let tx = tx_index.get(txid).expect("must be in index");
|
||||||
Ok((tx.previous_outputs(), tx.to_tx()))
|
(tx.previous_outputs(), tx.to_tx())
|
||||||
})
|
})
|
||||||
.collect::<Result<_, Error>>()?;
|
.collect();
|
||||||
tx_req.satisfy(full_txs)?
|
tx_req.satisfy(full_txs)?
|
||||||
}
|
}
|
||||||
Request::Finish(batch_update) => break batch_update,
|
Request::Finish(batch_update) => break batch_update,
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ impl Blockchain for EsploraBlockchain {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
|
Ok(self.url_client._get_tx(txid)?)
|
||||||
|
}
|
||||||
|
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
let _txid = self.url_client._broadcast(tx)?;
|
let _txid = self.url_client._broadcast(tx)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -104,12 +108,6 @@ impl GetHeight for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetTx for EsploraBlockchain {
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
|
||||||
Ok(self.url_client._get_tx(txid)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WalletSync for EsploraBlockchain {
|
impl WalletSync for EsploraBlockchain {
|
||||||
fn wallet_setup<D: BatchDatabase>(
|
fn wallet_setup<D: BatchDatabase>(
|
||||||
&self,
|
&self,
|
||||||
@@ -127,11 +125,10 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
.take(self.concurrency as usize)
|
.take(self.concurrency as usize)
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
let mut handles = vec![];
|
let handles = scripts.map(move |script| {
|
||||||
for script in scripts {
|
|
||||||
let client = self.url_client.clone();
|
let client = self.url_client.clone();
|
||||||
// make each request in its own thread.
|
// make each request in its own thread.
|
||||||
handles.push(std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
let mut related_txs: Vec<Tx> = client._scripthash_txs(&script, None)?;
|
let mut related_txs: Vec<Tx> = client._scripthash_txs(&script, None)?;
|
||||||
|
|
||||||
let n_confirmed =
|
let n_confirmed =
|
||||||
@@ -153,11 +150,10 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Result::<_, Error>::Ok(related_txs)
|
Result::<_, Error>::Ok(related_txs)
|
||||||
}));
|
})
|
||||||
}
|
});
|
||||||
|
|
||||||
let txs_per_script: Vec<Vec<Tx>> = handles
|
let txs_per_script: Vec<Vec<Tx>> = handles
|
||||||
.into_iter()
|
|
||||||
.map(|handle| handle.join().unwrap())
|
.map(|handle| handle.join().unwrap())
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
let mut satisfaction = vec![];
|
let mut satisfaction = vec![];
|
||||||
@@ -192,9 +188,9 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
.request()
|
.request()
|
||||||
.map(|txid| {
|
.map(|txid| {
|
||||||
let tx = tx_index.get(txid).expect("must be in index");
|
let tx = tx_index.get(txid).expect("must be in index");
|
||||||
Ok((tx.previous_outputs(), tx.to_tx()))
|
(tx.previous_outputs(), tx.to_tx())
|
||||||
})
|
})
|
||||||
.collect::<Result<_, Error>>()?;
|
.collect();
|
||||||
tx_req.satisfy(full_txs)?
|
tx_req.satisfy(full_txs)?
|
||||||
}
|
}
|
||||||
Request::Finish(batch_update) => break batch_update,
|
Request::Finish(batch_update) => break batch_update,
|
||||||
|
|||||||
@@ -86,9 +86,11 @@ pub enum Capability {
|
|||||||
|
|
||||||
/// Trait that defines the actions that must be supported by a blockchain backend
|
/// Trait that defines the actions that must be supported by a blockchain backend
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
pub trait Blockchain: WalletSync + GetHeight + GetTx {
|
pub trait Blockchain: WalletSync + GetHeight {
|
||||||
/// Return the set of [`Capability`] supported by this backend
|
/// Return the set of [`Capability`] supported by this backend
|
||||||
fn get_capabilities(&self) -> HashSet<Capability>;
|
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
|
/// Broadcast a transaction
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
|
||||||
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
|
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
|
||||||
@@ -102,13 +104,6 @@ pub trait GetHeight {
|
|||||||
fn get_height(&self) -> Result<u32, Error>;
|
fn get_height(&self) -> Result<u32, Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
|
||||||
/// Trait for getting a transaction by txid
|
|
||||||
pub trait GetTx {
|
|
||||||
/// Fetch a transaction given its txid
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Trait for blockchains that can sync by updating the database directly.
|
/// Trait for blockchains that can sync by updating the database directly.
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
pub trait WalletSync {
|
pub trait WalletSync {
|
||||||
@@ -235,6 +230,9 @@ impl<T: Blockchain> Blockchain for Arc<T> {
|
|||||||
maybe_await!(self.deref().get_capabilities())
|
maybe_await!(self.deref().get_capabilities())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
|
maybe_await!(self.deref().get_tx(txid))
|
||||||
|
}
|
||||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
maybe_await!(self.deref().broadcast(tx))
|
maybe_await!(self.deref().broadcast(tx))
|
||||||
}
|
}
|
||||||
@@ -244,13 +242,6 @@ impl<T: Blockchain> Blockchain for Arc<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[maybe_async]
|
|
||||||
impl<T: GetTx> GetTx for Arc<T> {
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
|
||||||
maybe_await!(self.deref().get_tx(txid))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[maybe_async]
|
#[maybe_async]
|
||||||
impl<T: GetHeight> GetHeight for Arc<T> {
|
impl<T: GetHeight> GetHeight for Arc<T> {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
|
|||||||
@@ -33,7 +33,9 @@
|
|||||||
|
|
||||||
use crate::bitcoin::consensus::deserialize;
|
use crate::bitcoin::consensus::deserialize;
|
||||||
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
|
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
|
||||||
use crate::blockchain::*;
|
use crate::blockchain::{
|
||||||
|
Blockchain, Capability, ConfigurableBlockchain, GetHeight, Progress, WalletSync,
|
||||||
|
};
|
||||||
use crate::database::{BatchDatabase, DatabaseUtils};
|
use crate::database::{BatchDatabase, DatabaseUtils};
|
||||||
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
||||||
use bitcoincore_rpc::json::{
|
use bitcoincore_rpc::json::{
|
||||||
@@ -139,6 +141,10 @@ impl Blockchain for RpcBlockchain {
|
|||||||
self.capabilities.clone()
|
self.capabilities.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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> {
|
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||||
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
|
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
|
||||||
}
|
}
|
||||||
@@ -155,12 +161,6 @@ impl Blockchain for RpcBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GetTx for RpcBlockchain {
|
|
||||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
|
||||||
Ok(Some(self.client.get_raw_transaction(txid, None)?))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GetHeight for RpcBlockchain {
|
impl GetHeight for RpcBlockchain {
|
||||||
fn get_height(&self) -> Result<u32, Error> {
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
|
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
|
||||||
@@ -249,7 +249,7 @@ impl WalletSync for RpcBlockchain {
|
|||||||
let mut list_txs_ids = HashSet::new();
|
let mut list_txs_ids = HashSet::new();
|
||||||
|
|
||||||
for tx_result in list_txs.iter().filter(|t| {
|
for tx_result in list_txs.iter().filter(|t| {
|
||||||
// list_txs returns all conflicting txs, we want to
|
// list_txs returns all conflicting tx we want to
|
||||||
// filter out replaced tx => unconfirmed and not in the mempool
|
// filter out replaced tx => unconfirmed and not in the mempool
|
||||||
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
|
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
|
||||||
}) {
|
}) {
|
||||||
@@ -286,9 +286,7 @@ impl WalletSync for RpcBlockchain {
|
|||||||
|
|
||||||
for input in tx.input.iter() {
|
for input in tx.input.iter() {
|
||||||
if let Some(previous_output) = db.get_previous_output(&input.previous_output)? {
|
if let Some(previous_output) = db.get_previous_output(&input.previous_output)? {
|
||||||
if db.is_mine(&previous_output.script_pubkey)? {
|
sent += previous_output.value;
|
||||||
sent += previous_output.value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,6 +300,7 @@ impl WalletSync for RpcBlockchain {
|
|||||||
received,
|
received,
|
||||||
sent,
|
sent,
|
||||||
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
|
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
|
||||||
|
verified: true,
|
||||||
};
|
};
|
||||||
debug!(
|
debug!(
|
||||||
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
|
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
|
||||||
@@ -318,37 +317,32 @@ impl WalletSync for RpcBlockchain {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out trasactions that are for script pubkeys that aren't in this wallet.
|
let current_utxos: HashSet<_> = current_utxo
|
||||||
let current_utxos = current_utxo
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(
|
.map(|u| {
|
||||||
|u| match db.get_path_from_script_pubkey(&u.script_pub_key) {
|
Ok(LocalUtxo {
|
||||||
Err(e) => Some(Err(e)),
|
outpoint: OutPoint::new(u.txid, u.vout),
|
||||||
Ok(None) => None,
|
keychain: db
|
||||||
Ok(Some(path)) => Some(Ok(LocalUtxo {
|
.get_path_from_script_pubkey(&u.script_pub_key)?
|
||||||
outpoint: OutPoint::new(u.txid, u.vout),
|
.ok_or(Error::TransactionNotFound)?
|
||||||
keychain: path.0,
|
.0,
|
||||||
txout: TxOut {
|
txout: TxOut {
|
||||||
value: u.amount.as_sat(),
|
value: u.amount.as_sat(),
|
||||||
script_pubkey: u.script_pub_key,
|
script_pubkey: u.script_pub_key,
|
||||||
},
|
},
|
||||||
is_spent: false,
|
})
|
||||||
})),
|
})
|
||||||
},
|
.collect::<Result<_, Error>>()?;
|
||||||
)
|
|
||||||
.collect::<Result<HashSet<_>, Error>>()?;
|
|
||||||
|
|
||||||
let spent: HashSet<_> = known_utxos.difference(¤t_utxos).collect();
|
let spent: HashSet<_> = known_utxos.difference(¤t_utxos).collect();
|
||||||
for utxo in spent {
|
for s in spent {
|
||||||
debug!("setting as spent utxo: {:?}", utxo);
|
debug!("removing utxo: {:?}", s);
|
||||||
let mut spent_utxo = utxo.clone();
|
db.del_utxo(&s.outpoint)?;
|
||||||
spent_utxo.is_spent = true;
|
|
||||||
db.set_utxo(&spent_utxo)?;
|
|
||||||
}
|
}
|
||||||
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
|
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
|
||||||
for utxo in received {
|
for s in received {
|
||||||
debug!("adding utxo: {:?}", utxo);
|
debug!("adding utxo: {:?}", s);
|
||||||
db.set_utxo(utxo)?;
|
db.set_utxo(s)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (keykind, index) in indexes {
|
for (keykind, index) in indexes {
|
||||||
|
|||||||
@@ -178,9 +178,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
|
|||||||
let mut inputs_sum: u64 = 0;
|
let mut inputs_sum: u64 = 0;
|
||||||
let mut outputs_sum: u64 = 0;
|
let mut outputs_sum: u64 = 0;
|
||||||
|
|
||||||
for (txout, (_input_index, input)) in
|
for (txout, input) in vout.into_iter().zip(tx.input.iter()) {
|
||||||
vout.into_iter().zip(tx.input.iter().enumerate())
|
|
||||||
{
|
|
||||||
let txout = match txout {
|
let txout = match txout {
|
||||||
Some(txout) => txout,
|
Some(txout) => txout,
|
||||||
None => {
|
None => {
|
||||||
@@ -192,19 +190,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Verify this input if requested via feature flag
|
|
||||||
#[cfg(feature = "verify")]
|
|
||||||
{
|
|
||||||
use crate::wallet::verify::VerifyError;
|
|
||||||
let serialized_tx = bitcoin::consensus::serialize(&tx);
|
|
||||||
bitcoinconsensus::verify(
|
|
||||||
txout.script_pubkey.to_bytes().as_ref(),
|
|
||||||
txout.value,
|
|
||||||
&serialized_tx,
|
|
||||||
_input_index,
|
|
||||||
)
|
|
||||||
.map_err(VerifyError::from)?;
|
|
||||||
}
|
|
||||||
inputs_sum += txout.value;
|
inputs_sum += txout.value;
|
||||||
if self.state.db.is_mine(&txout.script_pubkey)? {
|
if self.state.db.is_mine(&txout.script_pubkey)? {
|
||||||
sent += txout.value;
|
sent += txout.value;
|
||||||
@@ -228,6 +214,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
|
|||||||
// we're going to fill this in later
|
// we're going to fill this in later
|
||||||
confirmation_time: None,
|
confirmation_time: None,
|
||||||
fee: Some(fee),
|
fee: Some(fee),
|
||||||
|
verified: false,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Result<Vec<_>, _>>()?;
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
@@ -332,23 +319,7 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
|||||||
batch.del_tx(txid, true)?;
|
batch.del_tx(txid, true)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut spent_utxos = HashSet::new();
|
// Set every tx we observed
|
||||||
|
|
||||||
// track all the spent utxos
|
|
||||||
for finished_tx in &finished_txs {
|
|
||||||
let tx = finished_tx
|
|
||||||
.transaction
|
|
||||||
.as_ref()
|
|
||||||
.expect("transaction will always be present here");
|
|
||||||
for input in &tx.input {
|
|
||||||
spent_utxos.insert(&input.previous_output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// set every utxo we observed, unless it's already spent
|
|
||||||
// we don't do this in the loop above as we want to know all the spent outputs before
|
|
||||||
// adding the non-spent to the batch in case there are new tranasactions
|
|
||||||
// that spend form each other.
|
|
||||||
for finished_tx in &finished_txs {
|
for finished_tx in &finished_txs {
|
||||||
let tx = finished_tx
|
let tx = finished_tx
|
||||||
.transaction
|
.transaction
|
||||||
@@ -359,24 +330,32 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
|||||||
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
|
self.db.get_path_from_script_pubkey(&output.script_pubkey)?
|
||||||
{
|
{
|
||||||
// add utxos we own from the new transactions we've seen.
|
// add utxos we own from the new transactions we've seen.
|
||||||
let outpoint = OutPoint {
|
|
||||||
txid: finished_tx.txid,
|
|
||||||
vout: i as u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
batch.set_utxo(&LocalUtxo {
|
batch.set_utxo(&LocalUtxo {
|
||||||
outpoint,
|
outpoint: OutPoint {
|
||||||
|
txid: finished_tx.txid,
|
||||||
|
vout: i as u32,
|
||||||
|
},
|
||||||
txout: output.clone(),
|
txout: output.clone(),
|
||||||
keychain,
|
keychain,
|
||||||
// Is this UTXO in the spent_utxos set?
|
|
||||||
is_spent: spent_utxos.get(&outpoint).is_some(),
|
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
batch.set_tx(finished_tx)?;
|
batch.set_tx(finished_tx)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we don't do this in the loop above since we may want to delete some of the utxos we
|
||||||
|
// just added in case there are new tranasactions that spend form each other.
|
||||||
|
for finished_tx in &finished_txs {
|
||||||
|
let tx = finished_tx
|
||||||
|
.transaction
|
||||||
|
.as_ref()
|
||||||
|
.expect("transaction will always be present here");
|
||||||
|
for input in &tx.input {
|
||||||
|
// Delete any spent utxos
|
||||||
|
batch.del_utxo(&input.previous_output)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (keychain, last_active_index) in self.last_active_index {
|
for (keychain, last_active_index) in self.last_active_index {
|
||||||
batch.set_last_index(keychain, last_active_index as u32)?;
|
batch.set_last_index(keychain, last_active_index as u32)?;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ macro_rules! impl_from {
|
|||||||
|
|
||||||
macro_rules! impl_inner_method {
|
macro_rules! impl_inner_method {
|
||||||
( $enum_name:ident, $self:expr, $name:ident $(, $args:expr)* ) => {
|
( $enum_name:ident, $self:expr, $name:ident $(, $args:expr)* ) => {
|
||||||
#[allow(deprecated)]
|
|
||||||
match $self {
|
match $self {
|
||||||
$enum_name::Memory(inner) => inner.$name( $($args, )* ),
|
$enum_name::Memory(inner) => inner.$name( $($args, )* ),
|
||||||
#[cfg(feature = "key-value-db")]
|
#[cfg(feature = "key-value-db")]
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ macro_rules! impl_batch_operations {
|
|||||||
let value = json!({
|
let value = json!({
|
||||||
"t": utxo.txout,
|
"t": utxo.txout,
|
||||||
"i": utxo.keychain,
|
"i": utxo.keychain,
|
||||||
"s": utxo.is_spent,
|
|
||||||
});
|
});
|
||||||
self.insert(key, serde_json::to_vec(&value)?)$($after_insert)*;
|
self.insert(key, serde_json::to_vec(&value)?)$($after_insert)*;
|
||||||
|
|
||||||
@@ -126,9 +125,8 @@ macro_rules! impl_batch_operations {
|
|||||||
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
||||||
let txout = serde_json::from_value(val["t"].take())?;
|
let txout = serde_json::from_value(val["t"].take())?;
|
||||||
let keychain = serde_json::from_value(val["i"].take())?;
|
let keychain = serde_json::from_value(val["i"].take())?;
|
||||||
let is_spent = val.get_mut("s").and_then(|s| s.take().as_bool()).unwrap_or(false);
|
|
||||||
|
|
||||||
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain, is_spent, }))
|
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,16 +246,11 @@ impl Database for Tree {
|
|||||||
let mut val: serde_json::Value = serde_json::from_slice(&v)?;
|
let mut val: serde_json::Value = serde_json::from_slice(&v)?;
|
||||||
let txout = serde_json::from_value(val["t"].take())?;
|
let txout = serde_json::from_value(val["t"].take())?;
|
||||||
let keychain = serde_json::from_value(val["i"].take())?;
|
let keychain = serde_json::from_value(val["i"].take())?;
|
||||||
let is_spent = val
|
|
||||||
.get_mut("s")
|
|
||||||
.and_then(|s| s.take().as_bool())
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
Ok(LocalUtxo {
|
Ok(LocalUtxo {
|
||||||
outpoint,
|
outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
is_spent,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -321,16 +314,11 @@ impl Database for Tree {
|
|||||||
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
let mut val: serde_json::Value = serde_json::from_slice(&b)?;
|
||||||
let txout = serde_json::from_value(val["t"].take())?;
|
let txout = serde_json::from_value(val["t"].take())?;
|
||||||
let keychain = serde_json::from_value(val["i"].take())?;
|
let keychain = serde_json::from_value(val["i"].take())?;
|
||||||
let is_spent = val
|
|
||||||
.get_mut("s")
|
|
||||||
.and_then(|s| s.take().as_bool())
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
Ok(LocalUtxo {
|
Ok(LocalUtxo {
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
is_spent,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.transpose()
|
.transpose()
|
||||||
|
|||||||
@@ -150,10 +150,8 @@ impl BatchOperations for MemoryDatabase {
|
|||||||
|
|
||||||
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
fn set_utxo(&mut self, utxo: &LocalUtxo) -> Result<(), Error> {
|
||||||
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
|
let key = MapKey::Utxo(Some(&utxo.outpoint)).as_map_key();
|
||||||
self.map.insert(
|
self.map
|
||||||
key,
|
.insert(key, Box::new((utxo.txout.clone(), utxo.keychain)));
|
||||||
Box::new((utxo.txout.clone(), utxo.keychain, utxo.is_spent)),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -230,12 +228,11 @@ impl BatchOperations for MemoryDatabase {
|
|||||||
match res {
|
match res {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
let (txout, keychain, is_spent) = b.downcast_ref().cloned().unwrap();
|
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
||||||
Ok(Some(LocalUtxo {
|
Ok(Some(LocalUtxo {
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
is_spent,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,12 +326,11 @@ impl Database for MemoryDatabase {
|
|||||||
.range::<Vec<u8>, _>((Included(&key), Excluded(&after(&key))))
|
.range::<Vec<u8>, _>((Included(&key), Excluded(&after(&key))))
|
||||||
.map(|(k, v)| {
|
.map(|(k, v)| {
|
||||||
let outpoint = deserialize(&k[1..]).unwrap();
|
let outpoint = deserialize(&k[1..]).unwrap();
|
||||||
let (txout, keychain, is_spent) = v.downcast_ref().cloned().unwrap();
|
let (txout, keychain) = v.downcast_ref().cloned().unwrap();
|
||||||
Ok(LocalUtxo {
|
Ok(LocalUtxo {
|
||||||
outpoint,
|
outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
is_spent,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -393,12 +389,11 @@ impl Database for MemoryDatabase {
|
|||||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
let key = MapKey::Utxo(Some(outpoint)).as_map_key();
|
||||||
Ok(self.map.get(&key).map(|b| {
|
Ok(self.map.get(&key).map(|b| {
|
||||||
let (txout, keychain, is_spent) = b.downcast_ref().cloned().unwrap();
|
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
||||||
LocalUtxo {
|
LocalUtxo {
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
is_spent,
|
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -520,6 +515,7 @@ macro_rules! populate_test_db {
|
|||||||
received: 0,
|
received: 0,
|
||||||
sent: 0,
|
sent: 0,
|
||||||
confirmation_time,
|
confirmation_time,
|
||||||
|
verified: current_height.is_some(),
|
||||||
};
|
};
|
||||||
|
|
||||||
db.set_tx(&tx_details).unwrap();
|
db.set_tx(&tx_details).unwrap();
|
||||||
@@ -531,7 +527,6 @@ macro_rules! populate_test_db {
|
|||||||
vout: vout as u32,
|
vout: vout as u32,
|
||||||
},
|
},
|
||||||
keychain: $crate::KeychainKind::External,
|
keychain: $crate::KeychainKind::External,
|
||||||
is_spent: false,
|
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,10 +159,6 @@ pub trait Database: BatchOperations {
|
|||||||
/// It should insert and return `0` if not present in the database
|
/// It should insert and return `0` if not present in the database
|
||||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error>;
|
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error>;
|
||||||
|
|
||||||
#[deprecated(
|
|
||||||
since = "0.18.0",
|
|
||||||
note = "The flush function is only needed for the sled database on mobile, instead for mobile use the sqlite database."
|
|
||||||
)]
|
|
||||||
/// Force changes to be written to disk
|
/// Force changes to be written to disk
|
||||||
fn flush(&mut self) -> Result<(), Error>;
|
fn flush(&mut self) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
@@ -320,7 +316,6 @@ pub mod test {
|
|||||||
txout,
|
txout,
|
||||||
outpoint,
|
outpoint,
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
is_spent: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
tree.set_utxo(&utxo).unwrap();
|
tree.set_utxo(&utxo).unwrap();
|
||||||
@@ -353,6 +348,7 @@ pub mod test {
|
|||||||
timestamp: 123456,
|
timestamp: 123456,
|
||||||
height: 1000,
|
height: 1000,
|
||||||
}),
|
}),
|
||||||
|
verified: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
tree.set_tx(&tx_details).unwrap();
|
tree.set_tx(&tx_details).unwrap();
|
||||||
|
|||||||
@@ -35,12 +35,7 @@ static MIGRATIONS: &[&str] = &[
|
|||||||
"CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);",
|
"CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);",
|
||||||
"CREATE TABLE checksums (keychain TEXT, checksum BLOB);",
|
"CREATE TABLE checksums (keychain TEXT, checksum BLOB);",
|
||||||
"CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
|
"CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
|
||||||
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);",
|
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);"
|
||||||
"ALTER TABLE transaction_details RENAME TO transaction_details_old;",
|
|
||||||
"CREATE TABLE transaction_details (txid BLOB, timestamp INTEGER, received INTEGER, sent INTEGER, fee INTEGER, height INTEGER);",
|
|
||||||
"INSERT INTO transaction_details SELECT txid, timestamp, received, sent, fee, height FROM transaction_details_old;",
|
|
||||||
"DROP TABLE transaction_details_old;",
|
|
||||||
"ALTER TABLE utxos ADD COLUMN is_spent;",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Sqlite database stored on filesystem
|
/// Sqlite database stored on filesystem
|
||||||
@@ -84,16 +79,14 @@ impl SqliteDatabase {
|
|||||||
vout: u32,
|
vout: u32,
|
||||||
txid: &[u8],
|
txid: &[u8],
|
||||||
script: &[u8],
|
script: &[u8],
|
||||||
is_spent: bool,
|
|
||||||
) -> Result<i64, Error> {
|
) -> Result<i64, Error> {
|
||||||
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script, is_spent) VALUES (:value, :keychain, :vout, :txid, :script, :is_spent)")?;
|
let mut statement = self.connection.prepare_cached("INSERT INTO utxos (value, keychain, vout, txid, script) VALUES (:value, :keychain, :vout, :txid, :script)")?;
|
||||||
statement.execute(named_params! {
|
statement.execute(named_params! {
|
||||||
":value": value,
|
":value": value,
|
||||||
":keychain": keychain,
|
":keychain": keychain,
|
||||||
":vout": vout,
|
":vout": vout,
|
||||||
":txid": txid,
|
":txid": txid,
|
||||||
":script": script,
|
":script": script
|
||||||
":is_spent": is_spent,
|
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(self.connection.last_insert_rowid())
|
Ok(self.connection.last_insert_rowid())
|
||||||
@@ -134,7 +127,7 @@ impl SqliteDatabase {
|
|||||||
|
|
||||||
let txid: &[u8] = &transaction.txid;
|
let txid: &[u8] = &transaction.txid;
|
||||||
|
|
||||||
let mut statement = self.connection.prepare_cached("INSERT INTO transaction_details (txid, timestamp, received, sent, fee, height) VALUES (:txid, :timestamp, :received, :sent, :fee, :height)")?;
|
let mut statement = self.connection.prepare_cached("INSERT INTO transaction_details (txid, timestamp, received, sent, fee, height, verified) VALUES (:txid, :timestamp, :received, :sent, :fee, :height, :verified)")?;
|
||||||
|
|
||||||
statement.execute(named_params! {
|
statement.execute(named_params! {
|
||||||
":txid": txid,
|
":txid": txid,
|
||||||
@@ -143,6 +136,7 @@ impl SqliteDatabase {
|
|||||||
":sent": transaction.sent,
|
":sent": transaction.sent,
|
||||||
":fee": transaction.fee,
|
":fee": transaction.fee,
|
||||||
":height": height,
|
":height": height,
|
||||||
|
":verified": transaction.verified
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(self.connection.last_insert_rowid())
|
Ok(self.connection.last_insert_rowid())
|
||||||
@@ -159,7 +153,7 @@ impl SqliteDatabase {
|
|||||||
|
|
||||||
let txid: &[u8] = &transaction.txid;
|
let txid: &[u8] = &transaction.txid;
|
||||||
|
|
||||||
let mut statement = self.connection.prepare_cached("UPDATE transaction_details SET timestamp=:timestamp, received=:received, sent=:sent, fee=:fee, height=:height WHERE txid=:txid")?;
|
let mut statement = self.connection.prepare_cached("UPDATE transaction_details SET timestamp=:timestamp, received=:received, sent=:sent, fee=:fee, height=:height, verified=:verified WHERE txid=:txid")?;
|
||||||
|
|
||||||
statement.execute(named_params! {
|
statement.execute(named_params! {
|
||||||
":txid": txid,
|
":txid": txid,
|
||||||
@@ -168,6 +162,7 @@ impl SqliteDatabase {
|
|||||||
":sent": transaction.sent,
|
":sent": transaction.sent,
|
||||||
":fee": transaction.fee,
|
":fee": transaction.fee,
|
||||||
":height": height,
|
":height": height,
|
||||||
|
":verified": transaction.verified,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -294,7 +289,7 @@ impl SqliteDatabase {
|
|||||||
fn select_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
|
fn select_utxos(&self) -> Result<Vec<LocalUtxo>, Error> {
|
||||||
let mut statement = self
|
let mut statement = self
|
||||||
.connection
|
.connection
|
||||||
.prepare_cached("SELECT value, keychain, vout, txid, script, is_spent FROM utxos")?;
|
.prepare_cached("SELECT value, keychain, vout, txid, script FROM utxos")?;
|
||||||
let mut utxos: Vec<LocalUtxo> = vec![];
|
let mut utxos: Vec<LocalUtxo> = vec![];
|
||||||
let mut rows = statement.query([])?;
|
let mut rows = statement.query([])?;
|
||||||
while let Some(row) = rows.next()? {
|
while let Some(row) = rows.next()? {
|
||||||
@@ -303,7 +298,6 @@ impl SqliteDatabase {
|
|||||||
let vout = row.get(2)?;
|
let vout = row.get(2)?;
|
||||||
let txid: Vec<u8> = row.get(3)?;
|
let txid: Vec<u8> = row.get(3)?;
|
||||||
let script: Vec<u8> = row.get(4)?;
|
let script: Vec<u8> = row.get(4)?;
|
||||||
let is_spent: bool = row.get(5)?;
|
|
||||||
|
|
||||||
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
|
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
|
||||||
|
|
||||||
@@ -314,16 +308,19 @@ impl SqliteDatabase {
|
|||||||
script_pubkey: script.into(),
|
script_pubkey: script.into(),
|
||||||
},
|
},
|
||||||
keychain,
|
keychain,
|
||||||
is_spent,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(utxos)
|
Ok(utxos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_utxo_by_outpoint(&self, txid: &[u8], vout: u32) -> Result<Option<LocalUtxo>, Error> {
|
fn select_utxo_by_outpoint(
|
||||||
|
&self,
|
||||||
|
txid: &[u8],
|
||||||
|
vout: u32,
|
||||||
|
) -> Result<Option<(u64, KeychainKind, Script)>, Error> {
|
||||||
let mut statement = self.connection.prepare_cached(
|
let mut statement = self.connection.prepare_cached(
|
||||||
"SELECT value, keychain, script, is_spent FROM utxos WHERE txid=:txid AND vout=:vout",
|
"SELECT value, keychain, script FROM utxos WHERE txid=:txid AND vout=:vout",
|
||||||
)?;
|
)?;
|
||||||
let mut rows = statement.query(named_params! {":txid": txid,":vout": vout})?;
|
let mut rows = statement.query(named_params! {":txid": txid,":vout": vout})?;
|
||||||
match rows.next()? {
|
match rows.next()? {
|
||||||
@@ -332,18 +329,9 @@ impl SqliteDatabase {
|
|||||||
let keychain: String = row.get(1)?;
|
let keychain: String = row.get(1)?;
|
||||||
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
|
let keychain: KeychainKind = serde_json::from_str(&keychain)?;
|
||||||
let script: Vec<u8> = row.get(2)?;
|
let script: Vec<u8> = row.get(2)?;
|
||||||
let script_pubkey: Script = script.into();
|
let script: Script = script.into();
|
||||||
let is_spent: bool = row.get(3)?;
|
|
||||||
|
|
||||||
Ok(Some(LocalUtxo {
|
Ok(Some((value, keychain, script)))
|
||||||
outpoint: OutPoint::new(deserialize(txid)?, vout),
|
|
||||||
txout: TxOut {
|
|
||||||
value,
|
|
||||||
script_pubkey,
|
|
||||||
},
|
|
||||||
keychain,
|
|
||||||
is_spent,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
@@ -379,7 +367,7 @@ impl SqliteDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn select_transaction_details_with_raw(&self) -> Result<Vec<TransactionDetails>, Error> {
|
fn select_transaction_details_with_raw(&self) -> Result<Vec<TransactionDetails>, Error> {
|
||||||
let mut statement = self.connection.prepare_cached("SELECT transaction_details.txid, transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid = transactions.txid")?;
|
let mut statement = self.connection.prepare_cached("SELECT transaction_details.txid, transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transaction_details.verified, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid = transactions.txid")?;
|
||||||
let mut transaction_details: Vec<TransactionDetails> = vec![];
|
let mut transaction_details: Vec<TransactionDetails> = vec![];
|
||||||
let mut rows = statement.query([])?;
|
let mut rows = statement.query([])?;
|
||||||
while let Some(row) = rows.next()? {
|
while let Some(row) = rows.next()? {
|
||||||
@@ -390,6 +378,7 @@ impl SqliteDatabase {
|
|||||||
let sent: u64 = row.get(3)?;
|
let sent: u64 = row.get(3)?;
|
||||||
let fee: Option<u64> = row.get(4)?;
|
let fee: Option<u64> = row.get(4)?;
|
||||||
let height: Option<u32> = row.get(5)?;
|
let height: Option<u32> = row.get(5)?;
|
||||||
|
let verified: bool = row.get(6)?;
|
||||||
let raw_tx: Option<Vec<u8>> = row.get(7)?;
|
let raw_tx: Option<Vec<u8>> = row.get(7)?;
|
||||||
let tx: Option<Transaction> = match raw_tx {
|
let tx: Option<Transaction> = match raw_tx {
|
||||||
Some(raw_tx) => {
|
Some(raw_tx) => {
|
||||||
@@ -411,6 +400,7 @@ impl SqliteDatabase {
|
|||||||
sent,
|
sent,
|
||||||
fee,
|
fee,
|
||||||
confirmation_time,
|
confirmation_time,
|
||||||
|
verified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(transaction_details)
|
Ok(transaction_details)
|
||||||
@@ -418,7 +408,7 @@ impl SqliteDatabase {
|
|||||||
|
|
||||||
fn select_transaction_details(&self) -> Result<Vec<TransactionDetails>, Error> {
|
fn select_transaction_details(&self) -> Result<Vec<TransactionDetails>, Error> {
|
||||||
let mut statement = self.connection.prepare_cached(
|
let mut statement = self.connection.prepare_cached(
|
||||||
"SELECT txid, timestamp, received, sent, fee, height FROM transaction_details",
|
"SELECT txid, timestamp, received, sent, fee, height, verified FROM transaction_details",
|
||||||
)?;
|
)?;
|
||||||
let mut transaction_details: Vec<TransactionDetails> = vec![];
|
let mut transaction_details: Vec<TransactionDetails> = vec![];
|
||||||
let mut rows = statement.query([])?;
|
let mut rows = statement.query([])?;
|
||||||
@@ -430,6 +420,7 @@ impl SqliteDatabase {
|
|||||||
let sent: u64 = row.get(3)?;
|
let sent: u64 = row.get(3)?;
|
||||||
let fee: Option<u64> = row.get(4)?;
|
let fee: Option<u64> = row.get(4)?;
|
||||||
let height: Option<u32> = row.get(5)?;
|
let height: Option<u32> = row.get(5)?;
|
||||||
|
let verified: bool = row.get(6)?;
|
||||||
|
|
||||||
let confirmation_time = match (height, timestamp) {
|
let confirmation_time = match (height, timestamp) {
|
||||||
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
|
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
|
||||||
@@ -443,6 +434,7 @@ impl SqliteDatabase {
|
|||||||
sent,
|
sent,
|
||||||
fee,
|
fee,
|
||||||
confirmation_time,
|
confirmation_time,
|
||||||
|
verified,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(transaction_details)
|
Ok(transaction_details)
|
||||||
@@ -452,7 +444,7 @@ impl SqliteDatabase {
|
|||||||
&self,
|
&self,
|
||||||
txid: &[u8],
|
txid: &[u8],
|
||||||
) -> Result<Option<TransactionDetails>, Error> {
|
) -> Result<Option<TransactionDetails>, Error> {
|
||||||
let mut statement = self.connection.prepare_cached("SELECT transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid=transactions.txid AND transaction_details.txid=:txid")?;
|
let mut statement = self.connection.prepare_cached("SELECT transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transaction_details.verified, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid=transactions.txid AND transaction_details.txid=:txid")?;
|
||||||
let mut rows = statement.query(named_params! { ":txid": txid })?;
|
let mut rows = statement.query(named_params! { ":txid": txid })?;
|
||||||
|
|
||||||
match rows.next()? {
|
match rows.next()? {
|
||||||
@@ -462,8 +454,9 @@ impl SqliteDatabase {
|
|||||||
let sent: u64 = row.get(2)?;
|
let sent: u64 = row.get(2)?;
|
||||||
let fee: Option<u64> = row.get(3)?;
|
let fee: Option<u64> = row.get(3)?;
|
||||||
let height: Option<u32> = row.get(4)?;
|
let height: Option<u32> = row.get(4)?;
|
||||||
|
let verified: bool = row.get(5)?;
|
||||||
|
|
||||||
let raw_tx: Option<Vec<u8>> = row.get(5)?;
|
let raw_tx: Option<Vec<u8>> = row.get(6)?;
|
||||||
let tx: Option<Transaction> = match raw_tx {
|
let tx: Option<Transaction> = match raw_tx {
|
||||||
Some(raw_tx) => {
|
Some(raw_tx) => {
|
||||||
let tx: Transaction = deserialize(&raw_tx)?;
|
let tx: Transaction = deserialize(&raw_tx)?;
|
||||||
@@ -484,6 +477,7 @@ impl SqliteDatabase {
|
|||||||
sent,
|
sent,
|
||||||
fee,
|
fee,
|
||||||
confirmation_time,
|
confirmation_time,
|
||||||
|
verified,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
@@ -630,7 +624,6 @@ impl BatchOperations for SqliteDatabase {
|
|||||||
utxo.outpoint.vout,
|
utxo.outpoint.vout,
|
||||||
&utxo.outpoint.txid,
|
&utxo.outpoint.txid,
|
||||||
utxo.txout.script_pubkey.as_bytes(),
|
utxo.txout.script_pubkey.as_bytes(),
|
||||||
utxo.is_spent,
|
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -705,9 +698,16 @@ impl BatchOperations for SqliteDatabase {
|
|||||||
|
|
||||||
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
fn del_utxo(&mut self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
|
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
|
||||||
Some(local_utxo) => {
|
Some((value, keychain, script_pubkey)) => {
|
||||||
self.delete_utxo_by_outpoint(&outpoint.txid, outpoint.vout)?;
|
self.delete_utxo_by_outpoint(&outpoint.txid, outpoint.vout)?;
|
||||||
Ok(Some(local_utxo))
|
Ok(Some(LocalUtxo {
|
||||||
|
outpoint: *outpoint,
|
||||||
|
txout: TxOut {
|
||||||
|
value,
|
||||||
|
script_pubkey,
|
||||||
|
},
|
||||||
|
keychain,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
@@ -836,7 +836,17 @@ impl Database for SqliteDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)
|
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
|
||||||
|
Some((value, keychain, script_pubkey)) => Ok(Some(LocalUtxo {
|
||||||
|
outpoint: *outpoint,
|
||||||
|
txout: TxOut {
|
||||||
|
value,
|
||||||
|
script_pubkey,
|
||||||
|
},
|
||||||
|
keychain,
|
||||||
|
})),
|
||||||
|
None => Ok(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_raw_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
|
|||||||
@@ -10,41 +10,6 @@
|
|||||||
// licenses.
|
// licenses.
|
||||||
|
|
||||||
//! Derived descriptor keys
|
//! Derived descriptor keys
|
||||||
//!
|
|
||||||
//! The [`DerivedDescriptorKey`] type is a wrapper over the standard [`DescriptorPublicKey`] which
|
|
||||||
//! guarantees that all the extended keys have a fixed derivation path, i.e. all the wildcards have
|
|
||||||
//! been replaced by actual derivation indexes.
|
|
||||||
//!
|
|
||||||
//! The [`AsDerived`] trait provides a quick way to derive descriptors to obtain a
|
|
||||||
//! `Descriptor<DerivedDescriptorKey>` type. This, in turn, can be used to derive public
|
|
||||||
//! keys for arbitrary derivation indexes.
|
|
||||||
//!
|
|
||||||
//! Combining this with [`Wallet::get_signers`], secret keys can also be derived.
|
|
||||||
//!
|
|
||||||
//! # Example
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
//! # use std::str::FromStr;
|
|
||||||
//! # use bitcoin::secp256k1::Secp256k1;
|
|
||||||
//! use bdk::descriptor::{AsDerived, DescriptorPublicKey};
|
|
||||||
//! use bdk::miniscript::{ToPublicKey, TranslatePk, MiniscriptKey};
|
|
||||||
//!
|
|
||||||
//! let secp = Secp256k1::gen_new();
|
|
||||||
//!
|
|
||||||
//! let key = DescriptorPublicKey::from_str("[aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/*")?;
|
|
||||||
//! let (descriptor, _, _) = bdk::descriptor!(wpkh(key))?;
|
|
||||||
//!
|
|
||||||
//! // derived: wpkh([aa600a45/84'/0'/0']tpubDCbDXFKoLTQp44wQuC12JgSn5g9CWGjZdpBHeTqyypZ4VvgYjTJmK9CkyR5bFvG9f4PutvwmvpYCLkFx2rpx25hiMs4sUgxJveW8ZzSAVAc/0/42)#3ladd0t2
|
|
||||||
//! let derived = descriptor.as_derived(42, &secp);
|
|
||||||
//! println!("derived: {}", derived);
|
|
||||||
//!
|
|
||||||
//! // with_pks: wpkh(02373ecb54c5e83bd7e0d40adf78b65efaf12fafb13571f0261fc90364eee22e1e)#p4jjgvll
|
|
||||||
//! let with_pks = derived.translate_pk_infallible(|pk| pk.to_public_key(), |pkh| pkh.to_public_key().to_pubkeyhash());
|
|
||||||
//! println!("with_pks: {}", with_pks);
|
|
||||||
//! # Ok::<(), Box<dyn std::error::Error>>(())
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! [`Wallet::get_signers`]: crate::wallet::Wallet::get_signers
|
|
||||||
|
|
||||||
use std::cmp::Ordering;
|
use std::cmp::Ordering;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -54,7 +19,10 @@ use std::ops::Deref;
|
|||||||
use bitcoin::hashes::hash160;
|
use bitcoin::hashes::hash160;
|
||||||
use bitcoin::PublicKey;
|
use bitcoin::PublicKey;
|
||||||
|
|
||||||
use miniscript::{descriptor::Wildcard, Descriptor, DescriptorPublicKey};
|
pub use miniscript::{
|
||||||
|
descriptor::KeyMap, descriptor::Wildcard, Descriptor, DescriptorPublicKey, Legacy, Miniscript,
|
||||||
|
ScriptContext, Segwitv0,
|
||||||
|
};
|
||||||
use miniscript::{MiniscriptKey, ToPublicKey, TranslatePk};
|
use miniscript::{MiniscriptKey, ToPublicKey, TranslatePk};
|
||||||
|
|
||||||
use crate::wallet::utils::SecpCtx;
|
use crate::wallet::utils::SecpCtx;
|
||||||
@@ -151,19 +119,14 @@ impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Utilities to derive descriptors
|
pub(crate) trait AsDerived {
|
||||||
///
|
// Derive a descriptor and transform all of its keys to `DerivedDescriptorKey`
|
||||||
/// Check out the [module level] documentation for more.
|
|
||||||
///
|
|
||||||
/// [module level]: crate::descriptor::derived
|
|
||||||
pub trait AsDerived {
|
|
||||||
/// Derive a descriptor and transform all of its keys to `DerivedDescriptorKey`
|
|
||||||
fn as_derived<'s>(&self, index: u32, secp: &'s SecpCtx)
|
fn as_derived<'s>(&self, index: u32, secp: &'s SecpCtx)
|
||||||
-> Descriptor<DerivedDescriptorKey<'s>>;
|
-> Descriptor<DerivedDescriptorKey<'s>>;
|
||||||
|
|
||||||
/// Transform the keys into `DerivedDescriptorKey`.
|
// Transform the keys into `DerivedDescriptorKey`.
|
||||||
///
|
//
|
||||||
/// Panics if the descriptor is not "fixed", i.e. if it's derivable
|
// Panics if the descriptor is not "fixed", i.e. if it's derivable
|
||||||
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>>;
|
fn as_derived_fixed<'s>(&self, secp: &'s SecpCtx) -> Descriptor<DerivedDescriptorKey<'s>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ macro_rules! apply_modifier {
|
|||||||
/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a
|
/// syntax is more suitable for a fixed number of items known at compile time, while the other accepts a
|
||||||
/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors.
|
/// [`Vec`] of items, which makes it more suitable for writing dynamic descriptors.
|
||||||
///
|
///
|
||||||
/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv:older(...)))`
|
/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sdv:older(...)))`
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # use std::str::FromStr;
|
/// # use std::str::FromStr;
|
||||||
@@ -349,7 +349,7 @@ macro_rules! apply_modifier {
|
|||||||
///
|
///
|
||||||
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
|
/// let (descriptor_a, key_map_a, networks) = bdk::descriptor! {
|
||||||
/// wsh (
|
/// wsh (
|
||||||
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n:d:v:older(my_timelock))
|
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:d:v:older(my_timelock))
|
||||||
/// )
|
/// )
|
||||||
/// }?;
|
/// }?;
|
||||||
///
|
///
|
||||||
@@ -357,7 +357,7 @@ macro_rules! apply_modifier {
|
|||||||
/// let b_items = vec![
|
/// let b_items = vec![
|
||||||
/// bdk::fragment!(pk(my_key_1))?,
|
/// bdk::fragment!(pk(my_key_1))?,
|
||||||
/// bdk::fragment!(s:pk(my_key_2))?,
|
/// bdk::fragment!(s:pk(my_key_2))?,
|
||||||
/// bdk::fragment!(s:n:d:v:older(my_timelock))?,
|
/// bdk::fragment!(s:d:v:older(my_timelock))?,
|
||||||
/// ];
|
/// ];
|
||||||
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
|
/// let (descriptor_b, mut key_map_b, networks) = bdk::descriptor!(wsh(thresh_vec(2, b_items)))?;
|
||||||
///
|
///
|
||||||
@@ -1048,9 +1048,9 @@ mod test {
|
|||||||
let private_key =
|
let private_key =
|
||||||
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
|
PrivateKey::from_wif("cSQPHDBwXGjVzWRqAHm6zfvQhaTuj1f2bFH58h55ghbjtFwvmeXR").unwrap();
|
||||||
let (descriptor, _, _) =
|
let (descriptor, _, _) =
|
||||||
descriptor!(wsh(thresh(2,n:d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
|
descriptor!(wsh(thresh(2,d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
|
||||||
|
|
||||||
assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g")
|
assert_eq!(descriptor.to_string(), "wsh(thresh(2,dv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#cfdcqs3s")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -17,21 +17,20 @@
|
|||||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
|
||||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
use bitcoin::util::bip32::{
|
||||||
|
ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, KeySource,
|
||||||
|
};
|
||||||
use bitcoin::util::psbt;
|
use bitcoin::util::psbt;
|
||||||
use bitcoin::{Network, PublicKey, Script, TxOut};
|
use bitcoin::{Network, PublicKey, Script, TxOut};
|
||||||
|
|
||||||
use miniscript::descriptor::{DescriptorType, InnerXKey};
|
use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey, Wildcard};
|
||||||
pub use miniscript::{
|
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
|
||||||
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
|
||||||
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
|
||||||
};
|
|
||||||
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
||||||
|
|
||||||
use crate::descriptor::policy::BuildSatisfaction;
|
use crate::descriptor::policy::BuildSatisfaction;
|
||||||
|
|
||||||
pub mod checksum;
|
pub mod checksum;
|
||||||
pub mod derived;
|
pub(crate) mod derived;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod dsl;
|
pub mod dsl;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -39,7 +38,8 @@ pub mod policy;
|
|||||||
pub mod template;
|
pub mod template;
|
||||||
|
|
||||||
pub use self::checksum::get_checksum;
|
pub use self::checksum::get_checksum;
|
||||||
pub use self::derived::{AsDerived, DerivedDescriptorKey};
|
use self::derived::AsDerived;
|
||||||
|
pub use self::derived::DerivedDescriptorKey;
|
||||||
pub use self::error::Error as DescriptorError;
|
pub use self::error::Error as DescriptorError;
|
||||||
pub use self::policy::Policy;
|
pub use self::policy::Policy;
|
||||||
use self::template::DescriptorTemplateOut;
|
use self::template::DescriptorTemplateOut;
|
||||||
@@ -267,10 +267,41 @@ pub(crate) trait XKeyUtils {
|
|||||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> XKeyUtils for DescriptorXKey<T>
|
// FIXME: `InnerXKey` was made private in rust-miniscript, so we have to implement this manually on
|
||||||
where
|
// both `ExtendedPubKey` and `ExtendedPrivKey`.
|
||||||
T: InnerXKey,
|
//
|
||||||
{
|
// Revert back to using the trait once https://github.com/rust-bitcoin/rust-miniscript/pull/230 is
|
||||||
|
// released
|
||||||
|
impl XKeyUtils for DescriptorXKey<ExtendedPubKey> {
|
||||||
|
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
||||||
|
let full_path = match self.origin {
|
||||||
|
Some((_, ref path)) => path
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.derivation_path.into_iter())
|
||||||
|
.cloned()
|
||||||
|
.collect(),
|
||||||
|
None => self.derivation_path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.wildcard != Wildcard::None {
|
||||||
|
full_path
|
||||||
|
.into_iter()
|
||||||
|
.chain(append.iter())
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
full_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn root_fingerprint(&self, _: &SecpCtx) -> Fingerprint {
|
||||||
|
match self.origin {
|
||||||
|
Some((fingerprint, _)) => fingerprint,
|
||||||
|
None => self.xkey.fingerprint(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl XKeyUtils for DescriptorXKey<ExtendedPrivKey> {
|
||||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
||||||
let full_path = match self.origin {
|
let full_path = match self.origin {
|
||||||
Some((_, ref path)) => path
|
Some((_, ref path)) => path
|
||||||
@@ -295,7 +326,7 @@ where
|
|||||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
||||||
match self.origin {
|
match self.origin {
|
||||||
Some((fingerprint, _)) => fingerprint,
|
Some((fingerprint, _)) => fingerprint,
|
||||||
None => self.xkey.xkey_fingerprint(secp),
|
None => self.xkey.fingerprint(secp),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1501,7 +1501,7 @@ mod test {
|
|||||||
let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
|
let (prvkey_bob, _, _) = setup_keys(BOB_TPRV_STR, ALICE_BOB_PATH, &secp);
|
||||||
|
|
||||||
let desc =
|
let desc =
|
||||||
descriptor!(wsh(thresh(2,n:d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
|
descriptor!(wsh(thresh(2,d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
|
||||||
|
|
||||||
let (wallet_desc, keymap) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
@@ -1513,7 +1513,7 @@ mod test {
|
|||||||
.address(Network::Testnet)
|
.address(Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
"tb1qsydsey4hexagwkvercqsmes6yet0ndkyt6uzcphtqnygjd8hmzmsfxrv58",
|
"tb1qhpemaacpeu8ajlnh8k9v55ftg0px58r8630fz8t5mypxcwdk5d8sum522g",
|
||||||
addr.to_string()
|
addr.to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -94,23 +94,6 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for MnemonicWithPassphrase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for (GeneratedKey<Mnemonic, Ctx>, Option<String>) {
|
|
||||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
|
||||||
let (mnemonic, passphrase) = self;
|
|
||||||
(mnemonic.into_key(), passphrase).into_extended_key()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_descriptor_key(
|
|
||||||
self,
|
|
||||||
source: Option<bip32::KeySource>,
|
|
||||||
derivation_path: bip32::DerivationPath,
|
|
||||||
) -> Result<DescriptorKey<Ctx>, KeyError> {
|
|
||||||
let (mnemonic, passphrase) = self;
|
|
||||||
(mnemonic.into_key(), passphrase).into_descriptor_key(source, derivation_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
|
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Mnemonic {
|
||||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||||
|
|||||||
@@ -548,16 +548,6 @@ impl<K, Ctx: ScriptContext> Deref for GeneratedKey<K, Ctx> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<K: Clone, Ctx: ScriptContext> Clone for GeneratedKey<K, Ctx> {
|
|
||||||
fn clone(&self) -> GeneratedKey<K, Ctx> {
|
|
||||||
GeneratedKey {
|
|
||||||
key: self.key.clone(),
|
|
||||||
valid_networks: self.valid_networks.clone(),
|
|
||||||
phantom: self.phantom,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the
|
// Make generated "derivable" keys themselves "derivable". Also make sure they are assigned the
|
||||||
// right `valid_networks`.
|
// right `valid_networks`.
|
||||||
impl<Ctx, K> DerivableKey<Ctx> for GeneratedKey<K, Ctx>
|
impl<Ctx, K> DerivableKey<Ctx> for GeneratedKey<K, Ctx>
|
||||||
|
|||||||
10
src/lib.rs
10
src/lib.rs
@@ -44,15 +44,14 @@
|
|||||||
//! interact with the bitcoin P2P network.
|
//! interact with the bitcoin P2P network.
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```toml
|
||||||
//! bdk = "0.18.0"
|
//! bdk = "0.15.0"
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
|
||||||
//! # Examples
|
|
||||||
# mnemonic codes for generating deterministic keys
|
//! * `keys-bip39`: [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic codes for generating deterministic keys
|
||||||
//!
|
//!
|
||||||
//! # Internal features
|
//! ## Internal features
|
||||||
//!
|
//!
|
||||||
//! These features do not expose any new API, but influence internal implementation aspects of
|
//! These features do not expose any new API, but influence internal implementation aspects of
|
||||||
//! BDK.
|
//! BDK.
|
||||||
|
|||||||
@@ -90,19 +90,13 @@ impl TestClient {
|
|||||||
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
|
map.insert(out.to_address.clone(), Amount::from_sat(out.value));
|
||||||
}
|
}
|
||||||
|
|
||||||
let input: Vec<_> = meta_tx
|
|
||||||
.input
|
|
||||||
.into_iter()
|
|
||||||
.map(|x| x.into_raw_tx_input())
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
|
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
|
||||||
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
|
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: core can't create a tx with two outputs to the same address
|
// FIXME: core can't create a tx with two outputs to the same address
|
||||||
let tx = self
|
let tx = self
|
||||||
.create_raw_transaction_hex(&input, &map, meta_tx.locktime, meta_tx.replaceable)
|
.create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
|
let tx = self.fund_raw_transaction(tx, None, None).unwrap();
|
||||||
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
|
let mut tx: Transaction = deserialize(&tx.hex).unwrap();
|
||||||
@@ -320,7 +314,7 @@ impl Default for TestClient {
|
|||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let bitcoind_exe = env::var("BITCOIND_EXE")
|
let bitcoind_exe = env::var("BITCOIND_EXE")
|
||||||
.ok()
|
.ok()
|
||||||
.or(bitcoind::downloaded_exe_path().ok())
|
.or(bitcoind::downloaded_exe_path())
|
||||||
.expect(
|
.expect(
|
||||||
"you should provide env var BITCOIND_EXE or specifiy a bitcoind version feature",
|
"you should provide env var BITCOIND_EXE or specifiy a bitcoind version feature",
|
||||||
);
|
);
|
||||||
@@ -359,7 +353,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
fn $_fn_name:ident ( $( $test_client:ident : &TestClient )? $(,)? ) -> $blockchain:ty $block:block) => {
|
fn $_fn_name:ident ( $( $test_client:ident : &TestClient )? $(,)? ) -> $blockchain:ty $block:block) => {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod bdk_blockchain_tests {
|
mod bdk_blockchain_tests {
|
||||||
use $crate::bitcoin::{Transaction, Network};
|
use $crate::bitcoin::Network;
|
||||||
use $crate::testutils::blockchain_tests::TestClient;
|
use $crate::testutils::blockchain_tests::TestClient;
|
||||||
use $crate::blockchain::Blockchain;
|
use $crate::blockchain::Blockchain;
|
||||||
use $crate::database::MemoryDatabase;
|
use $crate::database::MemoryDatabase;
|
||||||
@@ -621,7 +615,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_sync_double_receive() {
|
fn test_sync_double_receive() {
|
||||||
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
||||||
let receiver_wallet = get_wallet_from_descriptors(&("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None));
|
let receiver_wallet = get_wallet_from_descriptors(&("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None));
|
||||||
// need to sync so rpc can start watching
|
// need to sync so rpc can start watching
|
||||||
receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
@@ -629,15 +623,15 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
|
||||||
});
|
});
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||||
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||||
|
|
||||||
let tx1 = {
|
let tx1 = {
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf();
|
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf();
|
||||||
let (mut psbt, _details) = builder.finish().expect("building first tx");
|
let (mut psbt, _details) = builder.finish().unwrap();
|
||||||
let finalized = wallet.sign(&mut psbt, Default::default()).expect("signing first tx");
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
psbt.extract_tx()
|
psbt.extract_tx()
|
||||||
};
|
};
|
||||||
@@ -645,17 +639,17 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let tx2 = {
|
let tx2 = {
|
||||||
let mut builder = wallet.build_tx();
|
let mut builder = wallet.build_tx();
|
||||||
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf().fee_rate(FeeRate::from_sat_per_vb(5.0));
|
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf().fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||||
let (mut psbt, _details) = builder.finish().expect("building replacement tx");
|
let (mut psbt, _details) = builder.finish().unwrap();
|
||||||
let finalized = wallet.sign(&mut psbt, Default::default()).expect("signing replacement tx");
|
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
psbt.extract_tx()
|
psbt.extract_tx()
|
||||||
};
|
};
|
||||||
|
|
||||||
blockchain.broadcast(&tx1).expect("broadcasting first");
|
blockchain.broadcast(&tx1).unwrap();
|
||||||
blockchain.broadcast(&tx2).expect("broadcasting replacement");
|
blockchain.broadcast(&tx2).unwrap();
|
||||||
|
|
||||||
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
|
receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
|
assert_eq!(receiver_wallet.get_balance().unwrap(), 49_000, "should have received coins once and only once");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -817,7 +811,7 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
|
|
||||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||||
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
||||||
let (mut new_psbt, new_details) = builder.finish().expect("fee bump tx");
|
let (mut new_psbt, new_details) = builder.finish().unwrap();
|
||||||
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
assert!(finalized, "Cannot finalize transaction");
|
||||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||||
@@ -1081,120 +1075,6 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
|
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
|
||||||
assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance");
|
assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tx_chain() {
|
|
||||||
use bitcoincore_rpc::RpcApi;
|
|
||||||
use bitcoin::consensus::encode::deserialize;
|
|
||||||
use $crate::wallet::AddressIndex;
|
|
||||||
|
|
||||||
// Here we want to test that we set correctly the send and receive
|
|
||||||
// fields in the transaction object. For doing so, we create two
|
|
||||||
// different txs, the second one spending from the first:
|
|
||||||
// 1.
|
|
||||||
// Core (#1) -> Core (#2)
|
|
||||||
// -> Us (#3)
|
|
||||||
// 2.
|
|
||||||
// Core (#2) -> Us (#4)
|
|
||||||
|
|
||||||
let (wallet, blockchain, _, mut test_client) = init_single_sig();
|
|
||||||
let bdk_address = wallet.get_address(AddressIndex::New).unwrap().address;
|
|
||||||
let core_address = test_client.get_new_address(None, None).unwrap();
|
|
||||||
let tx = testutils! {
|
|
||||||
@tx ( (@addr bdk_address.clone()) => 50_000, (@addr core_address.clone()) => 40_000 )
|
|
||||||
};
|
|
||||||
|
|
||||||
// Tx one: from Core #1 to Core #2 and Us #3.
|
|
||||||
let txid_1 = test_client.receive(tx);
|
|
||||||
let tx_1: Transaction = deserialize(&test_client.get_transaction(&txid_1, None).unwrap().hex).unwrap();
|
|
||||||
let vout_1 = tx_1.output.into_iter().position(|o| o.script_pubkey == core_address.script_pubkey()).unwrap() as u32;
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
|
||||||
let tx_1 = wallet.list_transactions(false).unwrap().into_iter().find(|tx| tx.txid == txid_1).unwrap();
|
|
||||||
assert_eq!(tx_1.received, 50_000);
|
|
||||||
assert_eq!(tx_1.sent, 0);
|
|
||||||
|
|
||||||
// Tx two: from Core #2 to Us #4.
|
|
||||||
let tx = testutils! {
|
|
||||||
@tx ( (@addr bdk_address) => 10_000 ) ( @inputs (txid_1,vout_1))
|
|
||||||
};
|
|
||||||
let txid_2 = test_client.receive(tx);
|
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
|
||||||
let tx_2 = wallet.list_transactions(false).unwrap().into_iter().find(|tx| tx.txid == txid_2).unwrap();
|
|
||||||
assert_eq!(tx_2.received, 10_000);
|
|
||||||
assert_eq!(tx_2.sent, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_double_spend() {
|
|
||||||
// We create a tx and then we try to double spend it; BDK will always allow
|
|
||||||
// us to do so, as it never forgets about spent UTXOs
|
|
||||||
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(&blockchain, SyncOptions::default()).unwrap();
|
|
||||||
let mut builder = wallet.build_tx();
|
|
||||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
|
||||||
let (mut psbt, _details) = builder.finish().unwrap();
|
|
||||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
|
||||||
assert!(finalized, "Cannot finalize transaction");
|
|
||||||
let initial_tx = psbt.extract_tx();
|
|
||||||
let _sent_txid = blockchain.broadcast(&initial_tx).unwrap();
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
|
||||||
for utxo in wallet.list_unspent().unwrap() {
|
|
||||||
// Making sure the TXO we just spent is not returned by list_unspent
|
|
||||||
assert!(utxo.outpoint != initial_tx.input[0].previous_output, "wallet displays spent txo in unspents");
|
|
||||||
}
|
|
||||||
// We can still create a transaction double spending `initial_tx`
|
|
||||||
let mut builder = wallet.build_tx();
|
|
||||||
builder
|
|
||||||
.add_utxo(initial_tx.input[0].previous_output)
|
|
||||||
.expect("Can't manually add an UTXO spent");
|
|
||||||
test_client.generate(1, Some(node_addr));
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
|
||||||
// Even after confirmation, we can still create a tx double spend it
|
|
||||||
let mut builder = wallet.build_tx();
|
|
||||||
builder
|
|
||||||
.add_utxo(initial_tx.input[0].previous_output)
|
|
||||||
.expect("Can't manually add an UTXO spent");
|
|
||||||
for utxo in wallet.list_unspent().unwrap() {
|
|
||||||
// Making sure the TXO we just spent is not returned by list_unspent
|
|
||||||
assert!(utxo.outpoint != initial_tx.input[0].previous_output, "wallet displays spent txo in unspents");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_send_receive_pkh() {
|
|
||||||
let descriptors = ("pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None);
|
|
||||||
let mut test_client = TestClient::default();
|
|
||||||
let blockchain = get_blockchain(&test_client);
|
|
||||||
|
|
||||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
|
||||||
#[cfg(feature = "test-rpc")]
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
|
||||||
|
|
||||||
let _ = test_client.receive(testutils! {
|
|
||||||
@tx ( (@external descriptors, 0) => 50_000 )
|
|
||||||
});
|
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
|
||||||
|
|
||||||
let tx = {
|
|
||||||
let mut builder = wallet.build_tx();
|
|
||||||
builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000);
|
|
||||||
let (mut psbt, _details) = builder.finish().unwrap();
|
|
||||||
wallet.sign(&mut psbt, Default::default()).unwrap();
|
|
||||||
psbt.extract_tx()
|
|
||||||
};
|
|
||||||
blockchain.broadcast(&tx).unwrap();
|
|
||||||
|
|
||||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,37 +15,11 @@
|
|||||||
pub mod blockchain_tests;
|
pub mod blockchain_tests;
|
||||||
|
|
||||||
use bitcoin::secp256k1::{Secp256k1, Verification};
|
use bitcoin::secp256k1::{Secp256k1, Verification};
|
||||||
use bitcoin::{Address, PublicKey, Txid};
|
use bitcoin::{Address, PublicKey};
|
||||||
|
|
||||||
use miniscript::descriptor::DescriptorPublicKey;
|
use miniscript::descriptor::DescriptorPublicKey;
|
||||||
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
|
use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct TestIncomingInput {
|
|
||||||
pub txid: Txid,
|
|
||||||
pub vout: u32,
|
|
||||||
pub sequence: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TestIncomingInput {
|
|
||||||
pub fn new(txid: Txid, vout: u32, sequence: Option<u32>) -> Self {
|
|
||||||
Self {
|
|
||||||
txid,
|
|
||||||
vout,
|
|
||||||
sequence,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "test-blockchains")]
|
|
||||||
pub fn into_raw_tx_input(self) -> bitcoincore_rpc::json::CreateRawTransactionInput {
|
|
||||||
bitcoincore_rpc::json::CreateRawTransactionInput {
|
|
||||||
txid: self.txid,
|
|
||||||
vout: self.vout,
|
|
||||||
sequence: self.sequence,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TestIncomingOutput {
|
pub struct TestIncomingOutput {
|
||||||
pub value: u64,
|
pub value: u64,
|
||||||
@@ -63,7 +37,6 @@ impl TestIncomingOutput {
|
|||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct TestIncomingTx {
|
pub struct TestIncomingTx {
|
||||||
pub input: Vec<TestIncomingInput>,
|
|
||||||
pub output: Vec<TestIncomingOutput>,
|
pub output: Vec<TestIncomingOutput>,
|
||||||
pub min_confirmations: Option<u64>,
|
pub min_confirmations: Option<u64>,
|
||||||
pub locktime: Option<i64>,
|
pub locktime: Option<i64>,
|
||||||
@@ -72,14 +45,12 @@ pub struct TestIncomingTx {
|
|||||||
|
|
||||||
impl TestIncomingTx {
|
impl TestIncomingTx {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
input: Vec<TestIncomingInput>,
|
|
||||||
output: Vec<TestIncomingOutput>,
|
output: Vec<TestIncomingOutput>,
|
||||||
min_confirmations: Option<u64>,
|
min_confirmations: Option<u64>,
|
||||||
locktime: Option<i64>,
|
locktime: Option<i64>,
|
||||||
replaceable: Option<bool>,
|
replaceable: Option<bool>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
input,
|
|
||||||
output,
|
output,
|
||||||
min_confirmations,
|
min_confirmations,
|
||||||
locktime,
|
locktime,
|
||||||
@@ -87,10 +58,6 @@ impl TestIncomingTx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_input(&mut self, input: TestIncomingInput) {
|
|
||||||
self.input.push(input);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_output(&mut self, output: TestIncomingOutput) {
|
pub fn add_output(&mut self, output: TestIncomingOutput) {
|
||||||
self.output.push(output);
|
self.output.push(output);
|
||||||
}
|
}
|
||||||
@@ -156,21 +123,16 @@ macro_rules! testutils {
|
|||||||
});
|
});
|
||||||
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
|
||||||
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
|
||||||
( @addr $addr:expr ) => ({ $addr });
|
|
||||||
|
|
||||||
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @inputs $( ($txid:expr, $vout:expr) ),+ ) )? $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
|
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
|
||||||
let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+];
|
let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+];
|
||||||
let _ins: Vec<$crate::testutils::TestIncomingInput> = vec![];
|
|
||||||
$(
|
|
||||||
let _ins = vec![$( $crate::testutils::TestIncomingInput { txid: $txid, vout: $vout, sequence: None }),+];
|
|
||||||
)?
|
|
||||||
|
|
||||||
let locktime = None::<i64>$(.or(Some($locktime)))?;
|
let locktime = None::<i64>$(.or(Some($locktime)))?;
|
||||||
|
|
||||||
let min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
|
let min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
|
||||||
let replaceable = None::<bool>$(.or(Some($replaceable)))?;
|
let replaceable = None::<bool>$(.or(Some($replaceable)))?;
|
||||||
|
|
||||||
$crate::testutils::TestIncomingTx::new(_ins, outs, min_confirmations, locktime, replaceable)
|
$crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
|
||||||
});
|
});
|
||||||
|
|
||||||
( @literal $key:expr ) => ({
|
( @literal $key:expr ) => ({
|
||||||
|
|||||||
11
src/types.rs
11
src/types.rs
@@ -131,8 +131,6 @@ pub struct LocalUtxo {
|
|||||||
pub txout: TxOut,
|
pub txout: TxOut,
|
||||||
/// Type of keychain
|
/// Type of keychain
|
||||||
pub keychain: KeychainKind,
|
pub keychain: KeychainKind,
|
||||||
/// Whether this UTXO is spent or not
|
|
||||||
pub is_spent: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [`Utxo`] with its `satisfaction_weight`.
|
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||||
@@ -213,6 +211,15 @@ pub struct TransactionDetails {
|
|||||||
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
||||||
/// transaction, unconfirmed transaction contains `None`.
|
/// transaction, unconfirmed transaction contains `None`.
|
||||||
pub confirmation_time: Option<BlockTime>,
|
pub confirmation_time: Option<BlockTime>,
|
||||||
|
/// Whether the tx has been verified against the consensus rules
|
||||||
|
///
|
||||||
|
/// Confirmed txs are considered "verified" by default, while unconfirmed txs are checked to
|
||||||
|
/// ensure an unstrusted [`Blockchain`](crate::blockchain::Blockchain) backend can't trick the
|
||||||
|
/// wallet into using an invalid tx as an RBF template.
|
||||||
|
///
|
||||||
|
/// The check is only performed when the `verify` feature is enabled.
|
||||||
|
#[serde(default = "bool::default")] // default to `false` if not specified
|
||||||
|
pub verified: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Block height and timestamp of a block
|
/// Block height and timestamp of a block
|
||||||
|
|||||||
@@ -569,7 +569,6 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
is_spent: false,
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -597,7 +596,6 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
is_spent: false,
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -617,7 +615,6 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
is_spent: false,
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
vec![utxo; utxos_number]
|
vec![utxo; utxos_number]
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
//! "label":"testnet"
|
//! "label":"testnet"
|
||||||
//! }"#;
|
//! }"#;
|
||||||
//!
|
//!
|
||||||
//! let import = FullyNodedExport::from_str(import)?;
|
//! let import = WalletExport::from_str(import)?;
|
||||||
//! let wallet = Wallet::new(
|
//! let wallet = Wallet::new(
|
||||||
//! &import.descriptor(),
|
//! &import.descriptor(),
|
||||||
//! import.change_descriptor().as_ref(),
|
//! import.change_descriptor().as_ref(),
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
//! Network::Testnet,
|
//! Network::Testnet,
|
||||||
//! MemoryDatabase::default()
|
//! MemoryDatabase::default()
|
||||||
//! )?;
|
//! )?;
|
||||||
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
//! let export = WalletExport::export_wallet(&wallet, "exported wallet", true)
|
||||||
//! .map_err(ToString::to_string)
|
//! .map_err(ToString::to_string)
|
||||||
//! .map_err(bdk::Error::Generic)?;
|
//! .map_err(bdk::Error::Generic)?;
|
||||||
//!
|
//!
|
||||||
@@ -64,21 +64,16 @@ use std::str::FromStr;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use miniscript::descriptor::{ShInner, WshInner};
|
use miniscript::descriptor::{ShInner, WshInner};
|
||||||
use miniscript::{Descriptor, ScriptContext, Terminal};
|
use miniscript::{Descriptor, DescriptorPublicKey, ScriptContext, Terminal};
|
||||||
|
|
||||||
use crate::database::BatchDatabase;
|
use crate::database::BatchDatabase;
|
||||||
use crate::types::KeychainKind;
|
|
||||||
use crate::wallet::Wallet;
|
use crate::wallet::Wallet;
|
||||||
|
|
||||||
/// Alias for [`FullyNodedExport`]
|
|
||||||
#[deprecated(since = "0.18.0", note = "Please use [`FullyNodedExport`] instead")]
|
|
||||||
pub type WalletExport = FullyNodedExport;
|
|
||||||
|
|
||||||
/// Structure that contains the export of a wallet
|
/// Structure that contains the export of a wallet
|
||||||
///
|
///
|
||||||
/// For a usage example see [this module](crate::wallet::export)'s documentation.
|
/// For a usage example see [this module](crate::wallet::export)'s documentation.
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct FullyNodedExport {
|
pub struct WalletExport {
|
||||||
descriptor: String,
|
descriptor: String,
|
||||||
/// Earliest block to rescan when looking for the wallet's transactions
|
/// Earliest block to rescan when looking for the wallet's transactions
|
||||||
pub blockheight: u32,
|
pub blockheight: u32,
|
||||||
@@ -86,13 +81,13 @@ pub struct FullyNodedExport {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for FullyNodedExport {
|
impl ToString for WalletExport {
|
||||||
fn to_string(&self) -> String {
|
fn to_string(&self) -> String {
|
||||||
serde_json::to_string(self).unwrap()
|
serde_json::to_string(self).unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for FullyNodedExport {
|
impl FromStr for WalletExport {
|
||||||
type Err = serde_json::Error;
|
type Err = serde_json::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
@@ -104,7 +99,7 @@ fn remove_checksum(s: String) -> String {
|
|||||||
s.splitn(2, '#').next().map(String::from).unwrap()
|
s.splitn(2, '#').next().map(String::from).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FullyNodedExport {
|
impl WalletExport {
|
||||||
/// Export a wallet
|
/// Export a wallet
|
||||||
///
|
///
|
||||||
/// This function returns an error if it determines that the `wallet`'s descriptor(s) are not
|
/// This function returns an error if it determines that the `wallet`'s descriptor(s) are not
|
||||||
@@ -122,12 +117,8 @@ impl FullyNodedExport {
|
|||||||
include_blockheight: bool,
|
include_blockheight: bool,
|
||||||
) -> Result<Self, &'static str> {
|
) -> Result<Self, &'static str> {
|
||||||
let descriptor = wallet
|
let descriptor = wallet
|
||||||
.get_descriptor_for_keychain(KeychainKind::External)
|
.descriptor
|
||||||
.to_string_with_secret(
|
.to_string_with_secret(&wallet.signers.as_key_map(wallet.secp_ctx()));
|
||||||
&wallet
|
|
||||||
.get_signers(KeychainKind::External)
|
|
||||||
.as_key_map(wallet.secp_ctx()),
|
|
||||||
);
|
|
||||||
let descriptor = remove_checksum(descriptor);
|
let descriptor = remove_checksum(descriptor);
|
||||||
Self::is_compatible_with_core(&descriptor)?;
|
Self::is_compatible_with_core(&descriptor)?;
|
||||||
|
|
||||||
@@ -145,30 +136,18 @@ impl FullyNodedExport {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let export = FullyNodedExport {
|
let export = WalletExport {
|
||||||
descriptor,
|
descriptor,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
blockheight,
|
blockheight,
|
||||||
};
|
};
|
||||||
|
|
||||||
let change_descriptor = match wallet
|
let desc_to_string = |d: &Descriptor<DescriptorPublicKey>| {
|
||||||
.public_descriptor(KeychainKind::Internal)
|
let descriptor =
|
||||||
.map_err(|_| "Invalid change descriptor")?
|
d.to_string_with_secret(&wallet.change_signers.as_key_map(wallet.secp_ctx()));
|
||||||
.is_some()
|
remove_checksum(descriptor)
|
||||||
{
|
|
||||||
false => None,
|
|
||||||
true => {
|
|
||||||
let descriptor = wallet
|
|
||||||
.get_descriptor_for_keychain(KeychainKind::Internal)
|
|
||||||
.to_string_with_secret(
|
|
||||||
&wallet
|
|
||||||
.get_signers(KeychainKind::Internal)
|
|
||||||
.as_key_map(wallet.secp_ctx()),
|
|
||||||
);
|
|
||||||
Some(remove_checksum(descriptor))
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
if export.change_descriptor() != change_descriptor {
|
if export.change_descriptor() != wallet.change_descriptor.as_ref().map(desc_to_string) {
|
||||||
return Err("Incompatible change descriptor");
|
return Err("Incompatible change descriptor");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,6 +230,7 @@ mod test {
|
|||||||
timestamp: 12345678,
|
timestamp: 12345678,
|
||||||
height: 5000,
|
height: 5000,
|
||||||
}),
|
}),
|
||||||
|
verified: true,
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@@ -269,7 +249,7 @@ mod test {
|
|||||||
get_test_db(),
|
get_test_db(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||||
|
|
||||||
assert_eq!(export.descriptor(), descriptor);
|
assert_eq!(export.descriptor(), descriptor);
|
||||||
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
||||||
@@ -287,7 +267,7 @@ mod test {
|
|||||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||||
|
|
||||||
let wallet = Wallet::new(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
|
let wallet = Wallet::new(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
|
||||||
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -306,7 +286,7 @@ mod test {
|
|||||||
get_test_db(),
|
get_test_db(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -329,7 +309,7 @@ mod test {
|
|||||||
get_test_db(),
|
get_test_db(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||||
|
|
||||||
assert_eq!(export.descriptor(), descriptor);
|
assert_eq!(export.descriptor(), descriptor);
|
||||||
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
||||||
@@ -349,7 +329,7 @@ mod test {
|
|||||||
get_test_db(),
|
get_test_db(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||||
|
|
||||||
assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
|
assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
|
||||||
}
|
}
|
||||||
@@ -360,7 +340,7 @@ mod test {
|
|||||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
||||||
|
|
||||||
let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}";
|
let import_str = "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}";
|
||||||
let export = FullyNodedExport::from_str(import_str).unwrap();
|
let export = WalletExport::from_str(import_str).unwrap();
|
||||||
|
|
||||||
assert_eq!(export.descriptor(), descriptor);
|
assert_eq!(export.descriptor(), descriptor);
|
||||||
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
assert_eq!(export.change_descriptor(), Some(change_descriptor.into()));
|
||||||
|
|||||||
@@ -140,8 +140,6 @@ pub struct AddressInfo {
|
|||||||
pub index: u32,
|
pub index: u32,
|
||||||
/// Address
|
/// Address
|
||||||
pub address: Address,
|
pub address: Address,
|
||||||
/// Type of keychain
|
|
||||||
pub keychain: KeychainKind,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Deref for AddressInfo {
|
impl Deref for AddressInfo {
|
||||||
@@ -159,29 +157,18 @@ impl fmt::Display for AddressInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
/// Options to a [`sync`].
|
/// Options to a [`Wallet::sync`]
|
||||||
///
|
|
||||||
/// [`sync`]: Wallet::sync
|
|
||||||
pub struct SyncOptions {
|
pub struct SyncOptions {
|
||||||
/// The progress tracker which may be informed when progress is made.
|
/// The progress tracker which may be informated when progress is made.
|
||||||
pub progress: Option<Box<dyn Progress>>,
|
pub progress: Option<Box<dyn Progress>>,
|
||||||
|
/// The maximum number of addresses sync on.
|
||||||
|
pub max_addresses: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<D> Wallet<D>
|
impl<D> Wallet<D>
|
||||||
where
|
where
|
||||||
D: BatchDatabase,
|
D: BatchDatabase,
|
||||||
{
|
{
|
||||||
#[deprecated = "Just use Wallet::new -- all wallets are offline now!"]
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a wallet.
|
/// Create a wallet.
|
||||||
///
|
///
|
||||||
/// The only way this can fail is if the descriptors passed in do not match the checksums in `database`.
|
/// The only way this can fail is if the descriptors passed in do not match the checksums in `database`.
|
||||||
@@ -235,12 +222,12 @@ where
|
|||||||
self.network
|
self.network
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return a newly derived address for the specified `keychain`.
|
// Return a newly derived address using the external descriptor
|
||||||
fn get_new_address(&self, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
fn get_new_address(&self) -> Result<AddressInfo, Error> {
|
||||||
let incremented_index = self.fetch_and_increment_index(keychain)?;
|
let incremented_index = self.fetch_and_increment_index(KeychainKind::External)?;
|
||||||
|
|
||||||
let address_result = self
|
let address_result = self
|
||||||
.get_descriptor_for_keychain(keychain)
|
.descriptor
|
||||||
.as_derived(incremented_index, &self.secp)
|
.as_derived(incremented_index, &self.secp)
|
||||||
.address(self.network);
|
.address(self.network);
|
||||||
|
|
||||||
@@ -248,19 +235,16 @@ where
|
|||||||
.map(|address| AddressInfo {
|
.map(|address| AddressInfo {
|
||||||
address,
|
address,
|
||||||
index: incremented_index,
|
index: incremented_index,
|
||||||
keychain,
|
|
||||||
})
|
})
|
||||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the the last previously derived address for `keychain` if it has not been used in a
|
// Return the the last previously derived address if it has not been used in a received
|
||||||
// received transaction. Otherwise return a new address using [`Wallet::get_new_address`].
|
// transaction. Otherwise return a new address using [`Wallet::get_new_address`].
|
||||||
fn get_unused_address(&self, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
fn get_unused_address(&self) -> Result<AddressInfo, Error> {
|
||||||
let current_index = self.fetch_index(keychain)?;
|
let current_index = self.fetch_index(KeychainKind::External)?;
|
||||||
|
|
||||||
let derived_key = self
|
let derived_key = self.descriptor.as_derived(current_index, &self.secp);
|
||||||
.get_descriptor_for_keychain(keychain)
|
|
||||||
.as_derived(current_index, &self.secp);
|
|
||||||
|
|
||||||
let script_pubkey = derived_key.script_pubkey();
|
let script_pubkey = derived_key.script_pubkey();
|
||||||
|
|
||||||
@@ -272,45 +256,36 @@ where
|
|||||||
.any(|o| o.script_pubkey == script_pubkey);
|
.any(|o| o.script_pubkey == script_pubkey);
|
||||||
|
|
||||||
if found_used {
|
if found_used {
|
||||||
self.get_new_address(keychain)
|
self.get_new_address()
|
||||||
} else {
|
} else {
|
||||||
derived_key
|
derived_key
|
||||||
.address(self.network)
|
.address(self.network)
|
||||||
.map(|address| AddressInfo {
|
.map(|address| AddressInfo {
|
||||||
address,
|
address,
|
||||||
index: current_index,
|
index: current_index,
|
||||||
keychain,
|
|
||||||
})
|
})
|
||||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return derived address for the descriptor of given [`KeychainKind`] at a specific index
|
// Return derived address for the external descriptor at a specific index
|
||||||
fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
fn peek_address(&self, index: u32) -> Result<AddressInfo, Error> {
|
||||||
self.get_descriptor_for_keychain(keychain)
|
self.descriptor
|
||||||
.as_derived(index, &self.secp)
|
.as_derived(index, &self.secp)
|
||||||
.address(self.network)
|
.address(self.network)
|
||||||
.map(|address| AddressInfo {
|
.map(|address| AddressInfo { index, address })
|
||||||
index,
|
|
||||||
address,
|
|
||||||
keychain,
|
|
||||||
})
|
|
||||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return derived address for `keychain` at a specific index and reset current
|
// Return derived address for the external descriptor at a specific index and reset current
|
||||||
// address index
|
// address index
|
||||||
fn reset_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
fn reset_address(&self, index: u32) -> Result<AddressInfo, Error> {
|
||||||
self.set_index(keychain, index)?;
|
self.set_index(KeychainKind::External, index)?;
|
||||||
|
|
||||||
self.get_descriptor_for_keychain(keychain)
|
self.descriptor
|
||||||
.as_derived(index, &self.secp)
|
.as_derived(index, &self.secp)
|
||||||
.address(self.network)
|
.address(self.network)
|
||||||
.map(|address| AddressInfo {
|
.map(|address| AddressInfo { index, address })
|
||||||
index,
|
|
||||||
address,
|
|
||||||
keychain,
|
|
||||||
})
|
|
||||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,77 +293,14 @@ where
|
|||||||
/// available address index selection strategies. If none of the keys in the descriptor are derivable
|
/// available address index selection strategies. If none of the keys in the descriptor are derivable
|
||||||
/// (ie. does not end with /*) then the same address will always be returned for any [`AddressIndex`].
|
/// (ie. does not end with /*) then the same address will always be returned for any [`AddressIndex`].
|
||||||
pub fn get_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
|
pub fn get_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
|
||||||
self._get_address(address_index, KeychainKind::External)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a derived address using the internal (change) descriptor.
|
|
||||||
///
|
|
||||||
/// If the wallet doesn't have an internal descriptor it will use the external descriptor.
|
|
||||||
///
|
|
||||||
/// see [`AddressIndex`] for available address index selection strategies. If none of the keys
|
|
||||||
/// in the descriptor are derivable (ie. does not end with /*) then the same address will always
|
|
||||||
/// be returned for any [`AddressIndex`].
|
|
||||||
pub fn get_internal_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
|
|
||||||
self._get_address(address_index, KeychainKind::Internal)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn _get_address(
|
|
||||||
&self,
|
|
||||||
address_index: AddressIndex,
|
|
||||||
keychain: KeychainKind,
|
|
||||||
) -> Result<AddressInfo, Error> {
|
|
||||||
match address_index {
|
match address_index {
|
||||||
AddressIndex::New => self.get_new_address(keychain),
|
AddressIndex::New => self.get_new_address(),
|
||||||
AddressIndex::LastUnused => self.get_unused_address(keychain),
|
AddressIndex::LastUnused => self.get_unused_address(),
|
||||||
AddressIndex::Peek(index) => self.peek_address(index, keychain),
|
AddressIndex::Peek(index) => self.peek_address(index),
|
||||||
AddressIndex::Reset(index) => self.reset_address(index, keychain),
|
AddressIndex::Reset(index) => self.reset_address(index),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensures that there are at least `max_addresses` addresses cached in the database if the
|
|
||||||
/// descriptor is derivable, or 1 address if it is not.
|
|
||||||
/// Will return `Ok(true)` if there are new addresses generated (either external or internal),
|
|
||||||
/// and `Ok(false)` if all the required addresses are already cached. This function is useful to
|
|
||||||
/// explicitly cache addresses in a wallet to do things like check [`Wallet::is_mine`] on
|
|
||||||
/// transaction output scripts.
|
|
||||||
pub fn ensure_addresses_cached(&self, max_addresses: u32) -> Result<bool, Error> {
|
|
||||||
let mut new_addresses_cached = false;
|
|
||||||
let max_address = match self.descriptor.is_deriveable() {
|
|
||||||
false => 0,
|
|
||||||
true => max_addresses,
|
|
||||||
};
|
|
||||||
debug!("max_address {}", max_address);
|
|
||||||
if self
|
|
||||||
.database
|
|
||||||
.borrow()
|
|
||||||
.get_script_pubkey_from_path(KeychainKind::External, max_address.saturating_sub(1))?
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
debug!("caching external addresses");
|
|
||||||
new_addresses_cached = true;
|
|
||||||
self.cache_addresses(KeychainKind::External, 0, max_address)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(change_descriptor) = &self.change_descriptor {
|
|
||||||
let max_address = match change_descriptor.is_deriveable() {
|
|
||||||
false => 0,
|
|
||||||
true => max_addresses,
|
|
||||||
};
|
|
||||||
|
|
||||||
if self
|
|
||||||
.database
|
|
||||||
.borrow()
|
|
||||||
.get_script_pubkey_from_path(KeychainKind::Internal, max_address.saturating_sub(1))?
|
|
||||||
.is_none()
|
|
||||||
{
|
|
||||||
debug!("caching internal addresses");
|
|
||||||
new_addresses_cached = true;
|
|
||||||
self.cache_addresses(KeychainKind::Internal, 0, max_address)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(new_addresses_cached)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return whether or not a `script` is part of this wallet (either internal or external)
|
/// Return whether or not a `script` is part of this wallet (either internal or external)
|
||||||
pub fn is_mine(&self, script: &Script) -> Result<bool, Error> {
|
pub fn is_mine(&self, script: &Script) -> Result<bool, Error> {
|
||||||
self.database.borrow().is_mine(script)
|
self.database.borrow().is_mine(script)
|
||||||
@@ -399,13 +311,7 @@ where
|
|||||||
/// Note that this method only operates on the internal database, which first needs to be
|
/// Note that this method only operates on the internal database, which first needs to be
|
||||||
/// [`Wallet::sync`] manually.
|
/// [`Wallet::sync`] manually.
|
||||||
pub fn list_unspent(&self) -> Result<Vec<LocalUtxo>, Error> {
|
pub fn list_unspent(&self) -> Result<Vec<LocalUtxo>, Error> {
|
||||||
Ok(self
|
self.database.borrow().iter_utxos()
|
||||||
.database
|
|
||||||
.borrow()
|
|
||||||
.iter_utxos()?
|
|
||||||
.into_iter()
|
|
||||||
.filter(|l| !l.is_spent)
|
|
||||||
.collect())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the `UTXO` owned by this wallet corresponding to `outpoint` if it exists in the
|
/// Returns the `UTXO` owned by this wallet corresponding to `outpoint` if it exists in the
|
||||||
@@ -468,29 +374,6 @@ where
|
|||||||
signers.add_external(signer.id(&self.secp), ordering, signer);
|
signers.add_external(signer.id(&self.secp), ordering, signer);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the signers
|
|
||||||
///
|
|
||||||
/// ## Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// # use bdk::{Wallet, KeychainKind};
|
|
||||||
/// # use bdk::bitcoin::Network;
|
|
||||||
/// # use bdk::database::MemoryDatabase;
|
|
||||||
/// let wallet = Wallet::new("wpkh(tprv8ZgxMBicQKsPe73PBRSmNbTfbcsZnwWhz5eVmhHpi31HW29Z7mc9B4cWGRQzopNUzZUT391DeDJxL2PefNunWyLgqCKRMDkU1s2s8bAfoSk/84'/0'/0'/0/*)", None, Network::Testnet, MemoryDatabase::new())?;
|
|
||||||
/// 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);
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// Ok::<(), Box<dyn std::error::Error>>(())
|
|
||||||
/// ```
|
|
||||||
pub fn get_signers(&self, keychain: KeychainKind) -> Arc<SignersContainer> {
|
|
||||||
match keychain {
|
|
||||||
KeychainKind::External => Arc::clone(&self.signers),
|
|
||||||
KeychainKind::Internal => Arc::clone(&self.change_signers),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add an address validator
|
/// Add an address validator
|
||||||
///
|
///
|
||||||
/// See [the `address_validator` module](address_validator) for an example.
|
/// See [the `address_validator` module](address_validator) for an example.
|
||||||
@@ -498,11 +381,6 @@ where
|
|||||||
self.address_validators.push(validator);
|
self.address_validators.push(validator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the address validators
|
|
||||||
pub fn get_address_validators(&self) -> &[Arc<dyn AddressValidator>] {
|
|
||||||
&self.address_validators
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start building a transaction.
|
/// Start building a transaction.
|
||||||
///
|
///
|
||||||
/// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction.
|
/// This returns a blank [`TxBuilder`] from which you can specify the parameters for the transaction.
|
||||||
@@ -769,10 +647,7 @@ where
|
|||||||
let mut drain_output = {
|
let mut drain_output = {
|
||||||
let script_pubkey = match params.drain_to {
|
let script_pubkey = match params.drain_to {
|
||||||
Some(ref drain_recipient) => drain_recipient.clone(),
|
Some(ref drain_recipient) => drain_recipient.clone(),
|
||||||
None => self
|
None => self.get_change_address()?,
|
||||||
.get_internal_address(AddressIndex::New)?
|
|
||||||
.address
|
|
||||||
.script_pubkey(),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
TxOut {
|
TxOut {
|
||||||
@@ -822,6 +697,7 @@ where
|
|||||||
received,
|
received,
|
||||||
sent,
|
sent,
|
||||||
fee: Some(fee_amount),
|
fee: Some(fee_amount),
|
||||||
|
verified: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((psbt, transaction_details))
|
Ok((psbt, transaction_details))
|
||||||
@@ -925,7 +801,6 @@ where
|
|||||||
outpoint: txin.previous_output,
|
outpoint: txin.previous_output,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
is_spent: true,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(WeightedUtxo {
|
Ok(WeightedUtxo {
|
||||||
@@ -1206,6 +1081,13 @@ where
|
|||||||
.map(|(desc, child)| desc.as_derived(child, &self.secp)))
|
.map(|(desc, child)| desc.as_derived(child, &self.secp)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_change_address(&self) -> Result<Script, Error> {
|
||||||
|
let (desc, keychain) = self._get_descriptor_for_keychain(KeychainKind::Internal);
|
||||||
|
let index = self.fetch_and_increment_index(keychain)?;
|
||||||
|
|
||||||
|
Ok(desc.as_derived(index, &self.secp).script_pubkey())
|
||||||
|
}
|
||||||
|
|
||||||
fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
|
fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||||
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
||||||
let index = match descriptor.is_deriveable() {
|
let index = match descriptor.is_deriveable() {
|
||||||
@@ -1569,10 +1451,46 @@ where
|
|||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
debug!("Begin sync...");
|
debug!("Begin sync...");
|
||||||
|
|
||||||
let SyncOptions { progress } = sync_opts;
|
let mut run_setup = false;
|
||||||
|
let SyncOptions {
|
||||||
|
max_addresses,
|
||||||
|
progress,
|
||||||
|
} = sync_opts;
|
||||||
let progress = progress.unwrap_or_else(|| Box::new(NoopProgress));
|
let progress = progress.unwrap_or_else(|| Box::new(NoopProgress));
|
||||||
|
|
||||||
let run_setup = self.ensure_addresses_cached(CACHE_ADDR_BATCH_SIZE)?;
|
let max_address = match self.descriptor.is_deriveable() {
|
||||||
|
false => 0,
|
||||||
|
true => max_addresses.unwrap_or(CACHE_ADDR_BATCH_SIZE),
|
||||||
|
};
|
||||||
|
debug!("max_address {}", max_address);
|
||||||
|
if self
|
||||||
|
.database
|
||||||
|
.borrow()
|
||||||
|
.get_script_pubkey_from_path(KeychainKind::External, max_address.saturating_sub(1))?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
debug!("caching external addresses");
|
||||||
|
run_setup = true;
|
||||||
|
self.cache_addresses(KeychainKind::External, 0, max_address)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(change_descriptor) = &self.change_descriptor {
|
||||||
|
let max_address = match change_descriptor.is_deriveable() {
|
||||||
|
false => 0,
|
||||||
|
true => max_addresses.unwrap_or(CACHE_ADDR_BATCH_SIZE),
|
||||||
|
};
|
||||||
|
|
||||||
|
if self
|
||||||
|
.database
|
||||||
|
.borrow()
|
||||||
|
.get_script_pubkey_from_path(KeychainKind::Internal, max_address.saturating_sub(1))?
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
debug!("caching internal addresses");
|
||||||
|
run_setup = true;
|
||||||
|
self.cache_addresses(KeychainKind::Internal, 0, max_address)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
debug!("run_setup: {}", run_setup);
|
debug!("run_setup: {}", run_setup);
|
||||||
// TODO: what if i generate an address first and cache some addresses?
|
// TODO: what if i generate an address first and cache some addresses?
|
||||||
@@ -1585,6 +1503,23 @@ where
|
|||||||
maybe_await!(blockchain.wallet_sync(self.database.borrow_mut().deref_mut(), progress,))?;
|
maybe_await!(blockchain.wallet_sync(self.database.borrow_mut().deref_mut(), progress,))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "verify")]
|
||||||
|
{
|
||||||
|
debug!("Verifying transactions...");
|
||||||
|
for mut tx in self.database.borrow().iter_txs(true)? {
|
||||||
|
if !tx.verified {
|
||||||
|
verify::verify_tx(
|
||||||
|
tx.transaction.as_ref().ok_or(Error::TransactionNotFound)?,
|
||||||
|
self.database.borrow().deref(),
|
||||||
|
blockchain,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
tx.verified = true;
|
||||||
|
self.database.borrow_mut().set_tx(&tx)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let sync_time = SyncTime {
|
let sync_time = SyncTime {
|
||||||
block_time: BlockTime {
|
block_time: BlockTime {
|
||||||
height: maybe_await!(blockchain.get_height())?,
|
height: maybe_await!(blockchain.get_height())?,
|
||||||
@@ -1596,18 +1531,6 @@ where
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the checksum of the public descriptor associated to `keychain`
|
|
||||||
///
|
|
||||||
/// Internally calls [`Self::get_descriptor_for_keychain`] to fetch the right descriptor
|
|
||||||
pub fn descriptor_checksum(&self, keychain: KeychainKind) -> String {
|
|
||||||
self.get_descriptor_for_keychain(keychain)
|
|
||||||
.to_string()
|
|
||||||
.splitn(2, '#')
|
|
||||||
.next()
|
|
||||||
.unwrap()
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a fake wallet that appears to be funded for testing.
|
/// Return a fake wallet that appears to be funded for testing.
|
||||||
@@ -3976,7 +3899,6 @@ pub(crate) mod test {
|
|||||||
AddressInfo {
|
AddressInfo {
|
||||||
index: 0,
|
index: 0,
|
||||||
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a").unwrap(),
|
address: Address::from_str("tb1q6yn66vajcctph75pvylgkksgpp6nq04ppwct9a").unwrap(),
|
||||||
keychain: KeychainKind::External,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3985,8 +3907,7 @@ pub(crate) mod test {
|
|||||||
wallet.get_address(New).unwrap(),
|
wallet.get_address(New).unwrap(),
|
||||||
AddressInfo {
|
AddressInfo {
|
||||||
index: 1,
|
index: 1,
|
||||||
address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7").unwrap(),
|
address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7").unwrap()
|
||||||
keychain: KeychainKind::External,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3995,8 +3916,7 @@ pub(crate) mod test {
|
|||||||
wallet.get_address(Peek(25)).unwrap(),
|
wallet.get_address(Peek(25)).unwrap(),
|
||||||
AddressInfo {
|
AddressInfo {
|
||||||
index: 25,
|
index: 25,
|
||||||
address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2").unwrap(),
|
address: Address::from_str("tb1qsp7qu0knx3sl6536dzs0703u2w2ag6ppl9d0c2").unwrap()
|
||||||
keychain: KeychainKind::External,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4005,8 +3925,7 @@ pub(crate) mod test {
|
|||||||
wallet.get_address(New).unwrap(),
|
wallet.get_address(New).unwrap(),
|
||||||
AddressInfo {
|
AddressInfo {
|
||||||
index: 2,
|
index: 2,
|
||||||
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2").unwrap(),
|
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2").unwrap()
|
||||||
keychain: KeychainKind::External,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4015,8 +3934,7 @@ pub(crate) mod test {
|
|||||||
wallet.get_address(Reset(1)).unwrap(),
|
wallet.get_address(Reset(1)).unwrap(),
|
||||||
AddressInfo {
|
AddressInfo {
|
||||||
index: 1,
|
index: 1,
|
||||||
address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7").unwrap(),
|
address: Address::from_str("tb1q4er7kxx6sssz3q7qp7zsqsdx4erceahhax77d7").unwrap()
|
||||||
keychain: KeychainKind::External,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -4025,8 +3943,7 @@ pub(crate) mod test {
|
|||||||
wallet.get_address(New).unwrap(),
|
wallet.get_address(New).unwrap(),
|
||||||
AddressInfo {
|
AddressInfo {
|
||||||
index: 2,
|
index: 2,
|
||||||
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2").unwrap(),
|
address: Address::from_str("tb1qzntf2mqex4ehwkjlfdyy3ewdlk08qkvkvrz7x2").unwrap()
|
||||||
keychain: KeychainKind::External,
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4041,55 +3958,6 @@ pub(crate) mod test {
|
|||||||
builder.add_recipient(addr.script_pubkey(), 45_000);
|
builder.add_recipient(addr.script_pubkey(), 45_000);
|
||||||
builder.finish().unwrap();
|
builder.finish().unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_get_address() {
|
|
||||||
use crate::descriptor::template::Bip84;
|
|
||||||
let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
|
||||||
let wallet = Wallet::new(
|
|
||||||
Bip84(key, KeychainKind::External),
|
|
||||||
Some(Bip84(key, KeychainKind::Internal)),
|
|
||||||
Network::Regtest,
|
|
||||||
MemoryDatabase::default(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
wallet.get_address(AddressIndex::New).unwrap(),
|
|
||||||
AddressInfo {
|
|
||||||
index: 0,
|
|
||||||
address: Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap(),
|
|
||||||
keychain: KeychainKind::External,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
wallet.get_internal_address(AddressIndex::New).unwrap(),
|
|
||||||
AddressInfo {
|
|
||||||
index: 0,
|
|
||||||
address: Address::from_str("bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa").unwrap(),
|
|
||||||
keychain: KeychainKind::Internal,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let wallet = Wallet::new(
|
|
||||||
Bip84(key, KeychainKind::External),
|
|
||||||
None,
|
|
||||||
Network::Regtest,
|
|
||||||
MemoryDatabase::default(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
wallet.get_internal_address(AddressIndex::New).unwrap(),
|
|
||||||
AddressInfo {
|
|
||||||
index: 0,
|
|
||||||
address: Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap(),
|
|
||||||
keychain: KeychainKind::Internal,
|
|
||||||
},
|
|
||||||
"when there's no internal descriptor it should just use external"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deterministically generate a unique name given the descriptors defining the wallet
|
/// Deterministically generate a unique name given the descriptors defining the wallet
|
||||||
|
|||||||
@@ -838,7 +838,6 @@ mod test {
|
|||||||
},
|
},
|
||||||
txout: Default::default(),
|
txout: Default::default(),
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
is_spent: false,
|
|
||||||
},
|
},
|
||||||
LocalUtxo {
|
LocalUtxo {
|
||||||
outpoint: OutPoint {
|
outpoint: OutPoint {
|
||||||
@@ -847,7 +846,6 @@ mod test {
|
|||||||
},
|
},
|
||||||
txout: Default::default(),
|
txout: Default::default(),
|
||||||
keychain: KeychainKind::Internal,
|
keychain: KeychainKind::Internal,
|
||||||
is_spent: false,
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ use std::fmt;
|
|||||||
use bitcoin::consensus::serialize;
|
use bitcoin::consensus::serialize;
|
||||||
use bitcoin::{OutPoint, Transaction, Txid};
|
use bitcoin::{OutPoint, Transaction, Txid};
|
||||||
|
|
||||||
use crate::blockchain::GetTx;
|
use crate::blockchain::Blockchain;
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
|
|
||||||
@@ -29,7 +29,7 @@ use crate::error::Error;
|
|||||||
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
|
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
|
||||||
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
|
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
|
||||||
/// with unconfirmed transactions that have been evicted from the backend's memory.
|
/// with unconfirmed transactions that have been evicted from the backend's memory.
|
||||||
pub fn verify_tx<D: Database, B: GetTx>(
|
pub fn verify_tx<D: Database, B: Blockchain>(
|
||||||
tx: &Transaction,
|
tx: &Transaction,
|
||||||
database: &D,
|
database: &D,
|
||||||
blockchain: &B,
|
blockchain: &B,
|
||||||
@@ -104,18 +104,43 @@ impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use std::collections::HashSet;
|
||||||
use crate::database::{BatchOperations, MemoryDatabase};
|
|
||||||
use bitcoin::consensus::encode::deserialize;
|
use bitcoin::consensus::encode::deserialize;
|
||||||
use bitcoin::hashes::hex::FromHex;
|
use bitcoin::hashes::hex::FromHex;
|
||||||
use bitcoin::{Transaction, Txid};
|
use bitcoin::{Transaction, Txid};
|
||||||
|
|
||||||
|
use crate::blockchain::{Blockchain, Capability, Progress};
|
||||||
|
use crate::database::{BatchDatabase, BatchOperations, MemoryDatabase};
|
||||||
|
use crate::FeeRate;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
struct DummyBlockchain;
|
struct DummyBlockchain;
|
||||||
|
|
||||||
impl GetTx for DummyBlockchain {
|
impl Blockchain for DummyBlockchain {
|
||||||
|
fn get_capabilities(&self) -> HashSet<Capability> {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
fn setup<D: BatchDatabase, P: 'static + Progress>(
|
||||||
|
&self,
|
||||||
|
_database: &mut D,
|
||||||
|
_progress_update: P,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
fn get_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> {
|
fn get_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
fn broadcast(&self, _tx: &Transaction) -> Result<(), Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
fn get_height(&self) -> Result<u32, Error> {
|
||||||
|
Ok(42)
|
||||||
|
}
|
||||||
|
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
|
||||||
|
Ok(FeeRate::default_min_relay_fee())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user