Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e3ce50059f | ||
|
|
8a2a6bbcee | ||
|
|
122e6e7140 | ||
|
|
1018bb2b17 | ||
|
|
9ed36875f1 | ||
|
|
502882d27c | ||
|
|
a328607d27 | ||
|
|
f90e3f978e | ||
|
|
68e1b32d81 | ||
|
|
44758f9483 | ||
|
|
c350064dae | ||
|
|
92746440db | ||
|
|
e4eb95fb9c | ||
|
|
c307bacb9c | ||
|
|
a111d25476 | ||
|
|
0621ca89d5 | ||
|
|
adef166b22 | ||
|
|
213f18f7b7 | ||
|
|
8cd055090d | ||
|
|
1b9014846c | ||
|
|
9c0141b5e3 | ||
|
|
2698fc0219 | ||
|
|
bac15bb207 | ||
|
|
06b80fdb15 | ||
|
|
ff6db18726 | ||
|
|
86abd8698f | ||
|
|
0d9c2f76e0 | ||
|
|
63d5bcee93 | ||
|
|
8a98e69e78 | ||
|
|
c6eeb7b989 | ||
|
|
3334c8da07 | ||
|
|
ce09203431 | ||
|
|
4b1be68965 | ||
|
|
1e9a684b54 | ||
|
|
52bc63e48f | ||
|
|
9a6db15d26 | ||
|
|
1803f5ea8a | ||
|
|
f2f0efc0b3 | ||
|
|
364ad95e85 |
3
.github/workflows/cont_integration.yml
vendored
3
.github/workflows/cont_integration.yml
vendored
@@ -28,6 +28,7 @@ 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
|
||||||
@@ -114,7 +115,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
|
||||||
|
|||||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -6,12 +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]
|
## [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.
|
- 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`.
|
- `verify` flag removed from `TransactionDetails`.
|
||||||
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
|
- 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
|
- 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
|
||||||
|
|
||||||
@@ -436,4 +449,5 @@ final transaction is created by calling `finish` on the builder.
|
|||||||
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.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.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.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
|
||||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD
|
[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
|
||||||
|
|||||||
12
Cargo.toml
12
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
version = "0.17.0"
|
version = "0.18.0"
|
||||||
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.0", features = ["use-serde"] }
|
miniscript = { version = "^6.1", 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" }
|
||||||
@@ -55,6 +55,7 @@ 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"]
|
||||||
@@ -96,7 +97,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.13", features = ["trigger", "bitcoind_22_0"] }
|
electrsd = { version= "0.15", features = ["trigger", "bitcoind_22_0"] }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "address_validator"
|
name = "address_validator"
|
||||||
@@ -109,6 +110,11 @@ 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]
|
||||||
|
|||||||
229
examples/rpcwallet.rs
Normal file
229
examples/rpcwallet.rs
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
// 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))
|
||||||
|
}
|
||||||
@@ -163,11 +163,19 @@ 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;
|
||||||
|
|
||||||
if database.is_mine(&previous_output.script_pubkey)? {
|
// this output is ours, we have a path to derive it
|
||||||
|
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, removing from utxo", tx.txid(), i);
|
debug!("{} input #{} is mine, setting utxo as spent", tx.txid(), i);
|
||||||
updates.del_utxo(&input.previous_output)?;
|
updates.set_utxo(&LocalUtxo {
|
||||||
|
outpoint: input.previous_output,
|
||||||
|
txout: previous_output.clone(),
|
||||||
|
keychain,
|
||||||
|
is_spent: true,
|
||||||
|
})?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,6 +193,7 @@ 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;
|
||||||
|
|
||||||
|
|||||||
@@ -127,10 +127,11 @@ impl WalletSync for EsploraBlockchain {
|
|||||||
.take(self.concurrency as usize)
|
.take(self.concurrency as usize)
|
||||||
.cloned();
|
.cloned();
|
||||||
|
|
||||||
let handles = scripts.map(move |script| {
|
let mut handles = vec![];
|
||||||
|
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.
|
||||||
std::thread::spawn(move || {
|
handles.push(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 =
|
||||||
@@ -152,10 +153,11 @@ 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![];
|
||||||
|
|||||||
@@ -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 tx we want to
|
// list_txs returns all conflicting txs, 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()
|
||||||
}) {
|
}) {
|
||||||
@@ -332,20 +332,23 @@ impl WalletSync for RpcBlockchain {
|
|||||||
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<HashSet<_>, Error>>()?;
|
.collect::<Result<HashSet<_>, Error>>()?;
|
||||||
|
|
||||||
let spent: HashSet<_> = known_utxos.difference(¤t_utxos).collect();
|
let spent: HashSet<_> = known_utxos.difference(¤t_utxos).collect();
|
||||||
for s in spent {
|
for utxo in spent {
|
||||||
debug!("removing utxo: {:?}", s);
|
debug!("setting as spent utxo: {:?}", utxo);
|
||||||
db.del_utxo(&s.outpoint)?;
|
let mut spent_utxo = utxo.clone();
|
||||||
|
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 s in received {
|
for utxo in received {
|
||||||
debug!("adding utxo: {:?}", s);
|
debug!("adding utxo: {:?}", utxo);
|
||||||
db.set_utxo(s)?;
|
db.set_utxo(utxo)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (keykind, index) in indexes {
|
for (keykind, index) in indexes {
|
||||||
|
|||||||
@@ -332,7 +332,23 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
|||||||
batch.del_tx(txid, true)?;
|
batch.del_tx(txid, true)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set every tx we observed
|
let mut spent_utxos = HashSet::new();
|
||||||
|
|
||||||
|
// 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
|
||||||
@@ -343,30 +359,22 @@ 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)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// we don't do this in the loop above since we may want to delete some of the utxos we
|
batch.set_tx(finished_tx)?;
|
||||||
// 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 {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ 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,6 +43,7 @@ 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)*;
|
||||||
|
|
||||||
@@ -125,8 +126,9 @@ 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 }))
|
Ok(Some(LocalUtxo { outpoint: outpoint.clone(), txout, keychain, is_spent, }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,11 +248,16 @@ 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()
|
||||||
@@ -314,11 +321,16 @@ 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,8 +150,10 @@ 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
|
self.map.insert(
|
||||||
.insert(key, Box::new((utxo.txout.clone(), utxo.keychain)));
|
key,
|
||||||
|
Box::new((utxo.txout.clone(), utxo.keychain, utxo.is_spent)),
|
||||||
|
);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -228,11 +230,12 @@ impl BatchOperations for MemoryDatabase {
|
|||||||
match res {
|
match res {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
Some(b) => {
|
Some(b) => {
|
||||||
let (txout, keychain) = b.downcast_ref().cloned().unwrap();
|
let (txout, keychain, is_spent) = b.downcast_ref().cloned().unwrap();
|
||||||
Ok(Some(LocalUtxo {
|
Ok(Some(LocalUtxo {
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
|
is_spent,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,11 +329,12 @@ 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) = v.downcast_ref().cloned().unwrap();
|
let (txout, keychain, is_spent) = v.downcast_ref().cloned().unwrap();
|
||||||
Ok(LocalUtxo {
|
Ok(LocalUtxo {
|
||||||
outpoint,
|
outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
|
is_spent,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
@@ -389,11 +393,12 @@ 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) = b.downcast_ref().cloned().unwrap();
|
let (txout, keychain, is_spent) = b.downcast_ref().cloned().unwrap();
|
||||||
LocalUtxo {
|
LocalUtxo {
|
||||||
outpoint: *outpoint,
|
outpoint: *outpoint,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
|
is_spent,
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
@@ -526,6 +531,7 @@ 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,6 +159,10 @@ 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>;
|
||||||
}
|
}
|
||||||
@@ -316,6 +320,7 @@ 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();
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ static MIGRATIONS: &[&str] = &[
|
|||||||
"CREATE TABLE transaction_details (txid BLOB, timestamp INTEGER, received INTEGER, sent INTEGER, fee INTEGER, height INTEGER);",
|
"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;",
|
"INSERT INTO transaction_details SELECT txid, timestamp, received, sent, fee, height FROM transaction_details_old;",
|
||||||
"DROP TABLE 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
|
||||||
@@ -83,14 +84,16 @@ 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) VALUES (:value, :keychain, :vout, :txid, :script)")?;
|
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)")?;
|
||||||
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())
|
||||||
@@ -291,7 +294,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 FROM utxos")?;
|
.prepare_cached("SELECT value, keychain, vout, txid, script, is_spent 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()? {
|
||||||
@@ -300,6 +303,7 @@ 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)?;
|
||||||
|
|
||||||
@@ -310,19 +314,16 @@ impl SqliteDatabase {
|
|||||||
script_pubkey: script.into(),
|
script_pubkey: script.into(),
|
||||||
},
|
},
|
||||||
keychain,
|
keychain,
|
||||||
|
is_spent,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(utxos)
|
Ok(utxos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select_utxo_by_outpoint(
|
fn select_utxo_by_outpoint(&self, txid: &[u8], vout: u32) -> Result<Option<LocalUtxo>, Error> {
|
||||||
&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 FROM utxos WHERE txid=:txid AND vout=:vout",
|
"SELECT value, keychain, script, is_spent 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()? {
|
||||||
@@ -331,9 +332,18 @@ 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: Script = script.into();
|
let script_pubkey: Script = script.into();
|
||||||
|
let is_spent: bool = row.get(3)?;
|
||||||
|
|
||||||
Ok(Some((value, keychain, script)))
|
Ok(Some(LocalUtxo {
|
||||||
|
outpoint: OutPoint::new(deserialize(txid)?, vout),
|
||||||
|
txout: TxOut {
|
||||||
|
value,
|
||||||
|
script_pubkey,
|
||||||
|
},
|
||||||
|
keychain,
|
||||||
|
is_spent,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
@@ -620,6 +630,7 @@ 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(())
|
||||||
}
|
}
|
||||||
@@ -694,16 +705,9 @@ 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((value, keychain, script_pubkey)) => {
|
Some(local_utxo) => {
|
||||||
self.delete_utxo_by_outpoint(&outpoint.txid, outpoint.vout)?;
|
self.delete_utxo_by_outpoint(&outpoint.txid, outpoint.vout)?;
|
||||||
Ok(Some(LocalUtxo {
|
Ok(Some(local_utxo))
|
||||||
outpoint: *outpoint,
|
|
||||||
txout: TxOut {
|
|
||||||
value,
|
|
||||||
script_pubkey,
|
|
||||||
},
|
|
||||||
keychain,
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
}
|
}
|
||||||
@@ -832,17 +836,7 @@ impl Database for SqliteDatabase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
fn get_utxo(&self, outpoint: &OutPoint) -> Result<Option<LocalUtxo>, Error> {
|
||||||
match self.select_utxo_by_outpoint(&outpoint.txid, outpoint.vout)? {
|
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,6 +10,41 @@
|
|||||||
// 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;
|
||||||
@@ -19,10 +54,7 @@ use std::ops::Deref;
|
|||||||
use bitcoin::hashes::hash160;
|
use bitcoin::hashes::hash160;
|
||||||
use bitcoin::PublicKey;
|
use bitcoin::PublicKey;
|
||||||
|
|
||||||
pub use miniscript::{
|
use miniscript::{descriptor::Wildcard, Descriptor, DescriptorPublicKey};
|
||||||
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;
|
||||||
@@ -119,14 +151,19 @@ impl<'s> ToPublicKey for DerivedDescriptorKey<'s> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) trait AsDerived {
|
/// Utilities to derive descriptors
|
||||||
// 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(...),sdv:older(...)))`
|
/// They both produce the descriptor: `wsh(thresh(2,pk(...),s:pk(...),sndv: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:d:v:older(my_timelock))
|
/// thresh(2, pk(my_key_1), s:pk(my_key_2), s:n: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:d:v:older(my_timelock))?,
|
/// bdk::fragment!(s:n: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,d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
|
descriptor!(wsh(thresh(2,n:d:v:older(1),s:pk(private_key),s:pk(private_key)))).unwrap();
|
||||||
|
|
||||||
assert_eq!(descriptor.to_string(), "wsh(thresh(2,dv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#cfdcqs3s")
|
assert_eq!(descriptor.to_string(), "wsh(thresh(2,ndv:older(1),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c),s:pk(02e96fe52ef0e22d2f131dd425ce1893073a3c6ad20e8cac36726393dfb4856a4c)))#zzk3ux8g")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -21,16 +21,17 @@ use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerpr
|
|||||||
use bitcoin::util::psbt;
|
use bitcoin::util::psbt;
|
||||||
use bitcoin::{Network, PublicKey, Script, TxOut};
|
use bitcoin::{Network, PublicKey, Script, TxOut};
|
||||||
|
|
||||||
use miniscript::descriptor::{
|
use miniscript::descriptor::{DescriptorType, InnerXKey};
|
||||||
DescriptorPublicKey, DescriptorType, DescriptorXKey, InnerXKey, Wildcard,
|
pub use miniscript::{
|
||||||
|
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
||||||
|
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||||
};
|
};
|
||||||
pub use miniscript::{descriptor::KeyMap, Descriptor, 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(crate) mod derived;
|
pub mod derived;
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub mod dsl;
|
pub mod dsl;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
@@ -38,8 +39,7 @@ pub mod policy;
|
|||||||
pub mod template;
|
pub mod template;
|
||||||
|
|
||||||
pub use self::checksum::get_checksum;
|
pub use self::checksum::get_checksum;
|
||||||
use self::derived::AsDerived;
|
pub use self::derived::{AsDerived, DerivedDescriptorKey};
|
||||||
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;
|
||||||
|
|||||||
@@ -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,d:v:older(2),s:pk(prvkey_alice),s:pk(prvkey_bob)))).unwrap();
|
descriptor!(wsh(thresh(2,n: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!(
|
||||||
"tb1qhpemaacpeu8ajlnh8k9v55ftg0px58r8630fz8t5mypxcwdk5d8sum522g",
|
"tb1qsydsey4hexagwkvercqsmes6yet0ndkyt6uzcphtqnygjd8hmzmsfxrv58",
|
||||||
addr.to_string()
|
addr.to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,23 @@ 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,6 +548,16 @@ 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,14 +44,15 @@
|
|||||||
//! interact with the bitcoin P2P network.
|
//! interact with the bitcoin P2P network.
|
||||||
//!
|
//!
|
||||||
//! ```toml
|
//! ```toml
|
||||||
//! bdk = "0.17.0"
|
//! bdk = "0.18.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.
|
||||||
|
|||||||
@@ -320,7 +320,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())
|
.or(bitcoind::downloaded_exe_path().ok())
|
||||||
.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",
|
||||||
);
|
);
|
||||||
@@ -1125,6 +1125,47 @@ macro_rules! bdk_blockchain_tests {
|
|||||||
assert_eq!(tx_2.sent, 0);
|
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]
|
#[test]
|
||||||
fn test_send_receive_pkh() {
|
fn test_send_receive_pkh() {
|
||||||
let descriptors = ("pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None);
|
let descriptors = ("pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None);
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ 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`.
|
||||||
|
|||||||
@@ -569,6 +569,7 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
|
is_spent: false,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -596,6 +597,7 @@ mod test {
|
|||||||
script_pubkey: Script::new(),
|
script_pubkey: Script::new(),
|
||||||
},
|
},
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
|
is_spent: false,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -615,6 +617,7 @@ 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 = WalletExport::from_str(import)?;
|
//! let import = FullyNodedExport::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 = WalletExport::export_wallet(&wallet, "exported wallet", true)
|
//! let export = FullyNodedExport::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,16 +64,21 @@ 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, DescriptorPublicKey, ScriptContext, Terminal};
|
use miniscript::{Descriptor, 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 WalletExport {
|
pub struct FullyNodedExport {
|
||||||
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,
|
||||||
@@ -81,13 +86,13 @@ pub struct WalletExport {
|
|||||||
pub label: String,
|
pub label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for WalletExport {
|
impl ToString for FullyNodedExport {
|
||||||
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 WalletExport {
|
impl FromStr for FullyNodedExport {
|
||||||
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> {
|
||||||
@@ -99,7 +104,7 @@ fn remove_checksum(s: String) -> String {
|
|||||||
s.splitn(2, '#').next().map(String::from).unwrap()
|
s.splitn(2, '#').next().map(String::from).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WalletExport {
|
impl FullyNodedExport {
|
||||||
/// 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
|
||||||
@@ -117,8 +122,12 @@ impl WalletExport {
|
|||||||
include_blockheight: bool,
|
include_blockheight: bool,
|
||||||
) -> Result<Self, &'static str> {
|
) -> Result<Self, &'static str> {
|
||||||
let descriptor = wallet
|
let descriptor = wallet
|
||||||
.descriptor
|
.get_descriptor_for_keychain(KeychainKind::External)
|
||||||
.to_string_with_secret(&wallet.signers.as_key_map(wallet.secp_ctx()));
|
.to_string_with_secret(
|
||||||
|
&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)?;
|
||||||
|
|
||||||
@@ -136,18 +145,30 @@ impl WalletExport {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let export = WalletExport {
|
let export = FullyNodedExport {
|
||||||
descriptor,
|
descriptor,
|
||||||
label: label.into(),
|
label: label.into(),
|
||||||
blockheight,
|
blockheight,
|
||||||
};
|
};
|
||||||
|
|
||||||
let desc_to_string = |d: &Descriptor<DescriptorPublicKey>| {
|
let change_descriptor = match wallet
|
||||||
let descriptor =
|
.public_descriptor(KeychainKind::Internal)
|
||||||
d.to_string_with_secret(&wallet.change_signers.as_key_map(wallet.secp_ctx()));
|
.map_err(|_| "Invalid change descriptor")?
|
||||||
remove_checksum(descriptor)
|
.is_some()
|
||||||
|
{
|
||||||
|
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() != wallet.change_descriptor.as_ref().map(desc_to_string) {
|
if export.change_descriptor() != change_descriptor {
|
||||||
return Err("Incompatible change descriptor");
|
return Err("Incompatible change descriptor");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +269,7 @@ mod test {
|
|||||||
get_test_db(),
|
get_test_db(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
let export = FullyNodedExport::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()));
|
||||||
@@ -266,7 +287,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();
|
||||||
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -285,7 +306,7 @@ mod test {
|
|||||||
get_test_db(),
|
get_test_db(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -308,7 +329,7 @@ mod test {
|
|||||||
get_test_db(),
|
get_test_db(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
let export = FullyNodedExport::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()));
|
||||||
@@ -328,7 +349,7 @@ mod test {
|
|||||||
get_test_db(),
|
get_test_db(),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let export = WalletExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
let export = FullyNodedExport::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\"}");
|
||||||
}
|
}
|
||||||
@@ -339,7 +360,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 = WalletExport::from_str(import_str).unwrap();
|
let export = FullyNodedExport::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,6 +140,8 @@ 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 {
|
||||||
@@ -246,6 +248,7 @@ where
|
|||||||
.map(|address| AddressInfo {
|
.map(|address| AddressInfo {
|
||||||
address,
|
address,
|
||||||
index: incremented_index,
|
index: incremented_index,
|
||||||
|
keychain,
|
||||||
})
|
})
|
||||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||||
}
|
}
|
||||||
@@ -276,6 +279,7 @@ where
|
|||||||
.map(|address| AddressInfo {
|
.map(|address| AddressInfo {
|
||||||
address,
|
address,
|
||||||
index: current_index,
|
index: current_index,
|
||||||
|
keychain,
|
||||||
})
|
})
|
||||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||||
}
|
}
|
||||||
@@ -286,7 +290,11 @@ where
|
|||||||
self.get_descriptor_for_keychain(keychain)
|
self.get_descriptor_for_keychain(keychain)
|
||||||
.as_derived(index, &self.secp)
|
.as_derived(index, &self.secp)
|
||||||
.address(self.network)
|
.address(self.network)
|
||||||
.map(|address| AddressInfo { index, address })
|
.map(|address| AddressInfo {
|
||||||
|
index,
|
||||||
|
address,
|
||||||
|
keychain,
|
||||||
|
})
|
||||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +306,11 @@ where
|
|||||||
self.get_descriptor_for_keychain(keychain)
|
self.get_descriptor_for_keychain(keychain)
|
||||||
.as_derived(index, &self.secp)
|
.as_derived(index, &self.secp)
|
||||||
.address(self.network)
|
.address(self.network)
|
||||||
.map(|address| AddressInfo { index, address })
|
.map(|address| AddressInfo {
|
||||||
|
index,
|
||||||
|
address,
|
||||||
|
keychain,
|
||||||
|
})
|
||||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,7 +399,13 @@ 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> {
|
||||||
self.database.borrow().iter_utxos()
|
Ok(self
|
||||||
|
.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
|
||||||
@@ -450,6 +468,29 @@ 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.
|
||||||
@@ -457,6 +498,11 @@ 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.
|
||||||
@@ -879,6 +925,7 @@ where
|
|||||||
outpoint: txin.previous_output,
|
outpoint: txin.previous_output,
|
||||||
txout,
|
txout,
|
||||||
keychain,
|
keychain,
|
||||||
|
is_spent: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(WeightedUtxo {
|
Ok(WeightedUtxo {
|
||||||
@@ -1549,6 +1596,18 @@ 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.
|
||||||
@@ -3917,6 +3976,7 @@ 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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3925,7 +3985,8 @@ 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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3934,7 +3995,8 @@ 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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3943,7 +4005,8 @@ 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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3952,7 +4015,8 @@ 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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -3961,7 +4025,8 @@ 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,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3990,15 +4055,21 @@ pub(crate) mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
wallet.get_address(AddressIndex::New).unwrap().address,
|
wallet.get_address(AddressIndex::New).unwrap(),
|
||||||
Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap()
|
AddressInfo {
|
||||||
|
index: 0,
|
||||||
|
address: Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap(),
|
||||||
|
keychain: KeychainKind::External,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
wallet
|
wallet.get_internal_address(AddressIndex::New).unwrap(),
|
||||||
.get_internal_address(AddressIndex::New)
|
AddressInfo {
|
||||||
.unwrap()
|
index: 0,
|
||||||
.address,
|
address: Address::from_str("bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa").unwrap(),
|
||||||
Address::from_str("bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa").unwrap()
|
keychain: KeychainKind::Internal,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
let wallet = Wallet::new(
|
let wallet = Wallet::new(
|
||||||
@@ -4010,11 +4081,12 @@ pub(crate) mod test {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
wallet
|
wallet.get_internal_address(AddressIndex::New).unwrap(),
|
||||||
.get_internal_address(AddressIndex::New)
|
AddressInfo {
|
||||||
.unwrap()
|
index: 0,
|
||||||
.address,
|
address: Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap(),
|
||||||
Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap(),
|
keychain: KeychainKind::Internal,
|
||||||
|
},
|
||||||
"when there's no internal descriptor it should just use external"
|
"when there's no internal descriptor it should just use external"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -838,6 +838,7 @@ mod test {
|
|||||||
},
|
},
|
||||||
txout: Default::default(),
|
txout: Default::default(),
|
||||||
keychain: KeychainKind::External,
|
keychain: KeychainKind::External,
|
||||||
|
is_spent: false,
|
||||||
},
|
},
|
||||||
LocalUtxo {
|
LocalUtxo {
|
||||||
outpoint: OutPoint {
|
outpoint: OutPoint {
|
||||||
@@ -846,6 +847,7 @@ mod test {
|
|||||||
},
|
},
|
||||||
txout: Default::default(),
|
txout: Default::default(),
|
||||||
keychain: KeychainKind::Internal,
|
keychain: KeychainKind::Internal,
|
||||||
|
is_spent: false,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user