Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
78d26f6eb3 | ||
|
|
844856d39e | ||
|
|
b5a120c649 | ||
|
|
92b9597f8b | ||
|
|
556105780b | ||
|
|
af6bde3997 | ||
|
|
4bd1fd2441 | ||
|
|
45db468c9b | ||
|
|
01141bed5a | ||
|
|
87e8646743 | ||
|
|
dd51380520 | ||
|
|
73d4f6d3b1 | ||
|
|
2af678aa84 | ||
|
|
1c94108d7e | ||
|
|
5d00f82388 | ||
|
|
98748906f6 | ||
|
|
dd832cb57a | ||
|
|
e3a17f67d9 | ||
|
|
c2e4ba8cbd | ||
|
|
1d9fdd01fa | ||
|
|
db9d43ed2f | ||
|
|
ec22fa2ad0 | ||
|
|
0e92820af4 | ||
|
|
e85aa247cb | ||
|
|
612da165f8 | ||
|
|
1fd62a7afc | ||
|
|
8a5f89e129 | ||
|
|
063d51fd75 | ||
|
|
0e0d5a0e95 | ||
|
|
bb55923a7d | ||
|
|
f184557fa0 | ||
|
|
77c7d0aae9 | ||
|
|
5ff8320e3b | ||
|
|
e68d3b9e63 | ||
|
|
97bc9dc717 | ||
|
|
6a15036867 | ||
|
|
17d0ae0f71 | ||
|
|
d020dede37 | ||
|
|
5c566bb05e | ||
|
|
b289c4ec2d | ||
|
|
2283444f72 | ||
|
|
a0e5820c32 | ||
|
|
04dc28d2b4 | ||
|
|
688ff96c8e | ||
|
|
3283a200bc | ||
|
|
3f9b4cdca9 | ||
|
|
a85ef62698 |
4
.github/workflows/cont_integration.yml
vendored
4
.github/workflows/cont_integration.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
rust:
|
||||
- version: 1.60.0 # STABLE
|
||||
clippy: true
|
||||
- version: 1.56.0 # MSRV
|
||||
- version: 1.56.1 # MSRV
|
||||
features:
|
||||
- default
|
||||
- minimal
|
||||
@@ -146,7 +146,7 @@ jobs:
|
||||
- run: sudo apt-get update || exit 1
|
||||
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
|
||||
- name: Set default toolchain
|
||||
run: rustup default 1.56.0 # STABLE
|
||||
run: rustup default 1.56.1 # STABLE
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add target wasm32
|
||||
|
||||
16
CHANGELOG.md
16
CHANGELOG.md
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
## [v0.20.0] - [v0.19.0]
|
||||
|
||||
- New MSRV set to `1.56.1`
|
||||
- Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced)
|
||||
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
|
||||
- Set coin type in BIP44, BIP49, and BIP84 templates
|
||||
- Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash.
|
||||
- Add `remove_partial_sigs` and `try_finalize` to `SignOptions`
|
||||
- Deprecate `AddressValidator`
|
||||
- Fix Electrum wallet sync potentially causing address index decrement - compare proposed index and current index before applying batch operations during sync.
|
||||
|
||||
## [v0.19.0] - [v0.18.0]
|
||||
|
||||
- added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm`
|
||||
@@ -51,6 +63,7 @@ To decouple the `Wallet` from the `Blockchain` we've made major changes:
|
||||
- Stop making a request for the block height when calling `Wallet:new`.
|
||||
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
|
||||
- Removed `max_addresses` sync parameter which determined how many addresses to cache before syncing since this can just be done with `ensure_addresses_cached`.
|
||||
- remove `flush` method from the `Database` trait.
|
||||
|
||||
## [v0.16.1] - [v0.16.0]
|
||||
|
||||
@@ -465,4 +478,5 @@ final transaction is created by calling `finish` on the builder.
|
||||
[v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
|
||||
[v0.18.0]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...v0.18.0
|
||||
[v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.0
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...HEAD
|
||||
[v0.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...HEAD
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.19.0"
|
||||
version = "0.20.0"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -23,8 +23,8 @@ rand = "^0.7"
|
||||
# Optional dependencies
|
||||
sled = { version = "0.34", optional = true }
|
||||
electrum-client = { version = "0.10", optional = true }
|
||||
rusqlite = { version = "0.25.3", optional = true }
|
||||
ahash = { version = "=0.7.4", optional = true }
|
||||
rusqlite = { version = "0.27.0", optional = true }
|
||||
ahash = { version = "0.7.6", optional = true }
|
||||
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
|
||||
ureq = { version = "~2.2.0", features = ["json"], optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
|
||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2020/08/27/Rust-1.56.0.html"><img alt="Rustc Version 1.56+" src="https://img.shields.io/badge/rustc-1.56%2B-lightgrey.svg"/></a>
|
||||
<a href="https://blog.rust-lang.org/2021/11/01/Rust-1.56.1.html"><img alt="Rustc Version 1.56.1+" src="https://img.shields.io/badge/rustc-1.56.1%2B-lightgrey.svg"/></a>
|
||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||
</p>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ use std::sync::Arc;
|
||||
use bdk::bitcoin;
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::descriptor::HdKeyPaths;
|
||||
#[allow(deprecated)]
|
||||
use bdk::wallet::address_validator::{AddressValidator, AddressValidatorError};
|
||||
use bdk::KeychainKind;
|
||||
use bdk::Wallet;
|
||||
@@ -25,6 +26,7 @@ use bitcoin::{Network, Script};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DummyValidator;
|
||||
#[allow(deprecated)]
|
||||
impl AddressValidator for DummyValidator {
|
||||
fn validate(
|
||||
&self,
|
||||
@@ -50,6 +52,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
let descriptor = "sh(and_v(v:pk(tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/*),after(630000)))";
|
||||
let mut wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new())?;
|
||||
|
||||
#[allow(deprecated)]
|
||||
wallet.add_address_validator(Arc::new(DummyValidator));
|
||||
|
||||
wallet.get_address(New)?;
|
||||
|
||||
@@ -120,6 +120,13 @@ impl GetTx for AnyBlockchain {
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl GetBlockHash for AnyBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
maybe_await!(impl_inner_method!(self, get_block_hash, height))
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl WalletSync for AnyBlockchain {
|
||||
fn wallet_sync<D: BatchDatabase>(
|
||||
|
||||
@@ -260,6 +260,16 @@ impl GetTx for CompactFiltersBlockchain {
|
||||
}
|
||||
}
|
||||
|
||||
impl GetBlockHash for CompactFiltersBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
self.headers
|
||||
.get_block_hash(height as usize)?
|
||||
.ok_or(Error::CompactFilters(
|
||||
CompactFiltersError::BlockHashNotFound,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletSync for CompactFiltersBlockchain {
|
||||
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
@@ -536,6 +546,8 @@ pub enum CompactFiltersError {
|
||||
InvalidFilter,
|
||||
/// The peer is missing a block in the valid chain
|
||||
MissingBlock,
|
||||
/// Block hash at specified height not found
|
||||
BlockHashNotFound,
|
||||
/// The data stored in the block filters storage are corrupted
|
||||
DataCorruption,
|
||||
|
||||
|
||||
@@ -98,6 +98,13 @@ impl GetTx for ElectrumBlockchain {
|
||||
}
|
||||
}
|
||||
|
||||
impl GetBlockHash for ElectrumBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
let block_header = self.client.block_header(height as usize)?;
|
||||
Ok(block_header.block_hash())
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletSync for ElectrumBlockchain {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
@@ -108,7 +115,10 @@ impl WalletSync for ElectrumBlockchain {
|
||||
let mut block_times = HashMap::<u32, u32>::new();
|
||||
let mut txid_to_height = HashMap::<Txid, u32>::new();
|
||||
let mut tx_cache = TxCache::new(database, &self.client);
|
||||
let chunk_size = self.stop_gap;
|
||||
|
||||
// Set chunk_size to the smallest value capable of finding a gap greater than stop_gap.
|
||||
let chunk_size = self.stop_gap + 1;
|
||||
|
||||
// The electrum server has been inconsistent somehow in its responses during sync. For
|
||||
// example, we do a batch request of transactions and the response contains less
|
||||
// tranascations than in the request. This should never happen but we don't want to panic.
|
||||
@@ -144,21 +154,12 @@ impl WalletSync for ElectrumBlockchain {
|
||||
|
||||
Request::Conftime(conftime_req) => {
|
||||
// collect up to chunk_size heights to fetch from electrum
|
||||
let needs_block_height = {
|
||||
let mut needs_block_height_iter = conftime_req
|
||||
.request()
|
||||
.filter_map(|txid| txid_to_height.get(txid).cloned())
|
||||
.filter(|height| block_times.get(height).is_none());
|
||||
let mut needs_block_height = HashSet::new();
|
||||
|
||||
while needs_block_height.len() < chunk_size {
|
||||
match needs_block_height_iter.next() {
|
||||
Some(height) => needs_block_height.insert(height),
|
||||
None => break,
|
||||
};
|
||||
}
|
||||
needs_block_height
|
||||
};
|
||||
let needs_block_height = conftime_req
|
||||
.request()
|
||||
.filter_map(|txid| txid_to_height.get(txid).cloned())
|
||||
.filter(|height| block_times.get(height).is_none())
|
||||
.take(chunk_size)
|
||||
.collect::<HashSet<u32>>();
|
||||
|
||||
let new_block_headers = self
|
||||
.client
|
||||
@@ -328,6 +329,7 @@ mod test {
|
||||
use super::*;
|
||||
use crate::database::MemoryDatabase;
|
||||
use crate::testutils::blockchain_tests::TestClient;
|
||||
use crate::testutils::configurable_blockchain_tests::ConfigurableBlockchainTester;
|
||||
use crate::wallet::{AddressIndex, Wallet};
|
||||
|
||||
crate::bdk_blockchain_tests! {
|
||||
@@ -385,4 +387,29 @@ mod test {
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_electrum_with_variable_configs() {
|
||||
struct ElectrumTester;
|
||||
|
||||
impl ConfigurableBlockchainTester<ElectrumBlockchain> for ElectrumTester {
|
||||
const BLOCKCHAIN_NAME: &'static str = "Electrum";
|
||||
|
||||
fn config_with_stop_gap(
|
||||
&self,
|
||||
test_client: &mut TestClient,
|
||||
stop_gap: usize,
|
||||
) -> Option<ElectrumBlockchainConfig> {
|
||||
Some(ElectrumBlockchainConfig {
|
||||
url: test_client.electrsd.electrum_url.clone(),
|
||||
socks5: None,
|
||||
retry: 0,
|
||||
timeout: None,
|
||||
stop_gap: stop_gap,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ElectrumTester.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,4 +209,38 @@ mod test {
|
||||
"should inherit from value for 25"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "test-esplora")]
|
||||
fn test_esplora_with_variable_configs() {
|
||||
use crate::testutils::{
|
||||
blockchain_tests::TestClient,
|
||||
configurable_blockchain_tests::ConfigurableBlockchainTester,
|
||||
};
|
||||
|
||||
struct EsploraTester;
|
||||
|
||||
impl ConfigurableBlockchainTester<EsploraBlockchain> for EsploraTester {
|
||||
const BLOCKCHAIN_NAME: &'static str = "Esplora";
|
||||
|
||||
fn config_with_stop_gap(
|
||||
&self,
|
||||
test_client: &mut TestClient,
|
||||
stop_gap: usize,
|
||||
) -> Option<EsploraBlockchainConfig> {
|
||||
Some(EsploraBlockchainConfig {
|
||||
base_url: format!(
|
||||
"http://{}",
|
||||
test_client.electrsd.esplora_url.as_ref().unwrap()
|
||||
),
|
||||
proxy: None,
|
||||
concurrency: None,
|
||||
stop_gap: stop_gap,
|
||||
timeout: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
EsploraTester.run();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,14 @@ impl GetTx for EsploraBlockchain {
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl GetBlockHash for EsploraBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
let block_header = await_or_block!(self.url_client._get_header(height as u32))?;
|
||||
Ok(block_header.block_hash())
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl WalletSync for EsploraBlockchain {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
|
||||
@@ -88,7 +88,7 @@ impl Blockchain for EsploraBlockchain {
|
||||
}
|
||||
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
|
||||
let _txid = self.url_client._broadcast(tx)?;
|
||||
self.url_client._broadcast(tx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -112,6 +112,13 @@ impl GetTx for EsploraBlockchain {
|
||||
}
|
||||
}
|
||||
|
||||
impl GetBlockHash for EsploraBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
let block_header = self.url_client._get_header(height as u32)?;
|
||||
Ok(block_header.block_hash())
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletSync for EsploraBlockchain {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
|
||||
@@ -21,7 +21,7 @@ use std::ops::Deref;
|
||||
use std::sync::mpsc::{channel, Receiver, Sender};
|
||||
use std::sync::Arc;
|
||||
|
||||
use bitcoin::{Transaction, Txid};
|
||||
use bitcoin::{BlockHash, Transaction, Txid};
|
||||
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::error::Error;
|
||||
@@ -87,7 +87,7 @@ pub enum Capability {
|
||||
|
||||
/// Trait that defines the actions that must be supported by a blockchain backend
|
||||
#[maybe_async]
|
||||
pub trait Blockchain: WalletSync + GetHeight + GetTx {
|
||||
pub trait Blockchain: WalletSync + GetHeight + GetTx + GetBlockHash {
|
||||
/// Return the set of [`Capability`] supported by this backend
|
||||
fn get_capabilities(&self) -> HashSet<Capability>;
|
||||
/// Broadcast a transaction
|
||||
@@ -110,6 +110,13 @@ pub trait GetTx {
|
||||
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
/// Trait for getting block hash by block height
|
||||
pub trait GetBlockHash {
|
||||
/// fetch block hash given its height
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error>;
|
||||
}
|
||||
|
||||
/// Trait for blockchains that can sync by updating the database directly.
|
||||
#[maybe_async]
|
||||
pub trait WalletSync {
|
||||
@@ -359,6 +366,13 @@ impl<T: GetHeight> GetHeight for Arc<T> {
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl<T: GetBlockHash> GetBlockHash for Arc<T> {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
maybe_await!(self.deref().get_block_hash(height))
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl<T: WalletSync> WalletSync for Arc<T> {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
|
||||
@@ -169,6 +169,12 @@ impl GetHeight for RpcBlockchain {
|
||||
}
|
||||
}
|
||||
|
||||
impl GetBlockHash for RpcBlockchain {
|
||||
fn get_block_hash(&self, height: u64) -> Result<BlockHash, Error> {
|
||||
Ok(self.client.get_block_hash(height)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl WalletSync for RpcBlockchain {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
@@ -334,7 +340,7 @@ impl WalletSync for RpcBlockchain {
|
||||
),
|
||||
received,
|
||||
sent,
|
||||
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
|
||||
fee: tx_result.fee.map(|f| f.as_sat().unsigned_abs()),
|
||||
};
|
||||
debug!(
|
||||
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
|
||||
|
||||
@@ -314,6 +314,22 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
||||
let finished_txs = make_txs_consistent(&self.finished_txs);
|
||||
let observed_txids: HashSet<Txid> = finished_txs.iter().map(|tx| tx.txid).collect();
|
||||
let txids_to_delete = existing_txids.difference(&observed_txids);
|
||||
|
||||
// Ensure `last_active_index` does not decrement database's current state.
|
||||
let index_updates = self
|
||||
.last_active_index
|
||||
.iter()
|
||||
.map(|(keychain, sync_index)| {
|
||||
let sync_index = *sync_index as u32;
|
||||
let index_res = match self.db.get_last_index(*keychain) {
|
||||
Ok(Some(db_index)) => Ok(std::cmp::max(db_index, sync_index)),
|
||||
Ok(None) => Ok(sync_index),
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
index_res.map(|index| (*keychain, index))
|
||||
})
|
||||
.collect::<Result<Vec<(KeychainKind, u32)>, _>>()?;
|
||||
|
||||
let mut batch = self.db.begin_batch();
|
||||
|
||||
// Delete old txs that no longer exist
|
||||
@@ -377,8 +393,10 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
||||
batch.set_tx(finished_tx)?;
|
||||
}
|
||||
|
||||
for (keychain, last_active_index) in self.last_active_index {
|
||||
batch.set_last_index(keychain, last_active_index as u32)?;
|
||||
// apply index updates
|
||||
for (keychain, new_index) in index_updates {
|
||||
debug!("updating index ({}, {})", keychain.as_byte(), new_index);
|
||||
batch.set_last_index(keychain, new_index)?;
|
||||
}
|
||||
|
||||
info!(
|
||||
|
||||
@@ -255,10 +255,6 @@ impl Database for AnyDatabase {
|
||||
fn increment_last_index(&mut self, keychain: KeychainKind) -> Result<u32, Error> {
|
||||
impl_inner_method!(AnyDatabase, self, increment_last_index, keychain)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
impl_inner_method!(AnyDatabase, self, flush)
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchOperations for AnyBatch {
|
||||
|
||||
@@ -166,16 +166,9 @@ macro_rules! impl_batch_operations {
|
||||
fn del_last_index(&mut self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||
let key = MapKey::LastIndex(keychain).as_map_key();
|
||||
let res = self.remove(key);
|
||||
let res = $process_delete!(res);
|
||||
|
||||
match res {
|
||||
None => Ok(None),
|
||||
Some(b) => {
|
||||
let array: [u8; 4] = b.as_ref().try_into().map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
|
||||
let val = u32::from_be_bytes(array);
|
||||
Ok(Some(val))
|
||||
}
|
||||
}
|
||||
$process_delete!(res)
|
||||
.map(ivec_to_u32)
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn del_sync_time(&mut self) -> Result<Option<SyncTime>, Error> {
|
||||
@@ -357,16 +350,7 @@ impl Database for Tree {
|
||||
|
||||
fn get_last_index(&self, keychain: KeychainKind) -> Result<Option<u32>, Error> {
|
||||
let key = MapKey::LastIndex(keychain).as_map_key();
|
||||
self.get(key)?
|
||||
.map(|b| -> Result<_, Error> {
|
||||
let array: [u8; 4] = b
|
||||
.as_ref()
|
||||
.try_into()
|
||||
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
|
||||
let val = u32::from_be_bytes(array);
|
||||
Ok(val)
|
||||
})
|
||||
.transpose()
|
||||
self.get(key)?.map(ivec_to_u32).transpose()
|
||||
}
|
||||
|
||||
fn get_sync_time(&self) -> Result<Option<SyncTime>, Error> {
|
||||
@@ -393,19 +377,17 @@ impl Database for Tree {
|
||||
|
||||
Some(new.to_be_bytes().to_vec())
|
||||
})?
|
||||
.map_or(Ok(0), |b| -> Result<_, Error> {
|
||||
let array: [u8; 4] = b
|
||||
.as_ref()
|
||||
.try_into()
|
||||
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
|
||||
let val = u32::from_be_bytes(array);
|
||||
Ok(val)
|
||||
})
|
||||
.map_or(Ok(0), ivec_to_u32)
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
Ok(Tree::flush(self).map(|_| ())?)
|
||||
}
|
||||
fn ivec_to_u32(b: sled::IVec) -> Result<u32, Error> {
|
||||
let array: [u8; 4] = b
|
||||
.as_ref()
|
||||
.try_into()
|
||||
.map_err(|_| Error::InvalidU32Bytes(b.to_vec()))?;
|
||||
let val = u32::from_be_bytes(array);
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
impl BatchDatabase for Tree {
|
||||
|
||||
@@ -449,10 +449,6 @@ impl Database for MemoryDatabase {
|
||||
|
||||
Ok(*value)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchDatabase for MemoryDatabase {
|
||||
@@ -486,15 +482,23 @@ impl ConfigurableDatabase for MemoryDatabase {
|
||||
/// don't have `test` set.
|
||||
macro_rules! populate_test_db {
|
||||
($db:expr, $tx_meta:expr, $current_height:expr$(,)?) => {{
|
||||
$crate::populate_test_db!($db, $tx_meta, $current_height, (@coinbase false))
|
||||
}};
|
||||
($db:expr, $tx_meta:expr, $current_height:expr, (@coinbase $is_coinbase:expr)$(,)?) => {{
|
||||
use std::str::FromStr;
|
||||
use $crate::database::BatchOperations;
|
||||
let mut db = $db;
|
||||
let tx_meta = $tx_meta;
|
||||
let current_height: Option<u32> = $current_height;
|
||||
let input = if $is_coinbase {
|
||||
vec![$crate::bitcoin::TxIn::default()]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let tx = $crate::bitcoin::Transaction {
|
||||
version: 1,
|
||||
lock_time: 0,
|
||||
input: vec![],
|
||||
input,
|
||||
output: tx_meta
|
||||
.output
|
||||
.iter()
|
||||
@@ -508,10 +512,13 @@ macro_rules! populate_test_db {
|
||||
};
|
||||
|
||||
let txid = tx.txid();
|
||||
let confirmation_time = tx_meta.min_confirmations.map(|conf| $crate::BlockTime {
|
||||
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
|
||||
timestamp: 0,
|
||||
});
|
||||
let confirmation_time = tx_meta
|
||||
.min_confirmations
|
||||
.and_then(|v| if v == 0 { None } else { Some(v) })
|
||||
.map(|conf| $crate::BlockTime {
|
||||
height: current_height.unwrap().checked_sub(conf as u32).unwrap() + 1,
|
||||
timestamp: 0,
|
||||
});
|
||||
|
||||
let tx_details = $crate::TransactionDetails {
|
||||
transaction: Some(tx.clone()),
|
||||
|
||||
@@ -158,13 +158,6 @@ pub trait Database: BatchOperations {
|
||||
///
|
||||
/// It should insert and return `0` if not present in the database
|
||||
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
|
||||
fn flush(&mut self) -> Result<(), Error>;
|
||||
}
|
||||
|
||||
/// Trait for a database that supports batch operations
|
||||
|
||||
@@ -891,10 +891,6 @@ impl Database for SqliteDatabase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl BatchDatabase for SqliteDatabase {
|
||||
|
||||
@@ -134,13 +134,13 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||
|
||||
let check_key = |pk: &DescriptorPublicKey| {
|
||||
let (pk, _, networks) = if self.0.is_witness() {
|
||||
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
desciptor_key.extract(secp)?
|
||||
descriptor_key.extract(secp)?
|
||||
} else {
|
||||
let desciptor_key: DescriptorKey<miniscript::Legacy> =
|
||||
let descriptor_key: DescriptorKey<miniscript::Legacy> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
desciptor_key.extract(secp)?
|
||||
descriptor_key.extract(secp)?
|
||||
};
|
||||
|
||||
if networks.contains(&network) {
|
||||
|
||||
@@ -40,18 +40,19 @@ pub type DescriptorTemplateOut = (ExtendedDescriptor, KeyMap, ValidNetworks);
|
||||
/// use bdk::keys::{IntoDescriptorKey, KeyError};
|
||||
/// use bdk::miniscript::Legacy;
|
||||
/// use bdk::template::{DescriptorTemplate, DescriptorTemplateOut};
|
||||
/// use bitcoin::Network;
|
||||
///
|
||||
/// struct MyP2PKH<K: IntoDescriptorKey<Legacy>>(K);
|
||||
///
|
||||
/// impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for MyP2PKH<K> {
|
||||
/// fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
/// fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
/// Ok(bdk::descriptor!(pkh(self.0))?)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait DescriptorTemplate {
|
||||
/// Build the complete descriptor
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError>;
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError>;
|
||||
}
|
||||
|
||||
/// Turns a [`DescriptorTemplate`] into a valid wallet descriptor by calling its
|
||||
@@ -62,7 +63,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
self.build()?.into_wallet_descriptor(secp, network)
|
||||
self.build(network)?.into_wallet_descriptor(secp, network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
descriptor!(pkh(self.0))
|
||||
}
|
||||
}
|
||||
@@ -130,7 +131,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
descriptor!(sh(wpkh(self.0)))
|
||||
}
|
||||
}
|
||||
@@ -164,12 +165,12 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
descriptor!(wpkh(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// BIP44 template. Expands to `pkh(key/44'/0'/0'/{0,1}/*)`
|
||||
/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||
///
|
||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||
///
|
||||
@@ -193,21 +194,21 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_private(44, self.0, self.1, network)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
/// BIP44 public template. Expands to `pkh(key/{0,1}/*)`
|
||||
///
|
||||
/// This assumes that the key used has already been derived with `m/44'/0'/0'`.
|
||||
/// This assumes that the key used has already been derived with `m/44'/0'/0'` for Mainnet or `m/44'/1'/0'` for Testnet.
|
||||
///
|
||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||
///
|
||||
@@ -240,12 +241,12 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
/// BIP49 template. Expands to `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
|
||||
/// BIP49 template. Expands to `sh(wpkh(key/49'/{0,1}'/0'/{0,1}/*))`
|
||||
///
|
||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||
///
|
||||
@@ -269,15 +270,15 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_private(49, self.0, self.1, network)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,18 +311,18 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49\'/0\'/0\']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/0'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
/// BIP84 template. Expands to `wpkh(key/84'/0'/0'/{0,1}/*)`
|
||||
/// BIP84 template. Expands to `wpkh(key/84'/{0,1}'/0'/{0,1}/*)`
|
||||
///
|
||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||
///
|
||||
@@ -345,15 +346,15 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_private(84, self.0, self.1, network)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,8 +393,8 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
fn build(self) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build()
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -406,10 +407,19 @@ macro_rules! expand_make_bipxx {
|
||||
bip: u32,
|
||||
key: K,
|
||||
keychain: KeychainKind,
|
||||
network: Network,
|
||||
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
||||
let mut derivation_path = Vec::with_capacity(4);
|
||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
|
||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
||||
|
||||
match network {
|
||||
Network::Bitcoin => {
|
||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
||||
}
|
||||
_ => {
|
||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(1)?);
|
||||
}
|
||||
}
|
||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(0)?);
|
||||
|
||||
match keychain {
|
||||
@@ -466,6 +476,40 @@ mod test {
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip44_template_cointype() {
|
||||
use bitcoin::util::bip32::ChildNumber::{self, Hardened};
|
||||
|
||||
let xprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
|
||||
assert_eq!(Network::Bitcoin, xprvkey.network);
|
||||
let xdesc = Bip44(xprvkey, KeychainKind::Internal)
|
||||
.build(Network::Bitcoin)
|
||||
.unwrap();
|
||||
|
||||
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
|
||||
let purpose = path.get(0).unwrap();
|
||||
assert!(matches!(purpose, Hardened { index: 44 }));
|
||||
let coin_type = path.get(1).unwrap();
|
||||
assert!(matches!(coin_type, Hardened { index: 0 }));
|
||||
}
|
||||
|
||||
let tprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
assert_eq!(Network::Testnet, tprvkey.network);
|
||||
let tdesc = Bip44(tprvkey, KeychainKind::Internal)
|
||||
.build(Network::Testnet)
|
||||
.unwrap();
|
||||
|
||||
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
|
||||
let purpose = path.get(0).unwrap();
|
||||
assert!(matches!(purpose, Hardened { index: 44 }));
|
||||
let coin_type = path.get(1).unwrap();
|
||||
assert!(matches!(coin_type, Hardened { index: 1 }));
|
||||
}
|
||||
}
|
||||
|
||||
// verify template descriptor generates expected address(es)
|
||||
fn check(
|
||||
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
|
||||
@@ -497,7 +541,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2Pkh(prvkey).build(),
|
||||
P2Pkh(prvkey).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
||||
@@ -508,7 +552,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2Pkh(pubkey).build(),
|
||||
P2Pkh(pubkey).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
||||
@@ -522,7 +566,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2Wpkh_P2Sh(prvkey).build(),
|
||||
P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
|
||||
true,
|
||||
true,
|
||||
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
||||
@@ -533,7 +577,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2Wpkh_P2Sh(pubkey).build(),
|
||||
P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
|
||||
true,
|
||||
true,
|
||||
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
||||
@@ -547,7 +591,7 @@ mod test {
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2Wpkh(prvkey).build(),
|
||||
P2Wpkh(prvkey).build(Network::Bitcoin),
|
||||
true,
|
||||
true,
|
||||
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
||||
@@ -558,7 +602,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2Wpkh(pubkey).build(),
|
||||
P2Wpkh(pubkey).build(Network::Bitcoin),
|
||||
true,
|
||||
true,
|
||||
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
|
||||
@@ -570,7 +614,7 @@ mod test {
|
||||
fn test_bip44_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip44(prvkey, KeychainKind::External).build(),
|
||||
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -580,7 +624,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip44(prvkey, KeychainKind::Internal).build(),
|
||||
Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -597,7 +641,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -607,7 +651,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
&[
|
||||
@@ -623,7 +667,7 @@ mod test {
|
||||
fn test_bip49_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip49(prvkey, KeychainKind::External).build(),
|
||||
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -633,7 +677,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip49(prvkey, KeychainKind::Internal).build(),
|
||||
Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -650,7 +694,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -660,7 +704,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -676,7 +720,7 @@ mod test {
|
||||
fn test_bip84_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip84(prvkey, KeychainKind::External).build(),
|
||||
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -686,7 +730,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip84(prvkey, KeychainKind::Internal).build(),
|
||||
Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -703,7 +747,7 @@ mod test {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(),
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
@@ -713,7 +757,7 @@ mod test {
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(),
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
&[
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
//! interact with the bitcoin P2P network.
|
||||
//!
|
||||
//! ```toml
|
||||
//! bdk = "0.18.0"
|
||||
//! bdk = "0.20.0"
|
||||
//! ```
|
||||
//!
|
||||
//! # Examples
|
||||
|
||||
@@ -372,6 +372,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
use $crate::blockchain::Blockchain;
|
||||
use $crate::database::MemoryDatabase;
|
||||
use $crate::types::KeychainKind;
|
||||
use $crate::wallet::AddressIndex;
|
||||
use $crate::{Wallet, FeeRate, SyncOptions};
|
||||
use $crate::testutils;
|
||||
|
||||
@@ -651,6 +652,60 @@ macro_rules! bdk_blockchain_tests {
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||
}
|
||||
|
||||
// Syncing wallet should not result in wallet address index to decrement.
|
||||
// This is critical as we should always ensure to not reuse addresses.
|
||||
#[test]
|
||||
fn test_sync_address_index_should_not_decrement() {
|
||||
let (wallet, blockchain, _descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
const ADDRS_TO_FUND: u32 = 7;
|
||||
const ADDRS_TO_IGNORE: u32 = 11;
|
||||
|
||||
let mut first_addr_index: u32 = 0;
|
||||
|
||||
(0..ADDRS_TO_FUND + ADDRS_TO_IGNORE).for_each(|i| {
|
||||
let new_addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||
|
||||
if i == 0 {
|
||||
first_addr_index = new_addr.index;
|
||||
}
|
||||
assert_eq!(new_addr.index, i+first_addr_index, "unexpected new address index (before sync)");
|
||||
|
||||
if i < ADDRS_TO_FUND {
|
||||
test_client.receive(testutils! {
|
||||
@tx ((@addr new_addr.address) => 50_000)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
let new_addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||
assert_eq!(new_addr.index, ADDRS_TO_FUND+ADDRS_TO_IGNORE+first_addr_index, "unexpected new address index (after sync)");
|
||||
}
|
||||
|
||||
// Even if user does not explicitly grab new addresses, the address index should
|
||||
// increment after sync (if wallet has a balance).
|
||||
#[test]
|
||||
fn test_sync_address_index_should_increment() {
|
||||
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
const START_FUND: u32 = 4;
|
||||
const END_FUND: u32 = 20;
|
||||
|
||||
// "secretly" fund wallet via given range
|
||||
(START_FUND..END_FUND).for_each(|addr_index| {
|
||||
test_client.receive(testutils! {
|
||||
@tx ((@external descriptors, addr_index) => 50_000)
|
||||
});
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
let address = wallet.get_address(AddressIndex::New).unwrap();
|
||||
assert_eq!(address.index, END_FUND, "unexpected new address index (after sync)");
|
||||
}
|
||||
|
||||
/// Send two conflicting transactions to the same address twice in a row.
|
||||
/// The coins should only be received once!
|
||||
#[test]
|
||||
@@ -1361,6 +1416,35 @@ macro_rules! bdk_blockchain_tests {
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert_eq!(finalized, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_block_hash() {
|
||||
use bitcoincore_rpc::{ RpcApi };
|
||||
use crate::blockchain::GetBlockHash;
|
||||
|
||||
// create wallet with init_wallet
|
||||
let (_, blockchain, _descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
let height = test_client.bitcoind.client.get_blockchain_info().unwrap().blocks as u64;
|
||||
let best_hash = test_client.bitcoind.client.get_best_block_hash().unwrap();
|
||||
|
||||
// use get_block_hash to get best block hash and compare with best_hash above
|
||||
let block_hash = blockchain.get_block_hash(height).unwrap();
|
||||
assert_eq!(best_hash, block_hash);
|
||||
|
||||
// generate blocks to address
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
test_client.generate(10, Some(node_addr));
|
||||
|
||||
let height = test_client.bitcoind.client.get_blockchain_info().unwrap().blocks as u64;
|
||||
let best_hash = test_client.bitcoind.client.get_best_block_hash().unwrap();
|
||||
|
||||
let block_hash = blockchain.get_block_hash(height).unwrap();
|
||||
assert_eq!(best_hash, block_hash);
|
||||
|
||||
// try to get hash for block that has not yet been created.
|
||||
assert!(blockchain.get_block_hash(height + 1).is_err());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
140
src/testutils/configurable_blockchain_tests.rs
Normal file
140
src/testutils/configurable_blockchain_tests.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use bitcoin::Network;
|
||||
|
||||
use crate::{
|
||||
blockchain::ConfigurableBlockchain, database::MemoryDatabase, testutils, wallet::AddressIndex,
|
||||
Wallet,
|
||||
};
|
||||
|
||||
use super::blockchain_tests::TestClient;
|
||||
|
||||
/// Trait for testing [`ConfigurableBlockchain`] implementations.
|
||||
pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized {
|
||||
/// Blockchain name for logging.
|
||||
const BLOCKCHAIN_NAME: &'static str;
|
||||
|
||||
/// Generates a blockchain config with a given stop_gap.
|
||||
///
|
||||
/// If this returns [`Option::None`], then the associated tests will not run.
|
||||
fn config_with_stop_gap(
|
||||
&self,
|
||||
_test_client: &mut TestClient,
|
||||
_stop_gap: usize,
|
||||
) -> Option<B::Config> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Runs all avaliable tests.
|
||||
fn run(&self) {
|
||||
let test_client = &mut TestClient::default();
|
||||
|
||||
if self.config_with_stop_gap(test_client, 0).is_some() {
|
||||
test_wallet_sync_with_stop_gaps(test_client, self);
|
||||
} else {
|
||||
println!(
|
||||
"{}: Skipped tests requiring config_with_stop_gap.",
|
||||
Self::BLOCKCHAIN_NAME
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Test whether blockchain implementation syncs with expected behaviour given different `stop_gap`
|
||||
/// parameters.
|
||||
///
|
||||
/// For each test vector:
|
||||
/// * Fill wallet's derived addresses with balances (as specified by test vector).
|
||||
/// * [0..addrs_before] => 1000sats for each address
|
||||
/// * [addrs_before..actual_gap] => empty addresses
|
||||
/// * [actual_gap..addrs_after] => 1000sats for each address
|
||||
/// * Then, perform wallet sync and obtain wallet balance
|
||||
/// * Check balance is within expected range (we can compare `stop_gap` and `actual_gap` to
|
||||
/// determine this).
|
||||
fn test_wallet_sync_with_stop_gaps<T, B>(test_client: &mut TestClient, tester: &T)
|
||||
where
|
||||
T: ConfigurableBlockchainTester<B>,
|
||||
B: ConfigurableBlockchain,
|
||||
{
|
||||
// Generates wallet descriptor
|
||||
let descriptor_of_account = |account_index: usize| -> String {
|
||||
format!("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/{account_index}/*)")
|
||||
};
|
||||
|
||||
// Amount (in satoshis) provided to a single address (which expects to have a balance)
|
||||
const AMOUNT_PER_TX: u64 = 1000;
|
||||
|
||||
// [stop_gap, actual_gap, addrs_before, addrs_after]
|
||||
//
|
||||
// [0] stop_gap: Passed to [`ElectrumBlockchainConfig`]
|
||||
// [1] actual_gap: Range size of address indexes without a balance
|
||||
// [2] addrs_before: Range size of address indexes (before gap) which contains a balance
|
||||
// [3] addrs_after: Range size of address indexes (after gap) which contains a balance
|
||||
let test_vectors: Vec<[u64; 4]> = vec![
|
||||
[0, 0, 0, 5],
|
||||
[0, 0, 5, 5],
|
||||
[0, 1, 5, 5],
|
||||
[0, 2, 5, 5],
|
||||
[1, 0, 5, 5],
|
||||
[1, 1, 5, 5],
|
||||
[1, 2, 5, 5],
|
||||
[2, 1, 5, 5],
|
||||
[2, 2, 5, 5],
|
||||
[2, 3, 5, 5],
|
||||
];
|
||||
|
||||
for (account_index, vector) in test_vectors.into_iter().enumerate() {
|
||||
let [stop_gap, actual_gap, addrs_before, addrs_after] = vector;
|
||||
let descriptor = descriptor_of_account(account_index);
|
||||
|
||||
let blockchain = B::from_config(
|
||||
&tester
|
||||
.config_with_stop_gap(test_client, stop_gap as _)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let wallet =
|
||||
Wallet::new(&descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||
|
||||
// fill server-side with txs to specified address indexes
|
||||
// return the max balance of the wallet (also the actual balance)
|
||||
let max_balance = (0..addrs_before)
|
||||
.chain(addrs_before + actual_gap..addrs_before + actual_gap + addrs_after)
|
||||
.fold(0_u64, |sum, i| {
|
||||
let address = wallet.get_address(AddressIndex::Peek(i as _)).unwrap();
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@addr address.address) => AMOUNT_PER_TX )
|
||||
});
|
||||
sum + AMOUNT_PER_TX
|
||||
});
|
||||
|
||||
// minimum allowed balance of wallet (based on stop gap)
|
||||
let min_balance = if actual_gap > stop_gap {
|
||||
addrs_before * AMOUNT_PER_TX
|
||||
} else {
|
||||
max_balance
|
||||
};
|
||||
|
||||
// perform wallet sync
|
||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
|
||||
let wallet_balance = wallet.get_balance().unwrap();
|
||||
|
||||
let details = format!(
|
||||
"test_vector: [stop_gap: {}, actual_gap: {}, addrs_before: {}, addrs_after: {}]",
|
||||
stop_gap, actual_gap, addrs_before, addrs_after,
|
||||
);
|
||||
assert!(
|
||||
wallet_balance <= max_balance,
|
||||
"wallet balance is greater than received amount: {}",
|
||||
details
|
||||
);
|
||||
assert!(
|
||||
wallet_balance >= min_balance,
|
||||
"wallet balance is smaller than expected: {}",
|
||||
details
|
||||
);
|
||||
|
||||
// generate block to confirm new transactions
|
||||
test_client.generate(1, None);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@
|
||||
#[cfg(feature = "test-blockchains")]
|
||||
pub mod blockchain_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(feature = "test-blockchains")]
|
||||
pub mod configurable_blockchain_tests;
|
||||
|
||||
use bitcoin::{Address, Txid};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -202,8 +202,10 @@ pub struct TransactionDetails {
|
||||
pub txid: Txid,
|
||||
|
||||
/// Received value (sats)
|
||||
/// Sum of owned outputs of this transaction.
|
||||
pub received: u64,
|
||||
/// Sent value (sats)
|
||||
/// Sum of owned inputs of this transaction.
|
||||
pub sent: u64,
|
||||
/// Fee value (sats) if available.
|
||||
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
||||
|
||||
@@ -100,6 +100,7 @@ impl std::error::Error for AddressValidatorError {}
|
||||
/// validator will be propagated up to the original caller that triggered the address generation.
|
||||
///
|
||||
/// For a usage example see [this module](crate::address_validator)'s documentation.
|
||||
#[deprecated = "AddressValidator was rarely used. Address validation can occur outside of BDK"]
|
||||
pub trait AddressValidator: Send + Sync + fmt::Debug {
|
||||
/// Validate or inspect an address
|
||||
fn validate(
|
||||
@@ -120,6 +121,7 @@ mod test {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestValidator;
|
||||
#[allow(deprecated)]
|
||||
impl AddressValidator for TestValidator {
|
||||
fn validate(
|
||||
&self,
|
||||
@@ -135,6 +137,7 @@ mod test {
|
||||
#[should_panic(expected = "InvalidScript")]
|
||||
fn test_address_validator_external() {
|
||||
let (mut wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
#[allow(deprecated)]
|
||||
wallet.add_address_validator(Arc::new(TestValidator));
|
||||
|
||||
wallet.get_address(New).unwrap();
|
||||
@@ -144,6 +147,7 @@ mod test {
|
||||
#[should_panic(expected = "InvalidScript")]
|
||||
fn test_address_validator_internal() {
|
||||
let (mut wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
||||
#[allow(deprecated)]
|
||||
wallet.add_address_validator(Arc::new(TestValidator));
|
||||
|
||||
let addr = crate::testutils!(@external descriptors, 10);
|
||||
|
||||
@@ -50,6 +50,7 @@ pub mod verify;
|
||||
|
||||
pub use utils::IsDust;
|
||||
|
||||
#[allow(deprecated)]
|
||||
use address_validator::AddressValidator;
|
||||
use coin_selection::DefaultCoinSelectionAlgorithm;
|
||||
use signer::{SignOptions, SignerOrdering, SignersContainer, TransactionSigner};
|
||||
@@ -73,6 +74,7 @@ use crate::testutils;
|
||||
use crate::types::*;
|
||||
|
||||
const CACHE_ADDR_BATCH_SIZE: u32 = 100;
|
||||
const COINBASE_MATURITY: u32 = 100;
|
||||
|
||||
/// A Bitcoin wallet
|
||||
///
|
||||
@@ -93,6 +95,7 @@ pub struct Wallet<D> {
|
||||
signers: Arc<SignersContainer>,
|
||||
change_signers: Arc<SignersContainer>,
|
||||
|
||||
#[allow(deprecated)]
|
||||
address_validators: Vec<Arc<dyn AddressValidator>>,
|
||||
|
||||
network: Network,
|
||||
@@ -499,11 +502,17 @@ where
|
||||
/// Add an address validator
|
||||
///
|
||||
/// See [the `address_validator` module](address_validator) for an example.
|
||||
#[deprecated]
|
||||
#[allow(deprecated)]
|
||||
pub fn add_address_validator(&mut self, validator: Arc<dyn AddressValidator>) {
|
||||
self.address_validators.push(validator);
|
||||
}
|
||||
|
||||
/// Get the address validators
|
||||
///
|
||||
/// See [the `address_validator` module](address_validator).
|
||||
#[deprecated]
|
||||
#[allow(deprecated)]
|
||||
pub fn get_address_validators(&self) -> &[Arc<dyn AddressValidator>] {
|
||||
&self.address_validators
|
||||
}
|
||||
@@ -619,9 +628,27 @@ where
|
||||
_ => 1,
|
||||
};
|
||||
|
||||
// We use a match here instead of a map_or_else as it's way more readable :)
|
||||
let current_height = match params.current_height {
|
||||
// If they didn't tell us the current height, we assume it's the latest sync height.
|
||||
None => self
|
||||
.database()
|
||||
.get_sync_time()?
|
||||
.map(|sync_time| sync_time.block_time.height),
|
||||
h => h,
|
||||
};
|
||||
|
||||
let lock_time = match params.locktime {
|
||||
// No nLockTime, default to 0
|
||||
None => requirements.timelock.unwrap_or(0),
|
||||
// When no nLockTime is specified, we try to prevent fee sniping, if possible
|
||||
None => {
|
||||
// Fee sniping can be partially prevented by setting the timelock
|
||||
// to current_height. If we don't know the current_height,
|
||||
// we default to 0.
|
||||
let fee_sniping_height = current_height.unwrap_or(0);
|
||||
// We choose the biggest between the required nlocktime and the fee sniping
|
||||
// height
|
||||
std::cmp::max(requirements.timelock.unwrap_or(0), fee_sniping_height)
|
||||
}
|
||||
// Specific nLockTime required and we have no constraints, so just set to that value
|
||||
Some(x) if requirements.timelock.is_none() => x,
|
||||
// Specific nLockTime required and it's compatible with the constraints
|
||||
@@ -747,6 +774,7 @@ where
|
||||
params.drain_wallet,
|
||||
params.manually_selected_only,
|
||||
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
||||
current_height,
|
||||
)?;
|
||||
|
||||
let coin_selection = coin_selection.coin_select(
|
||||
@@ -791,7 +819,14 @@ where
|
||||
let drain_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
|
||||
|
||||
if tx.output.is_empty() {
|
||||
if params.drain_to.is_some() {
|
||||
// Uh oh, our transaction has no outputs.
|
||||
// We allow this when:
|
||||
// - We have a drain_to address and the utxos we must spend (this happens,
|
||||
// for example, when we RBF)
|
||||
// - We have a drain_to address and drain_wallet set
|
||||
// Otherwise, we don't know who we should send the funds to, and how much
|
||||
// we should send!
|
||||
if params.drain_to.is_some() && (params.drain_wallet || !params.utxos.is_empty()) {
|
||||
if drain_val.is_dust(&drain_output.script_pubkey) {
|
||||
return Err(Error::InsufficientFunds {
|
||||
needed: drain_output.script_pubkey.dust_value().as_sat(),
|
||||
@@ -872,8 +907,6 @@ where
|
||||
/// # Ok::<(), bdk::Error>(())
|
||||
/// ```
|
||||
// TODO: support for merging multiple transactions while bumping the fees
|
||||
// TODO: option to force addition of an extra output? seems bad for privacy to update the
|
||||
// change
|
||||
pub fn build_fee_bump(
|
||||
&self,
|
||||
txid: Txid,
|
||||
@@ -1050,7 +1083,11 @@ where
|
||||
}
|
||||
|
||||
// attempt to finalize
|
||||
self.finalize_psbt(psbt, sign_options)
|
||||
if sign_options.try_finalize {
|
||||
self.finalize_psbt(psbt, sign_options)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the spending policies for the wallet's descriptor
|
||||
@@ -1161,6 +1198,9 @@ where
|
||||
let psbt_input = &mut psbt.inputs[n];
|
||||
psbt_input.final_script_sig = Some(tmp_input.script_sig);
|
||||
psbt_input.final_script_witness = Some(tmp_input.witness);
|
||||
if sign_options.remove_partial_sigs {
|
||||
psbt_input.partial_sigs.clear();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("satisfy error {:?} for input {}", e, n);
|
||||
@@ -1235,6 +1275,7 @@ where
|
||||
let script = derived_descriptor.script_pubkey();
|
||||
|
||||
for validator in &self.address_validators {
|
||||
#[allow(deprecated)]
|
||||
validator.validate(keychain, &hd_keypaths, &script)?;
|
||||
}
|
||||
|
||||
@@ -1317,6 +1358,7 @@ where
|
||||
/// Given the options returns the list of utxos that must be used to form the
|
||||
/// transaction and any further that may be used if needed.
|
||||
#[allow(clippy::type_complexity)]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn preselect_utxos(
|
||||
&self,
|
||||
change_policy: tx_builder::ChangeSpendPolicy,
|
||||
@@ -1325,6 +1367,7 @@ where
|
||||
must_use_all_available: bool,
|
||||
manual_only: bool,
|
||||
must_only_use_confirmed_tx: bool,
|
||||
current_height: Option<u32>,
|
||||
) -> Result<(Vec<WeightedUtxo>, Vec<WeightedUtxo>), Error> {
|
||||
// must_spend <- manually selected utxos
|
||||
// may_spend <- all other available utxos
|
||||
@@ -1343,23 +1386,44 @@ where
|
||||
return Ok((must_spend, vec![]));
|
||||
}
|
||||
|
||||
let satisfies_confirmed = match must_only_use_confirmed_tx {
|
||||
true => {
|
||||
let database = self.database.borrow();
|
||||
may_spend
|
||||
.iter()
|
||||
.map(|u| {
|
||||
database
|
||||
.get_tx(&u.0.outpoint.txid, true)
|
||||
.map(|tx| match tx {
|
||||
None => false,
|
||||
Some(tx) => tx.confirmation_time.is_some(),
|
||||
})
|
||||
let database = self.database.borrow();
|
||||
let satisfies_confirmed = may_spend
|
||||
.iter()
|
||||
.map(|u| {
|
||||
database
|
||||
.get_tx(&u.0.outpoint.txid, true)
|
||||
.map(|tx| match tx {
|
||||
// We don't have the tx in the db for some reason,
|
||||
// so we can't know for sure if it's mature or not.
|
||||
// We prefer not to spend it.
|
||||
None => false,
|
||||
Some(tx) => {
|
||||
// Whether the UTXO is mature and, if needed, confirmed
|
||||
let mut spendable = true;
|
||||
if must_only_use_confirmed_tx && tx.confirmation_time.is_none() {
|
||||
return false;
|
||||
}
|
||||
if tx
|
||||
.transaction
|
||||
.expect("We specifically ask for the transaction above")
|
||||
.is_coin_base()
|
||||
{
|
||||
if let Some(current_height) = current_height {
|
||||
match &tx.confirmation_time {
|
||||
Some(t) => {
|
||||
// https://github.com/bitcoin/bitcoin/blob/c5e67be03bb06a5d7885c55db1f016fbf2333fe3/src/validation.cpp#L373-L375
|
||||
spendable &= (current_height.saturating_sub(t.height))
|
||||
>= COINBASE_MATURITY;
|
||||
}
|
||||
None => spendable = false,
|
||||
}
|
||||
}
|
||||
}
|
||||
spendable
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
}
|
||||
false => vec![true; may_spend.len()],
|
||||
};
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let mut i = 0;
|
||||
may_spend.retain(|u| {
|
||||
@@ -1980,9 +2044,59 @@ pub(crate) mod test {
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let (psbt, _) = builder.finish().unwrap();
|
||||
|
||||
// Since we never synced the wallet we don't have a last_sync_height
|
||||
// we could use to try to prevent fee sniping. We default to 0.
|
||||
assert_eq!(psbt.unsigned_tx.lock_time, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_tx_fee_sniping_locktime_provided_height() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let sync_time = SyncTime {
|
||||
block_time: BlockTime {
|
||||
height: 24,
|
||||
timestamp: 0,
|
||||
},
|
||||
};
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_sync_time(sync_time)
|
||||
.unwrap();
|
||||
let current_height = 25;
|
||||
builder.current_height(current_height);
|
||||
let (psbt, _) = builder.finish().unwrap();
|
||||
|
||||
// current_height will override the last sync height
|
||||
assert_eq!(psbt.unsigned_tx.lock_time, current_height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_tx_fee_sniping_locktime_last_sync() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let sync_time = SyncTime {
|
||||
block_time: BlockTime {
|
||||
height: 25,
|
||||
timestamp: 0,
|
||||
},
|
||||
};
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_sync_time(sync_time.clone())
|
||||
.unwrap();
|
||||
let (psbt, _) = builder.finish().unwrap();
|
||||
|
||||
// If there's no current_height we're left with using the last sync height
|
||||
assert_eq!(psbt.unsigned_tx.lock_time, sync_time.block_time.height);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_tx_default_locktime_cltv() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_single_sig_cltv());
|
||||
@@ -2001,9 +2115,13 @@ pub(crate) mod test {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), 25_000)
|
||||
.current_height(630_001)
|
||||
.nlocktime(630_000);
|
||||
let (psbt, _) = builder.finish().unwrap();
|
||||
|
||||
// When we explicitly specify a nlocktime
|
||||
// we don't try any fee sniping prevention trick
|
||||
// (we ignore the current_height)
|
||||
assert_eq!(psbt.unsigned_tx.lock_time, 630_000);
|
||||
}
|
||||
|
||||
@@ -2160,7 +2278,6 @@ pub(crate) mod test {
|
||||
.drain_to(drain_addr.script_pubkey())
|
||||
.drain_wallet();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
dbg!(&psbt);
|
||||
let outputs = psbt.unsigned_tx.output;
|
||||
|
||||
assert_eq!(outputs.len(), 2);
|
||||
@@ -2176,6 +2293,40 @@ pub(crate) mod test {
|
||||
assert_eq!(drain_output.value, 30_000 - details.fee.unwrap_or(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_tx_drain_to_and_utxos() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let utxos: Vec<_> = wallet
|
||||
.get_available_utxos()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(u, _)| u.outpoint)
|
||||
.collect();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.drain_to(addr.script_pubkey())
|
||||
.add_utxos(&utxos)
|
||||
.unwrap();
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
|
||||
assert_eq!(psbt.unsigned_tx.output.len(), 1);
|
||||
assert_eq!(
|
||||
psbt.unsigned_tx.output[0].value,
|
||||
50_000 - details.fee.unwrap_or(0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "NoRecipients")]
|
||||
fn test_create_tx_drain_to_no_drain_wallet_no_utxos() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
let drain_addr = wallet.get_address(New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(drain_addr.script_pubkey());
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_tx_default_fee_rate() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
|
||||
@@ -3723,6 +3874,99 @@ pub(crate) mod test {
|
||||
assert_eq!(details.fee.unwrap_or(0), 250);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InsufficientFunds")]
|
||||
fn test_bump_fee_unconfirmed_inputs_only() {
|
||||
// We try to bump the fee, but:
|
||||
// - We can't reduce the change, as we have no change
|
||||
// - All our UTXOs are unconfirmed
|
||||
// So, we fail with "InsufficientFunds", as per RBF rule 2:
|
||||
// The replacement transaction may only include an unconfirmed input
|
||||
// if that input was included in one of the original transactions.
|
||||
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.drain_wallet()
|
||||
.drain_to(addr.script_pubkey())
|
||||
.enable_rbf();
|
||||
let (psbt, mut original_details) = builder.finish().unwrap();
|
||||
// Now we receive one transaction with 0 confirmations. We won't be able to use that for
|
||||
// fee bumping, as it's still unconfirmed!
|
||||
crate::populate_test_db!(
|
||||
wallet.database.borrow_mut(),
|
||||
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 0)),
|
||||
Some(100),
|
||||
);
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.del_utxo(&txin.previous_output)
|
||||
.unwrap();
|
||||
}
|
||||
original_details.transaction = Some(tx);
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_tx(&original_details)
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(25.0));
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bump_fee_unconfirmed_input() {
|
||||
// We create a tx draining the wallet and spending one confirmed
|
||||
// and one unconfirmed UTXO. We check that we can fee bump normally
|
||||
// (BIP125 rule 2 only apply to newly added unconfirmed input, you can
|
||||
// always fee bump with an unconfirmed input if it was included in the
|
||||
// original transaction)
|
||||
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
// We receive a tx with 0 confirmations, which will be used as an input
|
||||
// in the drain tx.
|
||||
crate::populate_test_db!(
|
||||
wallet.database.borrow_mut(),
|
||||
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 0)),
|
||||
Some(100),
|
||||
);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.drain_wallet()
|
||||
.drain_to(addr.script_pubkey())
|
||||
.enable_rbf();
|
||||
let (psbt, mut original_details) = builder.finish().unwrap();
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.del_utxo(&txin.previous_output)
|
||||
.unwrap();
|
||||
}
|
||||
original_details.transaction = Some(tx);
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_tx(&original_details)
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder
|
||||
.fee_rate(FeeRate::from_sat_per_vb(15.0))
|
||||
.allow_shrinking(addr.script_pubkey())
|
||||
.unwrap();
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_single_xprv() {
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
@@ -3878,6 +4122,70 @@ pub(crate) mod test {
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_remove_partial_sigs_after_finalize_sign_option() {
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
|
||||
for remove_partial_sigs in &[true, false] {
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
let mut psbt = builder.finish().unwrap().0;
|
||||
|
||||
assert!(wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
remove_partial_sigs: *remove_partial_sigs,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap());
|
||||
|
||||
psbt.inputs.iter().for_each(|input| {
|
||||
if *remove_partial_sigs {
|
||||
assert!(input.partial_sigs.is_empty())
|
||||
} else {
|
||||
assert!(!input.partial_sigs.is_empty())
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_finalize_sign_option() {
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
|
||||
for try_finalize in &[true, false] {
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
let mut psbt = builder.finish().unwrap().0;
|
||||
|
||||
let finalized = wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
try_finalize: *try_finalize,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
psbt.inputs.iter().for_each(|input| {
|
||||
if *try_finalize {
|
||||
assert!(finalized);
|
||||
assert!(input.final_script_sig.is_some());
|
||||
assert!(input.final_script_witness.is_some());
|
||||
} else {
|
||||
assert!(!finalized);
|
||||
assert!(input.final_script_sig.is_none());
|
||||
assert!(input.final_script_witness.is_none());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_nonstandard_sighash() {
|
||||
let sighash = EcdsaSighashType::NonePlusAnyoneCanPay;
|
||||
@@ -4158,7 +4466,7 @@ pub(crate) mod test {
|
||||
wallet.get_address(AddressIndex::New).unwrap(),
|
||||
AddressInfo {
|
||||
index: 0,
|
||||
address: Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap(),
|
||||
address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w").unwrap(),
|
||||
keychain: KeychainKind::External,
|
||||
}
|
||||
);
|
||||
@@ -4167,7 +4475,7 @@ pub(crate) mod test {
|
||||
wallet.get_internal_address(AddressIndex::New).unwrap(),
|
||||
AddressInfo {
|
||||
index: 0,
|
||||
address: Address::from_str("bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa").unwrap(),
|
||||
address: Address::from_str("bcrt1q0ue3s5y935tw7v3gmnh36c5zzsaw4n9c9smq79").unwrap(),
|
||||
keychain: KeychainKind::Internal,
|
||||
}
|
||||
);
|
||||
@@ -4184,7 +4492,7 @@ pub(crate) mod test {
|
||||
wallet.get_internal_address(AddressIndex::New).unwrap(),
|
||||
AddressInfo {
|
||||
index: 0,
|
||||
address: Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap(),
|
||||
address: Address::from_str("bcrt1qrhgaqu0zvf5q2d0gwwz04w0dh0cuehhqvzpp4w").unwrap(),
|
||||
keychain: KeychainKind::Internal,
|
||||
},
|
||||
"when there's no internal descriptor it should just use external"
|
||||
@@ -4571,4 +4879,70 @@ pub(crate) mod test {
|
||||
"The signature should have been made with the right sighash"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spend_coinbase() {
|
||||
let descriptors = testutils!(@descriptors (get_test_wpkh()));
|
||||
let wallet = Wallet::new(
|
||||
&descriptors.0,
|
||||
None,
|
||||
Network::Regtest,
|
||||
AnyDatabase::Memory(MemoryDatabase::new()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let confirmation_time = 5;
|
||||
|
||||
crate::populate_test_db!(
|
||||
wallet.database.borrow_mut(),
|
||||
testutils! (@tx ( (@external descriptors, 0) => 25_000 ) (@confirmations 1)),
|
||||
Some(confirmation_time),
|
||||
(@coinbase true)
|
||||
);
|
||||
|
||||
let not_yet_mature_time = confirmation_time + COINBASE_MATURITY - 1;
|
||||
let maturity_time = confirmation_time + COINBASE_MATURITY;
|
||||
|
||||
// The balance is nonzero, even if we can't spend anything
|
||||
// FIXME: we should differentiate the balance between immature,
|
||||
// trusted, untrusted_pending
|
||||
// See https://github.com/bitcoindevkit/bdk/issues/238
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
assert!(balance != 0);
|
||||
|
||||
// We try to create a transaction, only to notice that all
|
||||
// our funds are unspendable
|
||||
let addr = Address::from_str("2N1Ffz3WaNzbeLFBb51xyFMHYSEUXcbiSoX").unwrap();
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), balance / 2)
|
||||
.current_height(confirmation_time);
|
||||
assert!(matches!(
|
||||
builder.finish().unwrap_err(),
|
||||
Error::InsufficientFunds {
|
||||
needed: _,
|
||||
available: 0
|
||||
}
|
||||
));
|
||||
|
||||
// Still unspendable...
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), balance / 2)
|
||||
.current_height(not_yet_mature_time);
|
||||
assert!(matches!(
|
||||
builder.finish().unwrap_err(),
|
||||
Error::InsufficientFunds {
|
||||
needed: _,
|
||||
available: 0
|
||||
}
|
||||
));
|
||||
|
||||
// ...Now the coinbase is mature :)
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), balance / 2)
|
||||
.current_height(maturity_time);
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,6 +667,16 @@ pub struct SignOptions {
|
||||
///
|
||||
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
|
||||
pub allow_all_sighashes: bool,
|
||||
|
||||
/// Whether to remove partial_sigs from psbt inputs while finalizing psbt.
|
||||
///
|
||||
/// Defaults to `true` which will remove partial_sigs after finalizing.
|
||||
pub remove_partial_sigs: bool,
|
||||
|
||||
/// Whether to try finalizing psbt input after the inputs are signed.
|
||||
///
|
||||
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
|
||||
pub try_finalize: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
@@ -676,6 +686,8 @@ impl Default for SignOptions {
|
||||
trust_witness_utxo: false,
|
||||
assume_height: None,
|
||||
allow_all_sighashes: false,
|
||||
remove_partial_sigs: true,
|
||||
try_finalize: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,6 +147,7 @@ pub(crate) struct TxParams {
|
||||
pub(crate) add_global_xpubs: bool,
|
||||
pub(crate) include_output_redeem_witness_script: bool,
|
||||
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||
pub(crate) current_height: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -543,6 +544,22 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
self.params.rbf = Some(RbfValue::Value(nsequence));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the current blockchain height.
|
||||
///
|
||||
/// This will be used to:
|
||||
/// 1. Set the nLockTime for preventing fee sniping.
|
||||
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
|
||||
/// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
|
||||
/// mature at `current_height`, we ignore them in the coin selection.
|
||||
/// If you want to create a transaction that spends immature coinbase inputs, manually
|
||||
/// add them using [`TxBuilder::add_utxos`].
|
||||
///
|
||||
/// In both cases, if you don't provide a current height, we use the last sync height.
|
||||
pub fn current_height(&mut self, height: u32) -> &mut Self {
|
||||
self.params.current_height = Some(height);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
@@ -574,6 +591,9 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
|
||||
/// difference is that it is valid to use `drain_to` without setting any ordinary recipients
|
||||
/// with [`add_recipient`] (but it is perfectly fine to add recipients as well).
|
||||
///
|
||||
/// If you choose not to set any recipients, you should either provide the utxos that the
|
||||
/// transaction should spend via [`add_utxos`], or set [`drain_wallet`] to spend all of them.
|
||||
///
|
||||
/// When bumping the fees of a transaction made with this option, you probably want to
|
||||
/// use [`allow_shrinking`] to allow this output to be reduced to pay for the extra fees.
|
||||
///
|
||||
@@ -604,6 +624,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
|
||||
///
|
||||
/// [`allow_shrinking`]: Self::allow_shrinking
|
||||
/// [`add_recipient`]: Self::add_recipient
|
||||
/// [`add_utxos`]: Self::add_utxos
|
||||
/// [`drain_wallet`]: Self::drain_wallet
|
||||
pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
|
||||
Reference in New Issue
Block a user