Compare commits
16 Commits
release/1.
...
v0.25.0-rc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5168d41a93 | ||
|
|
4c5ceaff14 | ||
|
|
b5fcddcf1a | ||
|
|
d570ff2c65 | ||
|
|
21c96c9c81 | ||
|
|
c51d544932 | ||
|
|
5e56c3b3c1 | ||
|
|
235961a934 | ||
|
|
df905a8d5e | ||
|
|
8b68cf9546 | ||
|
|
150f4d6f41 | ||
|
|
1c95ca33a8 | ||
|
|
108edc3a6b | ||
|
|
f99a6b9f43 | ||
|
|
aedbc8c97d | ||
|
|
e9bbb8724f |
24
Cargo.toml
24
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "bdk"
|
name = "bdk"
|
||||||
version = "0.24.0"
|
version = "0.25.0-rc.1"
|
||||||
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"
|
||||||
@@ -41,7 +41,7 @@ bitcoincore-rpc = { version = "0.16", 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", features = ["rt"] }
|
tokio = { version = "1", features = ["rt", "macros"] }
|
||||||
|
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
getrandom = "0.2"
|
getrandom = "0.2"
|
||||||
@@ -138,6 +138,26 @@ name = "hardware_signer"
|
|||||||
path = "examples/hardware_signer.rs"
|
path = "examples/hardware_signer.rs"
|
||||||
required-features = ["electrum", "hardware-signer"]
|
required-features = ["electrum", "hardware-signer"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "electrum_backend"
|
||||||
|
path = "examples/electrum_backend.rs"
|
||||||
|
required-features = ["electrum"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "esplora_backend_synchronous"
|
||||||
|
path = "examples/esplora_backend_synchronous.rs"
|
||||||
|
required-features = ["use-esplora-ureq"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "esplora_backend_asynchronous"
|
||||||
|
path = "examples/esplora_backend_asynchronous.rs"
|
||||||
|
required-features = ["use-esplora-reqwest", "reqwest-default-tls", "async-interface"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "mnemonic_to_descriptors"
|
||||||
|
path = "examples/mnemonic_to_descriptors.rs"
|
||||||
|
required-features = ["all-keys"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["macros"]
|
members = ["macros"]
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|||||||
87
examples/electrum_backend.rs
Normal file
87
examples/electrum_backend.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use bdk::bitcoin::util::bip32::ExtendedPrivKey;
|
||||||
|
use bdk::bitcoin::Network;
|
||||||
|
use bdk::blockchain::{Blockchain, ElectrumBlockchain};
|
||||||
|
use bdk::database::MemoryDatabase;
|
||||||
|
use bdk::template::Bip84;
|
||||||
|
use bdk::wallet::export::FullyNodedExport;
|
||||||
|
use bdk::{KeychainKind, SyncOptions, Wallet};
|
||||||
|
|
||||||
|
use bdk::electrum_client::Client;
|
||||||
|
use bdk::wallet::AddressIndex;
|
||||||
|
use bitcoin::util::bip32;
|
||||||
|
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
use crate::utils::tx::build_signed_tx;
|
||||||
|
|
||||||
|
/// This will create a wallet from an xpriv and get the balance by connecting to an Electrum server.
|
||||||
|
/// If enough amount is available, this will send a transaction to an address.
|
||||||
|
/// Otherwise, this will display a wallet address to receive funds.
|
||||||
|
///
|
||||||
|
/// This can be run with `cargo run --example electrum_backend` in the root folder.
|
||||||
|
fn main() {
|
||||||
|
let network = Network::Testnet;
|
||||||
|
|
||||||
|
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||||
|
|
||||||
|
let electrum_url = "ssl://electrum.blockstream.info:60002";
|
||||||
|
|
||||||
|
run(&network, electrum_url, xpriv);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||||
|
Wallet::new(
|
||||||
|
Bip84(*xpriv, KeychainKind::External),
|
||||||
|
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||||
|
*network,
|
||||||
|
MemoryDatabase::default(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(network: &Network, electrum_url: &str, xpriv: &str) {
|
||||||
|
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||||
|
|
||||||
|
// Apparently it works only with Electrs (not EletrumX)
|
||||||
|
let blockchain = ElectrumBlockchain::from(Client::new(electrum_url).unwrap());
|
||||||
|
|
||||||
|
let wallet = create_wallet(network, &xpriv);
|
||||||
|
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||||
|
|
||||||
|
println!("address: {}", address);
|
||||||
|
|
||||||
|
let balance = wallet.get_balance().unwrap();
|
||||||
|
|
||||||
|
println!("Available coins in BDK wallet : {} sats", balance);
|
||||||
|
|
||||||
|
if balance.confirmed > 6500 {
|
||||||
|
// the wallet sends the amount to itself.
|
||||||
|
let recipient_address = wallet
|
||||||
|
.get_address(AddressIndex::New)
|
||||||
|
.unwrap()
|
||||||
|
.address
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let amount = 5359;
|
||||||
|
|
||||||
|
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||||
|
|
||||||
|
blockchain.broadcast(&tx).unwrap();
|
||||||
|
|
||||||
|
println!("tx id: {}", tx.txid());
|
||||||
|
} else {
|
||||||
|
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||||
|
}
|
||||||
|
|
||||||
|
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||||
|
.map_err(ToString::to_string)
|
||||||
|
.map_err(bdk::Error::Generic)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("------\nWallet Backup: {}", export.to_string());
|
||||||
|
}
|
||||||
93
examples/esplora_backend_asynchronous.rs
Normal file
93
examples/esplora_backend_asynchronous.rs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use bdk::blockchain::Blockchain;
|
||||||
|
use bdk::{
|
||||||
|
blockchain::esplora::EsploraBlockchain,
|
||||||
|
database::MemoryDatabase,
|
||||||
|
template::Bip84,
|
||||||
|
wallet::{export::FullyNodedExport, AddressIndex},
|
||||||
|
KeychainKind, SyncOptions, Wallet,
|
||||||
|
};
|
||||||
|
use bitcoin::{
|
||||||
|
util::bip32::{self, ExtendedPrivKey},
|
||||||
|
Network,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
use crate::utils::tx::build_signed_tx;
|
||||||
|
|
||||||
|
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
|
||||||
|
/// using non blocking asynchronous calls with `reqwest`.
|
||||||
|
/// If enough amount is available, this will send a transaction to an address.
|
||||||
|
/// Otherwise, this will display a wallet address to receive funds.
|
||||||
|
///
|
||||||
|
/// This can be run with `cargo run --no-default-features --features="use-esplora-reqwest, reqwest-default-tls, async-interface" --example esplora_backend_asynchronous`
|
||||||
|
/// in the root folder.
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() {
|
||||||
|
let network = Network::Signet;
|
||||||
|
|
||||||
|
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||||
|
|
||||||
|
let esplora_url = "https://explorer.bc-2.jp/api";
|
||||||
|
|
||||||
|
run(&network, esplora_url, xpriv).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||||
|
Wallet::new(
|
||||||
|
Bip84(*xpriv, KeychainKind::External),
|
||||||
|
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||||
|
*network,
|
||||||
|
MemoryDatabase::default(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(network: &Network, esplora_url: &str, xpriv: &str) {
|
||||||
|
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||||
|
|
||||||
|
let blockchain = EsploraBlockchain::new(esplora_url, 20);
|
||||||
|
|
||||||
|
let wallet = create_wallet(network, &xpriv);
|
||||||
|
|
||||||
|
wallet
|
||||||
|
.sync(&blockchain, SyncOptions::default())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||||
|
|
||||||
|
println!("address: {}", address);
|
||||||
|
|
||||||
|
let balance = wallet.get_balance().unwrap();
|
||||||
|
|
||||||
|
println!("Available coins in BDK wallet : {} sats", balance);
|
||||||
|
|
||||||
|
if balance.confirmed > 10500 {
|
||||||
|
// the wallet sends the amount to itself.
|
||||||
|
let recipient_address = wallet
|
||||||
|
.get_address(AddressIndex::New)
|
||||||
|
.unwrap()
|
||||||
|
.address
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let amount = 9359;
|
||||||
|
|
||||||
|
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||||
|
|
||||||
|
let _ = blockchain.broadcast(&tx);
|
||||||
|
|
||||||
|
println!("tx id: {}", tx.txid());
|
||||||
|
} else {
|
||||||
|
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||||
|
}
|
||||||
|
|
||||||
|
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||||
|
.map_err(ToString::to_string)
|
||||||
|
.map_err(bdk::Error::Generic)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("------\nWallet Backup: {}", export.to_string());
|
||||||
|
}
|
||||||
89
examples/esplora_backend_synchronous.rs
Normal file
89
examples/esplora_backend_synchronous.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use bdk::blockchain::Blockchain;
|
||||||
|
use bdk::{
|
||||||
|
blockchain::esplora::EsploraBlockchain,
|
||||||
|
database::MemoryDatabase,
|
||||||
|
template::Bip84,
|
||||||
|
wallet::{export::FullyNodedExport, AddressIndex},
|
||||||
|
KeychainKind, SyncOptions, Wallet,
|
||||||
|
};
|
||||||
|
use bitcoin::{
|
||||||
|
util::bip32::{self, ExtendedPrivKey},
|
||||||
|
Network,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
use crate::utils::tx::build_signed_tx;
|
||||||
|
|
||||||
|
/// This will create a wallet from an xpriv and get the balance by connecting to an Esplora server,
|
||||||
|
/// using blocking calls with `ureq`.
|
||||||
|
/// If enough amount is available, this will send a transaction to an address.
|
||||||
|
/// Otherwise, this will display a wallet address to receive funds.
|
||||||
|
///
|
||||||
|
/// This can be run with `cargo run --features=use-esplora-ureq --example esplora_backend_synchronous`
|
||||||
|
/// in the root folder.
|
||||||
|
fn main() {
|
||||||
|
let network = Network::Signet;
|
||||||
|
|
||||||
|
let xpriv = "tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy";
|
||||||
|
|
||||||
|
let esplora_url = "https://explorer.bc-2.jp/api";
|
||||||
|
|
||||||
|
run(&network, esplora_url, xpriv);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_wallet(network: &Network, xpriv: &ExtendedPrivKey) -> Wallet<MemoryDatabase> {
|
||||||
|
Wallet::new(
|
||||||
|
Bip84(*xpriv, KeychainKind::External),
|
||||||
|
Some(Bip84(*xpriv, KeychainKind::Internal)),
|
||||||
|
*network,
|
||||||
|
MemoryDatabase::default(),
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(network: &Network, esplora_url: &str, xpriv: &str) {
|
||||||
|
let xpriv = bip32::ExtendedPrivKey::from_str(xpriv).unwrap();
|
||||||
|
|
||||||
|
let blockchain = EsploraBlockchain::new(esplora_url, 20);
|
||||||
|
|
||||||
|
let wallet = create_wallet(network, &xpriv);
|
||||||
|
|
||||||
|
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||||
|
|
||||||
|
let address = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||||
|
|
||||||
|
println!("address: {}", address);
|
||||||
|
|
||||||
|
let balance = wallet.get_balance().unwrap();
|
||||||
|
|
||||||
|
println!("Available coins in BDK wallet : {} sats", balance);
|
||||||
|
|
||||||
|
if balance.confirmed > 10500 {
|
||||||
|
// the wallet sends the amount to itself.
|
||||||
|
let recipient_address = wallet
|
||||||
|
.get_address(AddressIndex::New)
|
||||||
|
.unwrap()
|
||||||
|
.address
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let amount = 9359;
|
||||||
|
|
||||||
|
let tx = build_signed_tx(&wallet, &recipient_address, amount);
|
||||||
|
|
||||||
|
blockchain.broadcast(&tx).unwrap();
|
||||||
|
|
||||||
|
println!("tx id: {}", tx.txid());
|
||||||
|
} else {
|
||||||
|
println!("Insufficient Funds. Fund the wallet with the address above");
|
||||||
|
}
|
||||||
|
|
||||||
|
let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||||
|
.map_err(ToString::to_string)
|
||||||
|
.map_err(bdk::Error::Generic)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
println!("------\nWallet Backup: {}", export.to_string());
|
||||||
|
}
|
||||||
60
examples/mnemonic_to_descriptors.rs
Normal file
60
examples/mnemonic_to_descriptors.rs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// 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::util::bip32::DerivationPath;
|
||||||
|
use bdk::bitcoin::Network;
|
||||||
|
use bdk::descriptor;
|
||||||
|
use bdk::descriptor::IntoWalletDescriptor;
|
||||||
|
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
|
||||||
|
use bdk::keys::{GeneratableKey, GeneratedKey};
|
||||||
|
use bdk::miniscript::Tap;
|
||||||
|
use bdk::Error as BDK_Error;
|
||||||
|
use std::error::Error;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
/// This example demonstrates how to generate a mnemonic phrase
|
||||||
|
/// using BDK and use that to generate a descriptor string.
|
||||||
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
|
// In this example we are generating a 12 words mnemonic phrase
|
||||||
|
// but it is also possible generate 15, 18, 21 and 24 words
|
||||||
|
// using their respective `WordCount` variant.
|
||||||
|
let mnemonic: GeneratedKey<_, Tap> =
|
||||||
|
Mnemonic::generate((WordCount::Words12, Language::English))
|
||||||
|
.map_err(|_| BDK_Error::Generic("Mnemonic generation error".to_string()))?;
|
||||||
|
|
||||||
|
println!("Mnemonic phrase: {}", *mnemonic);
|
||||||
|
let mnemonic_with_passphrase = (mnemonic, None);
|
||||||
|
|
||||||
|
// define external and internal derivation key path
|
||||||
|
let external_path = DerivationPath::from_str("m/86h/0h/0h/0").unwrap();
|
||||||
|
let internal_path = DerivationPath::from_str("m/86h/0h/0h/1").unwrap();
|
||||||
|
|
||||||
|
// generate external and internal descriptor from mnemonic
|
||||||
|
let (external_descriptor, ext_keymap) =
|
||||||
|
descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))?
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||||
|
let (internal_descriptor, int_keymap) =
|
||||||
|
descriptor!(tr((mnemonic_with_passphrase, internal_path)))?
|
||||||
|
.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||||
|
|
||||||
|
println!("tpub external descriptor: {}", external_descriptor);
|
||||||
|
println!("tpub internal descriptor: {}", internal_descriptor);
|
||||||
|
println!(
|
||||||
|
"tprv external descriptor: {}",
|
||||||
|
external_descriptor.to_string_with_secret(&ext_keymap)
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"tprv internal descriptor: {}",
|
||||||
|
internal_descriptor.to_string_with_secret(&int_keymap)
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
30
examples/utils/mod.rs
Normal file
30
examples/utils/mod.rs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
pub(crate) mod tx {
|
||||||
|
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use bdk::{database::BatchDatabase, SignOptions, Wallet};
|
||||||
|
use bitcoin::{Address, Transaction};
|
||||||
|
|
||||||
|
pub fn build_signed_tx<D: BatchDatabase>(
|
||||||
|
wallet: &Wallet<D>,
|
||||||
|
recipient_address: &str,
|
||||||
|
amount: u64,
|
||||||
|
) -> Transaction {
|
||||||
|
// Create a transaction builder
|
||||||
|
let mut tx_builder = wallet.build_tx();
|
||||||
|
|
||||||
|
let to_address = Address::from_str(recipient_address).unwrap();
|
||||||
|
|
||||||
|
// Set recipient of the transaction
|
||||||
|
tx_builder.set_recipients(vec![(to_address.script_pubkey(), amount)]);
|
||||||
|
|
||||||
|
// Finalise the transaction and extract PSBT
|
||||||
|
let (mut psbt, _) = tx_builder.finish().unwrap();
|
||||||
|
|
||||||
|
// Sign the above psbt with signing option
|
||||||
|
wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||||
|
|
||||||
|
// Extract the final transaction
|
||||||
|
psbt.extract_tx()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,17 @@ static MIGRATIONS: &[&str] = &[
|
|||||||
"CREATE TABLE utxos (value INTEGER, keychain TEXT, vout INTEGER, txid BLOB, script BLOB, is_spent BOOLEAN DEFAULT 0);",
|
"CREATE TABLE utxos (value INTEGER, keychain TEXT, vout INTEGER, txid BLOB, script BLOB, is_spent BOOLEAN DEFAULT 0);",
|
||||||
"INSERT INTO utxos SELECT value, keychain, vout, txid, script, is_spent FROM utxos_old;",
|
"INSERT INTO utxos SELECT value, keychain, vout, txid, script, is_spent FROM utxos_old;",
|
||||||
"DROP TABLE utxos_old;",
|
"DROP TABLE utxos_old;",
|
||||||
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);"
|
"CREATE UNIQUE INDEX idx_utxos_txid_vout ON utxos(txid, vout);",
|
||||||
|
// Fix issue https://github.com/bitcoindevkit/bdk/issues/801: drop duplicated script_pubkeys
|
||||||
|
"ALTER TABLE script_pubkeys RENAME TO script_pubkeys_old;",
|
||||||
|
"DROP INDEX idx_keychain_child;",
|
||||||
|
"DROP INDEX idx_script;",
|
||||||
|
"CREATE TABLE script_pubkeys (keychain TEXT, child INTEGER, script BLOB);",
|
||||||
|
"CREATE INDEX idx_keychain_child ON script_pubkeys(keychain, child);",
|
||||||
|
"CREATE INDEX idx_script ON script_pubkeys(script);",
|
||||||
|
"CREATE UNIQUE INDEX idx_script_pks_unique ON script_pubkeys(keychain, child);",
|
||||||
|
"INSERT OR REPLACE INTO script_pubkeys SELECT keychain, child, script FROM script_pubkeys_old;",
|
||||||
|
"DROP TABLE script_pubkeys_old;"
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Sqlite database stored on filesystem
|
/// Sqlite database stored on filesystem
|
||||||
@@ -88,7 +98,7 @@ impl SqliteDatabase {
|
|||||||
child: u32,
|
child: u32,
|
||||||
script: &[u8],
|
script: &[u8],
|
||||||
) -> Result<i64, Error> {
|
) -> Result<i64, Error> {
|
||||||
let mut statement = self.connection.prepare_cached("INSERT INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
|
let mut statement = self.connection.prepare_cached("INSERT OR REPLACE INTO script_pubkeys (keychain, child, script) VALUES (:keychain, :child, :script)")?;
|
||||||
statement.execute(named_params! {
|
statement.execute(named_params! {
|
||||||
":keychain": keychain,
|
":keychain": keychain,
|
||||||
":child": child,
|
":child": child,
|
||||||
@@ -1096,4 +1106,44 @@ pub mod test {
|
|||||||
fn test_check_descriptor_checksum() {
|
fn test_check_descriptor_checksum() {
|
||||||
crate::database::test::test_check_descriptor_checksum(get_database());
|
crate::database::test::test_check_descriptor_checksum(get_database());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Issue 801: https://github.com/bitcoindevkit/bdk/issues/801
|
||||||
|
#[test]
|
||||||
|
fn test_unique_spks() {
|
||||||
|
use crate::bitcoin::hashes::hex::FromHex;
|
||||||
|
use crate::database::*;
|
||||||
|
|
||||||
|
let mut db = get_database();
|
||||||
|
|
||||||
|
let script = Script::from(
|
||||||
|
Vec::<u8>::from_hex("76a91402306a7c23f3e8010de41e9e591348bb83f11daa88ac").unwrap(),
|
||||||
|
);
|
||||||
|
let path = 42;
|
||||||
|
let keychain = KeychainKind::External;
|
||||||
|
|
||||||
|
for _ in 0..100 {
|
||||||
|
db.set_script_pubkey(&script, keychain, path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut statement = db
|
||||||
|
.connection
|
||||||
|
.prepare_cached(
|
||||||
|
"select keychain,child,count(child) from script_pubkeys group by keychain,child;",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let mut rows = statement.query([]).unwrap();
|
||||||
|
while let Some(row) = rows.next().unwrap() {
|
||||||
|
let keychain: String = row.get(0).unwrap();
|
||||||
|
let child: u32 = row.get(1).unwrap();
|
||||||
|
let count: usize = row.get(2).unwrap();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
count == 1,
|
||||||
|
"keychain={}, child={}, count={}",
|
||||||
|
keychain,
|
||||||
|
child,
|
||||||
|
count
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ use bitcoin::util::{psbt, taproot};
|
|||||||
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
|
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
|
||||||
use bitcoin::{Network, TxOut};
|
use bitcoin::{Network, TxOut};
|
||||||
|
|
||||||
use miniscript::descriptor::{DefiniteDescriptorKey, DescriptorType, InnerXKey, SinglePubKey};
|
use miniscript::descriptor::{
|
||||||
|
DefiniteDescriptorKey, DescriptorSecretKey, DescriptorType, InnerXKey, SinglePubKey,
|
||||||
|
};
|
||||||
pub use miniscript::{
|
pub use miniscript::{
|
||||||
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
||||||
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||||
@@ -240,14 +242,34 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.2.contains(&network) {
|
let (desc, keymap, networks) = self;
|
||||||
|
|
||||||
|
if !networks.contains(&network) {
|
||||||
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
|
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixup the network for keys that need it
|
// fixup the network for keys that need it in the descriptor
|
||||||
let translated = self.0.translate_pk(&mut Translator { network })?;
|
let translated = desc.translate_pk(&mut Translator { network })?;
|
||||||
|
// ...and in the key map
|
||||||
|
let fixed_keymap = keymap
|
||||||
|
.into_iter()
|
||||||
|
.map(|(mut k, mut v)| {
|
||||||
|
match (&mut k, &mut v) {
|
||||||
|
(DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
|
||||||
|
xpub.xkey.network = network;
|
||||||
|
xprv.xkey.network = network;
|
||||||
|
}
|
||||||
|
(_, DescriptorSecretKey::Single(key)) => {
|
||||||
|
key.key.network = network;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
Ok((translated, self.1))
|
(k, v)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok((translated, fixed_keymap))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -682,23 +704,40 @@ mod test {
|
|||||||
|
|
||||||
let secp = Secp256k1::new();
|
let secp = Secp256k1::new();
|
||||||
|
|
||||||
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
|
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
|
||||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||||
|
|
||||||
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
||||||
// we are using an "xpub"
|
// we are using an "xpub"
|
||||||
let key = (xpub, path).into_descriptor_key().unwrap();
|
let key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||||
// override it with any. this happens in some key conversions, like bip39
|
// override it with any. this happens in some key conversions, like bip39
|
||||||
let key = key.override_valid_networks(any_network());
|
let key = key.override_valid_networks(any_network());
|
||||||
|
|
||||||
// make a descriptor out of it
|
// make a descriptor out of it
|
||||||
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
||||||
// this should convert the key that supports "any_network" to the right network (testnet)
|
// this should convert the key that supports "any_network" to the right network (testnet)
|
||||||
let (wallet_desc, _) = desc
|
let (wallet_desc, keymap) = desc
|
||||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)#y8p7e8kk");
|
let mut xprv_testnet = xprv;
|
||||||
|
xprv_testnet.network = Network::Testnet;
|
||||||
|
|
||||||
|
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
|
||||||
|
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
|
||||||
|
xkey: xpub_testnet,
|
||||||
|
origin: None,
|
||||||
|
derivation_path: path,
|
||||||
|
wildcard: Wildcard::Unhardened,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha");
|
||||||
|
assert_eq!(
|
||||||
|
keymap
|
||||||
|
.get(&desc_pubkey)
|
||||||
|
.map(|key| key.to_public(&secp).unwrap()),
|
||||||
|
Some(desc_pubkey)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// test IntoWalletDescriptor trait from &str with and without checksum appended
|
// test IntoWalletDescriptor trait from &str with and without checksum appended
|
||||||
|
|||||||
@@ -134,15 +134,11 @@ impl FullyNodedExport {
|
|||||||
let blockheight = match wallet.database.borrow().iter_txs(false) {
|
let blockheight = match wallet.database.borrow().iter_txs(false) {
|
||||||
_ if !include_blockheight => 0,
|
_ if !include_blockheight => 0,
|
||||||
Err(_) => 0,
|
Err(_) => 0,
|
||||||
Ok(txs) => {
|
Ok(txs) => txs
|
||||||
let mut heights = txs
|
.into_iter()
|
||||||
.into_iter()
|
.filter_map(|tx| tx.confirmation_time.map(|c| c.height))
|
||||||
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
|
.min()
|
||||||
.collect::<Vec<_>>();
|
.unwrap_or(0),
|
||||||
heights.sort_unstable();
|
|
||||||
|
|
||||||
*heights.last().unwrap_or(&0)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let export = FullyNodedExport {
|
let export = FullyNodedExport {
|
||||||
@@ -249,6 +245,22 @@ mod test {
|
|||||||
fee: Some(500),
|
fee: Some(500),
|
||||||
confirmation_time: Some(BlockTime {
|
confirmation_time: Some(BlockTime {
|
||||||
timestamp: 12345678,
|
timestamp: 12345678,
|
||||||
|
height: 5001,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
db.set_tx(&TransactionDetails {
|
||||||
|
transaction: None,
|
||||||
|
txid: Txid::from_str(
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
received: 25_000,
|
||||||
|
sent: 0,
|
||||||
|
fee: Some(300),
|
||||||
|
confirmation_time: Some(BlockTime {
|
||||||
|
timestamp: 12345677,
|
||||||
height: 5000,
|
height: 5000,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user