Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5493379806 | ||
|
|
9be63e66ec | ||
|
|
2db881519a | ||
|
|
74e2c477f1 | ||
|
|
134b19a9cb | ||
|
|
2c01b6118f | ||
|
|
03d3c786f2 | ||
|
|
0f03831274 | ||
|
|
dc7adb7161 | ||
|
|
5eeba6cced | ||
|
|
5eb74af414 | ||
|
|
ac19c19f21 | ||
|
|
ef03da0a76 | ||
|
|
9d85c9667f | ||
|
|
85bd126c6c | ||
|
|
7fdacdbad4 | ||
|
|
9c0a769675 | ||
|
|
11865fddff | ||
|
|
e8df3d2d91 | ||
|
|
a63c51f35d | ||
|
|
1730e0150f | ||
|
|
5a415979af | ||
|
|
a713a5a062 | ||
|
|
419dc248b6 | ||
|
|
632dabaa07 | ||
|
|
2756411ef7 | ||
|
|
50af51da5a | ||
|
|
ae919061e2 | ||
|
|
7ac87b8f99 | ||
|
|
ac051d7ae9 | ||
|
|
00d426b885 | ||
|
|
42fde6d457 | ||
|
|
8e0d00a3ea | ||
|
|
235011feef | ||
|
|
a1477405d1 | ||
|
|
558e37afa7 | ||
|
|
6bae52e6f2 | ||
|
|
32ae95f463 | ||
|
|
3644a452c1 | ||
|
|
5c940c33cb | ||
|
|
277e18f5cb | ||
|
|
8d3b2a9581 | ||
|
|
45a4ae5828 | ||
|
|
6db5b4a094 | ||
|
|
9d2024434e | ||
|
|
9165faef95 | ||
|
|
46c344feb0 | ||
|
|
2c02a44586 |
17
CHANGELOG.md
17
CHANGELOG.md
@@ -6,6 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.21.0] - [v0.20.0]
|
||||
|
||||
- Add `descriptor::checksum::get_checksum_bytes` method.
|
||||
- Add `Excess` enum to handle remaining amount after coin selection.
|
||||
- Move change creation from `Wallet::create_tx` to `CoinSelectionAlgorithm::coin_select`.
|
||||
- Change the interface of `SqliteDatabase::new` to accept any type that implement AsRef<Path>
|
||||
- Add the ability to specify which leaves to sign in a taproot transaction through `TapLeavesOptions` in `SignOptions`
|
||||
- Add the ability to specify whether a taproot transaction should be signed using the internal key or not, using `sign_with_tap_internal_key` in `SignOptions`
|
||||
- Consolidate params `fee_amount` and `amount_needed` in `target_amount` in `CoinSelectionAlgorithm::coin_select` signature.
|
||||
- Change the meaning of the `fee_amount` field inside `CoinSelectionResult`: from now on the `fee_amount` will represent only the fees asociated with the utxos in the `selected` field of `CoinSelectionResult`.
|
||||
- New `RpcBlockchain` implementation with various fixes.
|
||||
- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`.
|
||||
|
||||
## [v0.20.0] - [v0.19.0]
|
||||
|
||||
@@ -36,7 +48,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [v0.18.0] - [v0.17.0]
|
||||
|
||||
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, ie. for mobile platforms.
|
||||
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, i.e. for mobile platforms.
|
||||
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
|
||||
- Deprecate `database::Database::flush()`, the function is only needed for the sled database on mobile, instead for mobile use the sqlite database.
|
||||
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
|
||||
@@ -479,4 +491,5 @@ final transaction is created by calling `finish` on the builder.
|
||||
[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
|
||||
[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
|
||||
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.21.0...HEAD
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.20.0"
|
||||
version = "0.21.0-rc.1"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -114,7 +114,7 @@ required-features = ["compiler"]
|
||||
[[example]]
|
||||
name = "rpcwallet"
|
||||
path = "examples/rpcwallet.rs"
|
||||
required-features = ["keys-bip39", "key-value-db", "rpc"]
|
||||
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
|
||||
|
||||
[workspace]
|
||||
members = ["macros"]
|
||||
|
||||
@@ -39,7 +39,7 @@ Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordin
|
||||
11. Publish **all** the updated crates to crates.io.
|
||||
12. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
|
||||
13. Merge the release branch back into `master`.
|
||||
14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (ie. `bdk-macros = { path = "./macros"}`)
|
||||
14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (i.e. `bdk-macros = { path = "./macros"}`)
|
||||
15. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes.
|
||||
16. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
|
||||
17. Announce the release on Twitter, Discord and Telegram.
|
||||
|
||||
@@ -95,6 +95,7 @@ use bdk::blockchain::ElectrumBlockchain;
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
use bitcoin::base64;
|
||||
use bitcoin::consensus::serialize;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
@@ -131,6 +132,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
```rust,no_run
|
||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||
|
||||
use bitcoin::base64;
|
||||
use bitcoin::consensus::deserialize;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
@@ -154,7 +156,7 @@ fn main() -> Result<(), bdk::Error> {
|
||||
|
||||
### Unit testing
|
||||
|
||||
```
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
@@ -162,7 +164,7 @@ cargo test
|
||||
|
||||
Integration testing require testing features, for example:
|
||||
|
||||
```
|
||||
```bash
|
||||
cargo test --features test-electrum
|
||||
```
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
auth: bitcoind_auth,
|
||||
network: Network::Regtest,
|
||||
wallet_name,
|
||||
skip_blocks: None,
|
||||
sync_params: None,
|
||||
};
|
||||
|
||||
// Use the above configuration to create a RPC blockchain backend
|
||||
|
||||
@@ -385,7 +385,7 @@ mod test {
|
||||
.sync_wallet(&wallet, None, Default::default())
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -213,7 +213,6 @@ impl WalletSync for EsploraBlockchain {
|
||||
};
|
||||
|
||||
database.commit_batch(batch_update)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@ This example shows how to sync multiple walles and return the sum of their balan
|
||||
# use bdk::database::*;
|
||||
# use bdk::wallet::*;
|
||||
# use bdk::*;
|
||||
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<u64, Error> {
|
||||
fn sum_of_balances<B: BlockchainFactory>(blockchain_factory: B, wallets: &[Wallet<MemoryDatabase>]) -> Result<Balance, Error> {
|
||||
Ok(wallets
|
||||
.iter()
|
||||
.map(|w| -> Result<_, Error> {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ returns associated transactions i.e. electrum.
|
||||
#![allow(dead_code)]
|
||||
use crate::{
|
||||
database::{BatchDatabase, BatchOperations, DatabaseUtils},
|
||||
error::MissingCachedScripts,
|
||||
wallet::time::Instant,
|
||||
BlockTime, Error, KeychainKind, LocalUtxo, TransactionDetails,
|
||||
};
|
||||
@@ -34,11 +35,12 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
|
||||
let scripts_needed = db
|
||||
.iter_script_pubkeys(Some(keychain))?
|
||||
.into_iter()
|
||||
.collect();
|
||||
.collect::<VecDeque<_>>();
|
||||
let state = State::new(db);
|
||||
|
||||
Ok(Request::Script(ScriptReq {
|
||||
state,
|
||||
initial_scripts_needed: scripts_needed.len(),
|
||||
scripts_needed,
|
||||
script_index: 0,
|
||||
stop_gap,
|
||||
@@ -50,6 +52,7 @@ pub fn start<D: BatchDatabase>(db: &D, stop_gap: usize) -> Result<Request<'_, D>
|
||||
pub struct ScriptReq<'a, D: BatchDatabase> {
|
||||
state: State<'a, D>,
|
||||
script_index: usize,
|
||||
initial_scripts_needed: usize, // if this is 1, we assume the descriptor is not derivable
|
||||
scripts_needed: VecDeque<Script>,
|
||||
stop_gap: usize,
|
||||
keychain: KeychainKind,
|
||||
@@ -113,43 +116,71 @@ impl<'a, D: BatchDatabase> ScriptReq<'a, D> {
|
||||
self.script_index += 1;
|
||||
}
|
||||
|
||||
for _ in txids {
|
||||
self.scripts_needed.pop_front();
|
||||
}
|
||||
self.scripts_needed.drain(..txids.len());
|
||||
|
||||
let last_active_index = self
|
||||
// last active index: 0 => No last active
|
||||
let last = self
|
||||
.state
|
||||
.last_active_index
|
||||
.get(&self.keychain)
|
||||
.map(|x| x + 1)
|
||||
.unwrap_or(0); // so no addresses active maps to 0
|
||||
.map(|&l| l + 1)
|
||||
.unwrap_or(0);
|
||||
// remaining scripts left to check
|
||||
let remaining = self.scripts_needed.len();
|
||||
// difference between current index and last active index
|
||||
let current_gap = self.script_index - last;
|
||||
|
||||
Ok(
|
||||
if self.script_index > last_active_index + self.stop_gap
|
||||
|| self.scripts_needed.is_empty()
|
||||
{
|
||||
debug!(
|
||||
"finished scanning for transactions for keychain {:?} at index {}",
|
||||
self.keychain, last_active_index
|
||||
);
|
||||
// we're done here -- check if we need to do the next keychain
|
||||
if let Some(keychain) = self.next_keychains.pop() {
|
||||
self.keychain = keychain;
|
||||
self.script_index = 0;
|
||||
self.scripts_needed = self
|
||||
.state
|
||||
.db
|
||||
.iter_script_pubkeys(Some(keychain))?
|
||||
.into_iter()
|
||||
.collect();
|
||||
Request::Script(self)
|
||||
} else {
|
||||
Request::Tx(TxReq { state: self.state })
|
||||
}
|
||||
} else {
|
||||
Request::Script(self)
|
||||
},
|
||||
)
|
||||
// this is a hack to check whether the scripts are coming from a derivable descriptor
|
||||
// we assume for non-derivable descriptors, the initial script count is always 1
|
||||
let is_derivable = self.initial_scripts_needed > 1;
|
||||
|
||||
debug!(
|
||||
"sync: last={}, remaining={}, diff={}, stop_gap={}",
|
||||
last, remaining, current_gap, self.stop_gap
|
||||
);
|
||||
|
||||
if is_derivable {
|
||||
if remaining > 0 {
|
||||
// we still have scriptPubKeys to do requests for
|
||||
return Ok(Request::Script(self));
|
||||
}
|
||||
|
||||
if last > 0 && current_gap < self.stop_gap {
|
||||
// current gap is not large enough to stop, but we are unable to keep checking since
|
||||
// we have exhausted cached scriptPubKeys, so return error
|
||||
let err = MissingCachedScripts {
|
||||
last_count: self.script_index,
|
||||
missing_count: self.stop_gap - current_gap,
|
||||
};
|
||||
return Err(Error::MissingCachedScripts(err));
|
||||
}
|
||||
|
||||
// we have exhausted cached scriptPubKeys and found no txs, continue
|
||||
}
|
||||
|
||||
debug!(
|
||||
"finished scanning for txs of keychain {:?} at index {:?}",
|
||||
self.keychain, last
|
||||
);
|
||||
|
||||
if let Some(keychain) = self.next_keychains.pop() {
|
||||
// we still have another keychain to request txs with
|
||||
let scripts_needed = self
|
||||
.state
|
||||
.db
|
||||
.iter_script_pubkeys(Some(keychain))?
|
||||
.into_iter()
|
||||
.collect::<VecDeque<_>>();
|
||||
|
||||
self.keychain = keychain;
|
||||
self.script_index = 0;
|
||||
self.initial_scripts_needed = scripts_needed.len();
|
||||
self.scripts_needed = scripts_needed;
|
||||
return Ok(Request::Script(self));
|
||||
}
|
||||
|
||||
// We have finished requesting txids, let's get the actual txs.
|
||||
Ok(Request::Tx(TxReq { state: self.state }))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,6 +325,8 @@ struct State<'a, D> {
|
||||
tx_missing_conftime: BTreeMap<Txid, TransactionDetails>,
|
||||
/// The start of the sync
|
||||
start_time: Instant,
|
||||
/// Missing number of scripts to cache per keychain
|
||||
missing_script_counts: HashMap<KeychainKind, usize>,
|
||||
}
|
||||
|
||||
impl<'a, D: BatchDatabase> State<'a, D> {
|
||||
@@ -305,6 +338,7 @@ impl<'a, D: BatchDatabase> State<'a, D> {
|
||||
tx_needed: BTreeSet::default(),
|
||||
tx_missing_conftime: BTreeMap::default(),
|
||||
start_time: Instant::new(),
|
||||
missing_script_counts: HashMap::default(),
|
||||
}
|
||||
}
|
||||
fn into_db_update(self) -> Result<D::Batch, Error> {
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
// <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 std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::hash_types::Txid;
|
||||
@@ -60,7 +62,7 @@ static MIGRATIONS: &[&str] = &[
|
||||
#[derive(Debug)]
|
||||
pub struct SqliteDatabase {
|
||||
/// Path on the local filesystem to store the sqlite file
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
/// A rusqlite connection object to the sqlite database
|
||||
pub connection: Connection,
|
||||
}
|
||||
@@ -68,9 +70,12 @@ pub struct SqliteDatabase {
|
||||
impl SqliteDatabase {
|
||||
/// Instantiate a new SqliteDatabase instance by creating a connection
|
||||
/// to the database stored at path
|
||||
pub fn new(path: String) -> Self {
|
||||
pub fn new<T: AsRef<Path>>(path: T) -> Self {
|
||||
let connection = get_connection(&path).unwrap();
|
||||
SqliteDatabase { path, connection }
|
||||
SqliteDatabase {
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
connection,
|
||||
}
|
||||
}
|
||||
fn insert_script_pubkey(
|
||||
&self,
|
||||
@@ -908,7 +913,7 @@ impl BatchDatabase for SqliteDatabase {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_connection(path: &str) -> Result<Connection, Error> {
|
||||
pub fn get_connection<T: AsRef<Path>>(path: &T) -> Result<Connection, Error> {
|
||||
let connection = Connection::open(path)?;
|
||||
migrate(&connection)?;
|
||||
Ok(connection)
|
||||
|
||||
@@ -14,12 +14,10 @@
|
||||
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
|
||||
//! checksum of a descriptor
|
||||
|
||||
use std::iter::FromIterator;
|
||||
|
||||
use crate::descriptor::DescriptorError;
|
||||
|
||||
const INPUT_CHARSET: &str = "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
const CHECKSUM_CHARSET: &str = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
let c0 = c >> 35;
|
||||
@@ -43,15 +41,17 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
c
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor
|
||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
/// Computes the checksum bytes of a descriptor
|
||||
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
let mut c = 1;
|
||||
let mut cls = 0;
|
||||
let mut clscount = 0;
|
||||
for ch in desc.chars() {
|
||||
|
||||
for ch in desc.as_bytes() {
|
||||
let pos = INPUT_CHARSET
|
||||
.find(ch)
|
||||
.ok_or(DescriptorError::InvalidDescriptorCharacter(ch))? as u64;
|
||||
.iter()
|
||||
.position(|b| b == ch)
|
||||
.ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
|
||||
c = poly_mod(c, pos & 31);
|
||||
cls = cls * 3 + (pos >> 5);
|
||||
clscount += 1;
|
||||
@@ -67,17 +67,18 @@ pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
(0..8).for_each(|_| c = poly_mod(c, 0));
|
||||
c ^= 1;
|
||||
|
||||
let mut chars = Vec::with_capacity(8);
|
||||
let mut checksum = [0_u8; 8];
|
||||
for j in 0..8 {
|
||||
chars.push(
|
||||
CHECKSUM_CHARSET
|
||||
.chars()
|
||||
.nth(((c >> (5 * (7 - j))) & 31) as usize)
|
||||
.unwrap(),
|
||||
);
|
||||
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
|
||||
}
|
||||
|
||||
Ok(String::from_iter(chars))
|
||||
Ok(checksum)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor
|
||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
get_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -97,17 +98,12 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_get_checksum_invalid_character() {
|
||||
let sparkle_heart = vec![240, 159, 146, 150];
|
||||
let sparkle_heart = std::str::from_utf8(&sparkle_heart)
|
||||
.unwrap()
|
||||
.chars()
|
||||
.next()
|
||||
.unwrap();
|
||||
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
|
||||
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
|
||||
|
||||
assert!(matches!(
|
||||
get_checksum(&invalid_desc).err(),
|
||||
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart
|
||||
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,7 +839,7 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
// - at least one of each "type" of operator; ie. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
|
||||
// - at least one of each "type" of operator; i.e. one modifier, one leaf_opcode, one leaf_opcode_value, etc.
|
||||
// - mixing up key types that implement IntoDescriptorKey in multi() or thresh()
|
||||
|
||||
// expected script for pk and bare manually created
|
||||
|
||||
@@ -28,8 +28,8 @@ pub enum Error {
|
||||
/// Error while extracting and manipulating policies
|
||||
Policy(crate::descriptor::policy::PolicyError),
|
||||
|
||||
/// Invalid character found in the descriptor checksum
|
||||
InvalidDescriptorCharacter(char),
|
||||
/// Invalid byte found in the descriptor checksum
|
||||
InvalidDescriptorCharacter(u8),
|
||||
|
||||
/// BIP32 error
|
||||
Bip32(bitcoin::util::bip32::Error),
|
||||
|
||||
@@ -9,6 +9,6 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
#[doc(include = "../README.md")]
|
||||
#[doc = include_str!("../README.md")]
|
||||
#[cfg(doctest)]
|
||||
pub struct ReadmeDoctests;
|
||||
|
||||
16
src/error.rs
16
src/error.rs
@@ -13,7 +13,7 @@ use std::fmt;
|
||||
|
||||
use crate::bitcoin::Network;
|
||||
use crate::{descriptor, wallet, wallet::address_validator};
|
||||
use bitcoin::OutPoint;
|
||||
use bitcoin::{OutPoint, Txid};
|
||||
|
||||
/// Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
||||
#[derive(Debug)]
|
||||
@@ -125,6 +125,10 @@ pub enum Error {
|
||||
//DifferentDescriptorStructure,
|
||||
//Uncapable(crate::blockchain::Capability),
|
||||
//MissingCachedAddresses,
|
||||
/// [`crate::blockchain::WalletSync`] sync attempt failed due to missing scripts in cache which
|
||||
/// are needed to satisfy `stop_gap`.
|
||||
MissingCachedScripts(MissingCachedScripts),
|
||||
|
||||
#[cfg(feature = "electrum")]
|
||||
/// Electrum client error
|
||||
Electrum(electrum_client::Error),
|
||||
@@ -145,6 +149,16 @@ pub enum Error {
|
||||
Rusqlite(rusqlite::Error),
|
||||
}
|
||||
|
||||
/// Represents the last failed [`crate::blockchain::WalletSync`] sync attempt in which we were short
|
||||
/// on cached `scriptPubKey`s.
|
||||
#[derive(Debug)]
|
||||
pub struct MissingCachedScripts {
|
||||
/// Number of scripts in which txs were requested during last request.
|
||||
pub last_count: usize,
|
||||
/// Minimum number of scripts to cache more of in order to satisfy `stop_gap`.
|
||||
pub missing_count: usize,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
//! interact with the bitcoin P2P network.
|
||||
//!
|
||||
//! ```toml
|
||||
//! bdk = "0.20.0"
|
||||
//! bdk = "0.21.0"
|
||||
//! ```
|
||||
//!
|
||||
//! # Examples
|
||||
|
||||
@@ -454,7 +454,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert!(wallet.database().deref().get_sync_time().unwrap().is_some(), "sync_time hasn't been updated");
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External, "incorrect keychain kind");
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
@@ -477,7 +477,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 100_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||
}
|
||||
|
||||
@@ -486,7 +486,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0);
|
||||
assert_eq!(wallet.get_balance().unwrap().get_total(), 0);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
@@ -494,8 +494,16 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) (@confirmations 1)
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap().confirmed, 100_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -508,7 +516,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 105_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 105_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 3, "incorrect number of unspents");
|
||||
|
||||
@@ -532,7 +540,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 2, "incorrect number of unspent");
|
||||
}
|
||||
@@ -546,14 +554,14 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 25_000 )
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 75_000, "incorrect balance");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -566,7 +574,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent");
|
||||
|
||||
@@ -580,7 +588,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after bump");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after bump");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs after bump");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect unspent after bump");
|
||||
|
||||
@@ -603,8 +611,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||
|
||||
@@ -617,7 +624,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance after invalidate");
|
||||
|
||||
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
|
||||
assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate");
|
||||
@@ -635,7 +642,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
@@ -646,7 +653,12 @@ macro_rules! bdk_blockchain_tests {
|
||||
println!("{}", bitcoin::consensus::encode::serialize_hex(&tx));
|
||||
blockchain.broadcast(&tx).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap().trusted_pending, details.received, "incorrect balance after send");
|
||||
|
||||
test_client.generate(1, Some(node_addr));
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap().confirmed, details.received, "incorrect balance after send");
|
||||
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
|
||||
@@ -720,7 +732,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||
|
||||
let tx1 = {
|
||||
@@ -743,9 +755,8 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
blockchain.broadcast(&tx1).expect("broadcasting first");
|
||||
blockchain.broadcast(&tx2).expect("broadcasting replacement");
|
||||
|
||||
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
|
||||
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
|
||||
assert_eq!(receiver_wallet.get_balance().expect("balance").untrusted_pending, 49_000, "should have received coins once and only once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -771,7 +782,8 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 100_000);
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
assert_eq!(balance.untrusted_pending + balance.get_spendable(), 100_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -785,7 +797,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||
|
||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||
let details = tx_map.get(&received_txid).unwrap();
|
||||
@@ -809,7 +821,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey(), 25_000);
|
||||
@@ -821,7 +833,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
blockchain.broadcast(&sent_tx).unwrap();
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect balance after receive");
|
||||
|
||||
// empty wallet
|
||||
let wallet = get_wallet_from_descriptors(&descriptors);
|
||||
@@ -852,7 +864,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||
|
||||
let mut total_sent = 0;
|
||||
for _ in 0..5 {
|
||||
@@ -869,7 +881,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
}
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - total_sent, "incorrect balance after chain");
|
||||
|
||||
// empty wallet
|
||||
|
||||
@@ -879,7 +891,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
test_client.generate(1, Some(node_addr));
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - total_sent, "incorrect balance empty wallet");
|
||||
|
||||
}
|
||||
|
||||
@@ -893,7 +905,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
|
||||
@@ -902,8 +914,8 @@ macro_rules! bdk_blockchain_tests {
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance from received");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect balance from received");
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
|
||||
@@ -912,8 +924,8 @@ macro_rules! bdk_blockchain_tests {
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
|
||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance from received after bump");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - new_details.fee.unwrap_or(0) - 5_000, "incorrect balance from fees after bump");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance from received after bump");
|
||||
|
||||
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||
}
|
||||
@@ -928,7 +940,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
@@ -937,8 +949,8 @@ macro_rules! bdk_blockchain_tests {
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect received after send");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 1_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "incorrect received after send");
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(5.1));
|
||||
@@ -947,7 +959,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 0, "incorrect balance after change removal");
|
||||
assert_eq!(new_details.received, 0, "incorrect received after change removal");
|
||||
|
||||
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
|
||||
@@ -963,7 +975,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
@@ -972,7 +984,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
@@ -983,7 +995,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), new_details.received, "incorrect balance after add input");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -996,7 +1008,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 75_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
|
||||
@@ -1005,7 +1017,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 26_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(details.received, 1_000 - details.fee.unwrap_or(0), "incorrect received after send");
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
@@ -1018,7 +1030,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(new_details.sent, 75_000, "incorrect sent");
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 0, "incorrect balance after add input");
|
||||
assert_eq!(new_details.received, 0, "incorrect received after add input");
|
||||
}
|
||||
|
||||
@@ -1032,7 +1044,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "incorrect balance");
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
let data = [42u8;80];
|
||||
@@ -1047,7 +1059,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
blockchain.broadcast(&tx).unwrap();
|
||||
test_client.generate(1, Some(node_addr));
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000 - details.fee.unwrap_or(0), "incorrect balance after send");
|
||||
|
||||
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
|
||||
let _ = tx_map.get(&tx.txid()).unwrap();
|
||||
@@ -1058,22 +1070,24 @@ macro_rules! bdk_blockchain_tests {
|
||||
let (wallet, blockchain, _, mut test_client) = init_single_sig();
|
||||
|
||||
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||
println!("wallet addr: {}", wallet_addr);
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().immature, 0, "incorrect balance");
|
||||
|
||||
test_client.generate(1, Some(wallet_addr));
|
||||
|
||||
#[cfg(feature = "rpc")]
|
||||
{
|
||||
// rpc consider coinbase only when mature (100 blocks)
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
test_client.generate(100, Some(node_addr));
|
||||
}
|
||||
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
|
||||
|
||||
assert!(wallet.get_balance().unwrap().immature > 0, "incorrect balance after receiving coinbase");
|
||||
|
||||
// make coinbase mature (100 blocks)
|
||||
let node_addr = test_client.get_node_address(None);
|
||||
test_client.generate(100, Some(node_addr));
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert!(wallet.get_balance().unwrap().confirmed > 0, "incorrect balance after maturing coinbase");
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1150,7 +1164,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000, "wallet has incorrect balance");
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000, "wallet has incorrect balance");
|
||||
|
||||
// 4. Send 25_000 sats from test BDK wallet to test bitcoind node taproot wallet
|
||||
|
||||
@@ -1162,7 +1176,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
let tx = psbt.extract_tx();
|
||||
blockchain.broadcast(&tx).unwrap();
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), details.received, "wallet has incorrect balance after send");
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), details.received, "wallet has incorrect balance after send");
|
||||
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "wallet has incorrect number of txs");
|
||||
assert_eq!(wallet.list_unspent().unwrap().len(), 1, "wallet has incorrect number of unspents");
|
||||
test_client.generate(1, None);
|
||||
@@ -1268,12 +1282,12 @@ macro_rules! bdk_blockchain_tests {
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
let _ = test_client.receive(testutils! {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||
|
||||
let tx = {
|
||||
let mut builder = wallet.build_tx();
|
||||
@@ -1296,7 +1310,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
@tx ( (@external descriptors, 0) => 50_000 )
|
||||
});
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.get_balance().unwrap().untrusted_pending, 50_000);
|
||||
|
||||
let tx = {
|
||||
let mut builder = wallet.build_tx();
|
||||
@@ -1317,7 +1331,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
@tx ( (@external descriptors, 0) => 50_000 ) ( @confirmations 6 )
|
||||
});
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 50_000);
|
||||
assert_eq!(wallet.get_balance().unwrap().get_spendable(), 50_000);
|
||||
|
||||
let ext_policy = wallet.policies(KeychainKind::External).unwrap().unwrap();
|
||||
let int_policy = wallet.policies(KeychainKind::Internal).unwrap().unwrap();
|
||||
|
||||
@@ -29,6 +29,8 @@ pub trait ConfigurableBlockchainTester<B: ConfigurableBlockchain>: Sized {
|
||||
|
||||
if self.config_with_stop_gap(test_client, 0).is_some() {
|
||||
test_wallet_sync_with_stop_gaps(test_client, self);
|
||||
test_wallet_sync_fulfills_missing_script_cache(test_client, self);
|
||||
test_wallet_sync_self_transfer_tx(test_client, self);
|
||||
} else {
|
||||
println!(
|
||||
"{}: Skipped tests requiring config_with_stop_gap.",
|
||||
@@ -113,16 +115,21 @@ where
|
||||
} 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,
|
||||
);
|
||||
println!("{}", details);
|
||||
|
||||
// perform wallet sync
|
||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
|
||||
let wallet_balance = wallet.get_balance().unwrap().get_total();
|
||||
println!(
|
||||
"max: {}, min: {}, actual: {}",
|
||||
max_balance, min_balance, wallet_balance
|
||||
);
|
||||
|
||||
assert!(
|
||||
wallet_balance <= max_balance,
|
||||
"wallet balance is greater than received amount: {}",
|
||||
@@ -138,3 +145,113 @@ where
|
||||
test_client.generate(1, None);
|
||||
}
|
||||
}
|
||||
|
||||
/// With a `stop_gap` of x and every x addresses having a balance of 1000 (for y addresses),
|
||||
/// we expect `Wallet::sync` to correctly self-cache addresses, so that the resulting balance,
|
||||
/// after sync, should be y * 1000.
|
||||
fn test_wallet_sync_fulfills_missing_script_cache<T, B>(test_client: &mut TestClient, tester: &T)
|
||||
where
|
||||
T: ConfigurableBlockchainTester<B>,
|
||||
B: ConfigurableBlockchain,
|
||||
{
|
||||
// wallet descriptor
|
||||
let descriptor = "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/200/*)";
|
||||
|
||||
// amount in sats per tx
|
||||
const AMOUNT_PER_TX: u64 = 1000;
|
||||
|
||||
// addr constants
|
||||
const ADDR_COUNT: usize = 6;
|
||||
const ADDR_GAP: usize = 60;
|
||||
|
||||
let blockchain =
|
||||
B::from_config(&tester.config_with_stop_gap(test_client, ADDR_GAP).unwrap()).unwrap();
|
||||
|
||||
let wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||
|
||||
let expected_balance = (0..ADDR_COUNT).fold(0_u64, |sum, i| {
|
||||
let addr_i = i * ADDR_GAP;
|
||||
let address = wallet.get_address(AddressIndex::Peek(addr_i as _)).unwrap();
|
||||
|
||||
println!(
|
||||
"tx: {} sats => [{}] {}",
|
||||
AMOUNT_PER_TX,
|
||||
addr_i,
|
||||
address.to_string()
|
||||
);
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@addr address.address) => AMOUNT_PER_TX )
|
||||
});
|
||||
test_client.generate(1, None);
|
||||
|
||||
sum + AMOUNT_PER_TX
|
||||
});
|
||||
println!("expected balance: {}, syncing...", expected_balance);
|
||||
|
||||
// perform sync
|
||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
println!("sync done!");
|
||||
|
||||
let balance = wallet.get_balance().unwrap().get_total();
|
||||
assert_eq!(balance, expected_balance);
|
||||
}
|
||||
|
||||
/// Given a `stop_gap`, a wallet with a 2 transactions, one sending to `scriptPubKey` at derivation
|
||||
/// index of `stop_gap`, and the other spending from the same `scriptPubKey` into another
|
||||
/// `scriptPubKey` at derivation index of `stop_gap * 2`, we expect `Wallet::sync` to perform
|
||||
/// correctly, so that we detect the total balance.
|
||||
fn test_wallet_sync_self_transfer_tx<T, B>(test_client: &mut TestClient, tester: &T)
|
||||
where
|
||||
T: ConfigurableBlockchainTester<B>,
|
||||
B: ConfigurableBlockchain,
|
||||
{
|
||||
const TRANSFER_AMOUNT: u64 = 10_000;
|
||||
const STOP_GAP: usize = 75;
|
||||
|
||||
let descriptor = "wpkh(tprv8i8F4EhYDMquzqiecEX8SKYMXqfmmb1Sm7deoA1Hokxzn281XgTkwsd6gL8aJevLE4aJugfVf9MKMvrcRvPawGMenqMBA3bRRfp4s1V7Eg3/*)";
|
||||
|
||||
let blockchain =
|
||||
B::from_config(&tester.config_with_stop_gap(test_client, STOP_GAP).unwrap()).unwrap();
|
||||
|
||||
let wallet = Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||
|
||||
let address1 = wallet
|
||||
.get_address(AddressIndex::Peek(STOP_GAP as _))
|
||||
.unwrap();
|
||||
let address2 = wallet
|
||||
.get_address(AddressIndex::Peek((STOP_GAP * 2) as _))
|
||||
.unwrap();
|
||||
|
||||
test_client.receive(testutils! {
|
||||
@tx ( (@addr address1.address) => TRANSFER_AMOUNT )
|
||||
});
|
||||
test_client.generate(1, None);
|
||||
|
||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(address2.script_pubkey(), TRANSFER_AMOUNT / 2);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
assert!(wallet.sign(&mut psbt, Default::default()).unwrap());
|
||||
blockchain.broadcast(&psbt.extract_tx()).unwrap();
|
||||
|
||||
test_client.generate(1, None);
|
||||
|
||||
// obtain what is expected
|
||||
let fee = details.fee.unwrap();
|
||||
let expected_balance = TRANSFER_AMOUNT - fee;
|
||||
println!("fee={}, expected_balance={}", fee, expected_balance);
|
||||
|
||||
// actually test the wallet
|
||||
wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
let balance = wallet.get_balance().unwrap().get_total();
|
||||
assert_eq!(balance, expected_balance);
|
||||
|
||||
// now try with a fresh wallet
|
||||
let fresh_wallet =
|
||||
Wallet::new(descriptor, None, Network::Regtest, MemoryDatabase::new()).unwrap();
|
||||
fresh_wallet.sync(&blockchain, Default::default()).unwrap();
|
||||
let fresh_balance = fresh_wallet.get_balance().unwrap().get_total();
|
||||
assert_eq!(fresh_balance, expected_balance);
|
||||
}
|
||||
|
||||
117
src/types.rs
117
src/types.rs
@@ -51,14 +51,34 @@ impl AsRef<[u8]> for KeychainKind {
|
||||
pub struct FeeRate(f32);
|
||||
|
||||
impl FeeRate {
|
||||
/// Create a new instance checking the value provided
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
fn new_checked(value: f32) -> Self {
|
||||
assert!(value.is_normal() || value == 0.0);
|
||||
assert!(value.is_sign_positive());
|
||||
|
||||
FeeRate(value)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
pub fn from_btc_per_kvb(btc_per_kvb: f32) -> Self {
|
||||
FeeRate(btc_per_kvb * 1e5)
|
||||
FeeRate::new_checked(btc_per_kvb * 1e5)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in satoshi/vbyte
|
||||
pub const fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
||||
FeeRate(sat_per_vb)
|
||||
///
|
||||
/// ## Panics
|
||||
///
|
||||
/// Panics if the value is not [normal](https://doc.rust-lang.org/std/primitive.f32.html#method.is_normal) (except if it's a positive zero) or negative.
|
||||
pub fn from_sat_per_vb(sat_per_vb: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_vb)
|
||||
}
|
||||
|
||||
/// Create a new [`FeeRate`] with the default min relay fee value
|
||||
@@ -207,7 +227,7 @@ pub struct TransactionDetails {
|
||||
/// Sent value (sats)
|
||||
/// Sum of owned inputs of this transaction.
|
||||
pub sent: u64,
|
||||
/// Fee value (sats) if available.
|
||||
/// Fee value (sats) if confirmed.
|
||||
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
||||
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
||||
/// funds while offline.
|
||||
@@ -242,13 +262,100 @@ impl BlockTime {
|
||||
}
|
||||
}
|
||||
|
||||
/// Balance differentiated in various categories
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct Balance {
|
||||
/// All coinbase outputs not yet matured
|
||||
pub immature: u64,
|
||||
/// Unconfirmed UTXOs generated by a wallet tx
|
||||
pub trusted_pending: u64,
|
||||
/// Unconfirmed UTXOs received from an external wallet
|
||||
pub untrusted_pending: u64,
|
||||
/// Confirmed and immediately spendable balance
|
||||
pub confirmed: u64,
|
||||
}
|
||||
|
||||
impl Balance {
|
||||
/// Get sum of trusted_pending and confirmed coins
|
||||
pub fn get_spendable(&self) -> u64 {
|
||||
self.confirmed + self.trusted_pending
|
||||
}
|
||||
|
||||
/// Get the whole balance visible to the wallet
|
||||
pub fn get_total(&self) -> u64 {
|
||||
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Balance {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
|
||||
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Balance {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
immature: self.immature + other.immature,
|
||||
trusted_pending: self.trusted_pending + other.trusted_pending,
|
||||
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
|
||||
confirmed: self.confirmed + other.confirmed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Sum for Balance {
|
||||
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
|
||||
iter.fold(
|
||||
Balance {
|
||||
..Default::default()
|
||||
},
|
||||
|a, b| a + b,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_store_feerate_in_const() {
|
||||
const _MY_RATE: FeeRate = FeeRate::from_sat_per_vb(10.0);
|
||||
const _MIN_RELAY: FeeRate = FeeRate::default_min_relay_fee();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_feerate_neg_zero() {
|
||||
let _ = FeeRate::from_sat_per_vb(-0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_feerate_neg_value() {
|
||||
let _ = FeeRate::from_sat_per_vb(-5.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_feerate_nan() {
|
||||
let _ = FeeRate::from_sat_per_vb(f32::NAN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_invalid_feerate_inf() {
|
||||
let _ = FeeRate::from_sat_per_vb(f32::INFINITY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_feerate_pos_zero() {
|
||||
let _ = FeeRate::from_sat_per_vb(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -72,6 +72,7 @@ use crate::psbt::PsbtUtils;
|
||||
use crate::signer::SignerError;
|
||||
use crate::testutils;
|
||||
use crate::types::*;
|
||||
use crate::wallet::coin_selection::Excess::{Change, NoChange};
|
||||
|
||||
const CACHE_ADDR_BATCH_SIZE: u32 = 100;
|
||||
const COINBASE_MATURITY: u32 = 100;
|
||||
@@ -324,7 +325,7 @@ where
|
||||
|
||||
/// Return a derived address using the external descriptor, see [`AddressIndex`] for
|
||||
/// available address index selection strategies. If none of the keys in the descriptor are derivable
|
||||
/// (ie. does not end with /*) then the same address will always be returned for any [`AddressIndex`].
|
||||
/// (i.e. does not end with /*) then the same address will always be returned for any [`AddressIndex`].
|
||||
pub fn get_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
|
||||
self._get_address(address_index, KeychainKind::External)
|
||||
}
|
||||
@@ -334,7 +335,7 @@ where
|
||||
/// If the wallet doesn't have an internal descriptor it will use the external descriptor.
|
||||
///
|
||||
/// see [`AddressIndex`] for available address index selection strategies. If none of the keys
|
||||
/// in the descriptor are derivable (ie. does not end with /*) then the same address will always
|
||||
/// in the descriptor are derivable (i.e. does not end with /*) then the same address will always
|
||||
/// be returned for any [`AddressIndex`].
|
||||
pub fn get_internal_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
|
||||
self._get_address(address_index, KeychainKind::Internal)
|
||||
@@ -437,26 +438,74 @@ where
|
||||
self.database.borrow().get_tx(txid, include_raw)
|
||||
}
|
||||
|
||||
/// Return the list of transactions made and received by the wallet
|
||||
/// Return an unsorted list of transactions made and received by the wallet
|
||||
///
|
||||
/// Optionally fill the [`TransactionDetails::transaction`] field with the raw transaction if
|
||||
/// `include_raw` is `true`.
|
||||
///
|
||||
/// Note that this methods only operate on the internal database, which first needs to be
|
||||
/// To sort transactions, the following code can be used:
|
||||
/// ```no_run
|
||||
/// # let mut tx_list: Vec<bdk::TransactionDetails> = vec![];
|
||||
/// tx_list.sort_by(|a, b| {
|
||||
/// b.confirmation_time
|
||||
/// .as_ref()
|
||||
/// .map(|t| t.height)
|
||||
/// .cmp(&a.confirmation_time.as_ref().map(|t| t.height))
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// Note that this method only operates on the internal database, which first needs to be
|
||||
/// [`Wallet::sync`] manually.
|
||||
pub fn list_transactions(&self, include_raw: bool) -> Result<Vec<TransactionDetails>, Error> {
|
||||
self.database.borrow().iter_txs(include_raw)
|
||||
}
|
||||
|
||||
/// Return the balance, meaning the sum of this wallet's unspent outputs' values
|
||||
/// Return the balance, separated into available, trusted-pending, untrusted-pending and immature
|
||||
/// values.
|
||||
///
|
||||
/// Note that this methods only operate on the internal database, which first needs to be
|
||||
/// Note that this method only operates on the internal database, which first needs to be
|
||||
/// [`Wallet::sync`] manually.
|
||||
pub fn get_balance(&self) -> Result<u64, Error> {
|
||||
Ok(self
|
||||
.list_unspent()?
|
||||
.iter()
|
||||
.fold(0, |sum, i| sum + i.txout.value))
|
||||
pub fn get_balance(&self) -> Result<Balance, Error> {
|
||||
let mut immature = 0;
|
||||
let mut trusted_pending = 0;
|
||||
let mut untrusted_pending = 0;
|
||||
let mut confirmed = 0;
|
||||
let utxos = self.list_unspent()?;
|
||||
let database = self.database.borrow();
|
||||
let last_sync_height = match database
|
||||
.get_sync_time()?
|
||||
.map(|sync_time| sync_time.block_time.height)
|
||||
{
|
||||
Some(height) => height,
|
||||
// None means database was never synced
|
||||
None => return Ok(Balance::default()),
|
||||
};
|
||||
for u in utxos {
|
||||
// Unwrap used since utxo set is created from database
|
||||
let tx = database
|
||||
.get_tx(&u.outpoint.txid, true)?
|
||||
.expect("Transaction not found in database");
|
||||
if let Some(tx_conf_time) = &tx.confirmation_time {
|
||||
if tx.transaction.expect("No transaction").is_coin_base()
|
||||
&& (last_sync_height - tx_conf_time.height) < COINBASE_MATURITY
|
||||
{
|
||||
immature += u.txout.value;
|
||||
} else {
|
||||
confirmed += u.txout.value;
|
||||
}
|
||||
} else if u.keychain == KeychainKind::Internal {
|
||||
trusted_pending += u.txout.value;
|
||||
} else {
|
||||
untrusted_pending += u.txout.value;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Balance {
|
||||
immature,
|
||||
trusted_pending,
|
||||
untrusted_pending,
|
||||
confirmed,
|
||||
})
|
||||
}
|
||||
|
||||
/// Add an external signer
|
||||
@@ -735,8 +784,6 @@ where
|
||||
let mut outgoing: u64 = 0;
|
||||
let mut received: u64 = 0;
|
||||
|
||||
fee_amount += fee_rate.fee_wu(tx.weight());
|
||||
|
||||
let recipients = params.recipients.iter().map(|(r, v)| (r, *v));
|
||||
|
||||
for (index, (script_pubkey, value)) in recipients.enumerate() {
|
||||
@@ -752,13 +799,25 @@ where
|
||||
script_pubkey: script_pubkey.clone(),
|
||||
value,
|
||||
};
|
||||
fee_amount += fee_rate.fee_vb(serialize(&new_out).len());
|
||||
|
||||
tx.output.push(new_out);
|
||||
|
||||
outgoing += value;
|
||||
}
|
||||
|
||||
fee_amount += fee_rate.fee_wu(tx.weight());
|
||||
|
||||
// Segwit transactions' header is 2WU larger than legacy txs' header,
|
||||
// as they contain a witness marker (1WU) and a witness flag (1WU) (see BIP144).
|
||||
// At this point we really don't know if the resulting transaction will be segwit
|
||||
// or legacy, so we just add this 2WU to the fee_amount - overshooting the fee amount
|
||||
// is better than undershooting it.
|
||||
// If we pass a fee_amount that is slightly higher than the final fee_amount, we
|
||||
// end up with a transaction with a slightly higher fee rate than the requested one.
|
||||
// If, instead, we undershoot, we may end up with a feerate lower than the requested one
|
||||
// - we might come up with non broadcastable txs!
|
||||
fee_amount += fee_rate.fee_wu(2);
|
||||
|
||||
if params.change_policy != tx_builder::ChangeSpendPolicy::ChangeAllowed
|
||||
&& self.change_descriptor.is_none()
|
||||
{
|
||||
@@ -777,15 +836,25 @@ where
|
||||
current_height,
|
||||
)?;
|
||||
|
||||
// get drain script
|
||||
let drain_script = match params.drain_to {
|
||||
Some(ref drain_recipient) => drain_recipient.clone(),
|
||||
None => self
|
||||
.get_internal_address(AddressIndex::New)?
|
||||
.address
|
||||
.script_pubkey(),
|
||||
};
|
||||
|
||||
let coin_selection = coin_selection.coin_select(
|
||||
self.database.borrow().deref(),
|
||||
required_utxos,
|
||||
optional_utxos,
|
||||
fee_rate,
|
||||
outgoing,
|
||||
fee_amount,
|
||||
outgoing + fee_amount,
|
||||
&drain_script,
|
||||
)?;
|
||||
let mut fee_amount = coin_selection.fee_amount;
|
||||
fee_amount += coin_selection.fee_amount;
|
||||
let excess = &coin_selection.excess;
|
||||
|
||||
tx.input = coin_selection
|
||||
.selected
|
||||
@@ -798,26 +867,6 @@ where
|
||||
})
|
||||
.collect();
|
||||
|
||||
// prepare the drain output
|
||||
let mut drain_output = {
|
||||
let script_pubkey = match params.drain_to {
|
||||
Some(ref drain_recipient) => drain_recipient.clone(),
|
||||
None => self
|
||||
.get_internal_address(AddressIndex::New)?
|
||||
.address
|
||||
.script_pubkey(),
|
||||
};
|
||||
|
||||
TxOut {
|
||||
script_pubkey,
|
||||
value: 0,
|
||||
}
|
||||
};
|
||||
|
||||
fee_amount += fee_rate.fee_vb(serialize(&drain_output).len());
|
||||
|
||||
let drain_val = (coin_selection.selected_amount() - outgoing).saturating_sub(fee_amount);
|
||||
|
||||
if tx.output.is_empty() {
|
||||
// Uh oh, our transaction has no outputs.
|
||||
// We allow this when:
|
||||
@@ -827,10 +876,15 @@ where
|
||||
// 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) {
|
||||
if let NoChange {
|
||||
dust_threshold,
|
||||
remaining_amount,
|
||||
change_fee,
|
||||
} = excess
|
||||
{
|
||||
return Err(Error::InsufficientFunds {
|
||||
needed: drain_output.script_pubkey.dust_value().as_sat(),
|
||||
available: drain_val,
|
||||
needed: *dust_threshold,
|
||||
available: remaining_amount.saturating_sub(*change_fee),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -838,15 +892,28 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
if drain_val.is_dust(&drain_output.script_pubkey) {
|
||||
fee_amount += drain_val;
|
||||
} else {
|
||||
drain_output.value = drain_val;
|
||||
if self.is_mine(&drain_output.script_pubkey)? {
|
||||
received += drain_val;
|
||||
match excess {
|
||||
NoChange {
|
||||
remaining_amount, ..
|
||||
} => fee_amount += remaining_amount,
|
||||
Change { amount, fee } => {
|
||||
if self.is_mine(&drain_script)? {
|
||||
received += amount;
|
||||
}
|
||||
fee_amount += fee;
|
||||
|
||||
// create drain output
|
||||
let drain_output = TxOut {
|
||||
value: *amount,
|
||||
script_pubkey: drain_script,
|
||||
};
|
||||
|
||||
// TODO: We should pay attention when adding a new output: this might increase
|
||||
// the lenght of the "number of vouts" parameter by 2 bytes, potentially making
|
||||
// our feerate too low
|
||||
tx.output.push(drain_output);
|
||||
}
|
||||
tx.output.push(drain_output);
|
||||
}
|
||||
};
|
||||
|
||||
// sort input/outputs according to the chosen algorithm
|
||||
params.ordering.sort_tx(&mut tx);
|
||||
@@ -1079,7 +1146,7 @@ where
|
||||
.iter()
|
||||
.chain(self.change_signers.signers().iter())
|
||||
{
|
||||
signer.sign_transaction(psbt, &self.secp)?;
|
||||
signer.sign_transaction(psbt, &sign_options, &self.secp)?;
|
||||
}
|
||||
|
||||
// attempt to finalize
|
||||
@@ -1685,20 +1752,62 @@ where
|
||||
) -> Result<(), Error> {
|
||||
debug!("Begin sync...");
|
||||
|
||||
let SyncOptions { progress } = sync_opts;
|
||||
let progress = progress.unwrap_or_else(|| Box::new(NoopProgress));
|
||||
// TODO: for the next runs, we cannot reuse the `sync_opts.progress` object due to trait
|
||||
// restrictions
|
||||
let mut progress_iter = sync_opts.progress.into_iter();
|
||||
let mut new_progress = || {
|
||||
progress_iter
|
||||
.next()
|
||||
.unwrap_or_else(|| Box::new(NoopProgress))
|
||||
};
|
||||
|
||||
let run_setup = self.ensure_addresses_cached(CACHE_ADDR_BATCH_SIZE)?;
|
||||
|
||||
debug!("run_setup: {}", run_setup);
|
||||
|
||||
// TODO: what if i generate an address first and cache some addresses?
|
||||
// TODO: we should sync if generating an address triggers a new batch to be stored
|
||||
if run_setup {
|
||||
maybe_await!(
|
||||
blockchain.wallet_setup(self.database.borrow_mut().deref_mut(), progress,)
|
||||
|
||||
// We need to ensure descriptor is derivable to fullfil "missing cache", otherwise we will
|
||||
// end up with an infinite loop
|
||||
let is_deriveable = self.descriptor.is_deriveable()
|
||||
&& (self.change_descriptor.is_none()
|
||||
|| self.change_descriptor.as_ref().unwrap().is_deriveable());
|
||||
|
||||
// Restrict max rounds in case of faulty "missing cache" implementation by blockchain
|
||||
let max_rounds = if is_deriveable { 100 } else { 1 };
|
||||
|
||||
for _ in 0..max_rounds {
|
||||
let sync_res =
|
||||
if run_setup {
|
||||
maybe_await!(blockchain
|
||||
.wallet_setup(self.database.borrow_mut().deref_mut(), new_progress()))
|
||||
} else {
|
||||
maybe_await!(blockchain
|
||||
.wallet_sync(self.database.borrow_mut().deref_mut(), new_progress()))
|
||||
};
|
||||
|
||||
// If the error is the special `MissingCachedScripts` error, we return the number of
|
||||
// scripts we should ensure cached.
|
||||
// On any other error, we should return the error.
|
||||
// On no error, we say `ensure_cache` is 0.
|
||||
let ensure_cache = sync_res.map_or_else(
|
||||
|e| match e {
|
||||
Error::MissingCachedScripts(inner) => {
|
||||
// each call to `WalletSync` is expensive, maximize on scripts to search for
|
||||
let extra =
|
||||
std::cmp::max(inner.missing_count as u32, CACHE_ADDR_BATCH_SIZE);
|
||||
let last = inner.last_count as u32;
|
||||
Ok(extra + last)
|
||||
}
|
||||
_ => Err(e),
|
||||
},
|
||||
|_| Ok(0_u32),
|
||||
)?;
|
||||
} else {
|
||||
maybe_await!(blockchain.wallet_sync(self.database.borrow_mut().deref_mut(), progress,))?;
|
||||
|
||||
// cache and try again, break when there is nothing to cache
|
||||
if !self.ensure_addresses_cached(ensure_cache)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let sync_time = SyncTime {
|
||||
@@ -1809,6 +1918,14 @@ pub(crate) mod test {
|
||||
use crate::signer::{SignOptions, SignerError};
|
||||
use crate::wallet::AddressIndex::{LastUnused, New, Peek, Reset};
|
||||
|
||||
// The satisfaction size of a P2WPKH is 112 WU =
|
||||
// 1 (elements in witness) + 1 (OP_PUSH) + 33 (pk) + 1 (OP_PUSH) + 72 (signature + sighash) + 1*4 (script len)
|
||||
// On the witness itself, we have to push once for the pk (33WU) and once for signature + sighash (72WU), for
|
||||
// a total of 105 WU.
|
||||
// Here, we push just once for simplicity, so we have to add an extra byte for the missing
|
||||
// OP_PUSH.
|
||||
const P2WPKH_FAKE_WITNESS_SIZE: usize = 106;
|
||||
|
||||
#[test]
|
||||
fn test_cache_addresses_fixed() {
|
||||
let db = MemoryDatabase::new();
|
||||
@@ -1926,6 +2043,10 @@ pub(crate) mod test {
|
||||
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
}
|
||||
|
||||
pub(crate) fn get_test_tr_with_taptree_both_priv() -> &'static str {
|
||||
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})"
|
||||
}
|
||||
|
||||
pub(crate) fn get_test_tr_repeated_key() -> &'static str {
|
||||
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})"
|
||||
}
|
||||
@@ -1935,16 +2056,18 @@ pub(crate) mod test {
|
||||
}
|
||||
|
||||
pub(crate) fn get_test_tr_with_taptree_xprv() -> &'static str {
|
||||
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
}
|
||||
|
||||
macro_rules! assert_fee_rate {
|
||||
($tx:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
|
||||
let mut tx = $tx.clone();
|
||||
($psbt:expr, $fees:expr, $fee_rate:expr $( ,@dust_change $( $dust_change:expr )* )* $( ,@add_signature $( $add_signature:expr )* )* ) => ({
|
||||
let psbt = $psbt.clone();
|
||||
#[allow(unused_mut)]
|
||||
let mut tx = $psbt.clone().extract_tx();
|
||||
$(
|
||||
$( $add_signature )*
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
}
|
||||
)*
|
||||
|
||||
@@ -1956,11 +2079,23 @@ pub(crate) mod test {
|
||||
dust_change = true;
|
||||
)*
|
||||
|
||||
let fee_amount = psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.fold(0, |acc, i| acc + i.witness_utxo.as_ref().unwrap().value)
|
||||
- psbt
|
||||
.unsigned_tx
|
||||
.output
|
||||
.iter()
|
||||
.fold(0, |acc, o| acc + o.value);
|
||||
|
||||
assert_eq!(fee_amount, $fees);
|
||||
|
||||
let tx_fee_rate = FeeRate::from_wu($fees, tx.weight());
|
||||
let fee_rate = $fee_rate;
|
||||
|
||||
if !dust_change {
|
||||
assert!((tx_fee_rate - fee_rate).as_sat_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||
assert!(tx_fee_rate >= fee_rate && (tx_fee_rate - fee_rate).as_sat_vb().abs() < 0.5, "Expected fee rate of {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||
} else {
|
||||
assert!(tx_fee_rate >= fee_rate, "Expected fee rate of at least {:?}, the tx has {:?}", fee_rate, tx_fee_rate);
|
||||
}
|
||||
@@ -2335,7 +2470,7 @@ pub(crate) mod test {
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::default(), @add_signature);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::default(), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2348,7 +2483,7 @@ pub(crate) mod test {
|
||||
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3161,7 +3296,7 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the new utxos, we know they can't be used anyways
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3205,7 +3340,7 @@ pub(crate) mod test {
|
||||
details.received
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3221,7 +3356,7 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the new utxos, we know they can't be used anyways
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3287,7 +3422,7 @@ pub(crate) mod test {
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3315,7 +3450,7 @@ pub(crate) mod test {
|
||||
assert_eq!(tx.output.len(), 1);
|
||||
assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent);
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3331,7 +3466,7 @@ pub(crate) mod test {
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3387,7 +3522,7 @@ pub(crate) mod test {
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3444,7 +3579,7 @@ pub(crate) mod test {
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3485,7 +3620,7 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the new utxos, we know they can't be used anyways
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3526,7 +3661,7 @@ pub(crate) mod test {
|
||||
details.received
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3548,7 +3683,7 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the new utxos, we know they can't be used anyways
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3619,7 +3754,7 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the new utxos, we know they can't be used anyways
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3666,7 +3801,7 @@ pub(crate) mod test {
|
||||
75_000 - original_send_all_amount - details.fee.unwrap_or(0)
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3690,13 +3825,14 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the new utxos, we know they can't be used anyways
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.del_utxo(&txin.previous_output)
|
||||
.unwrap();
|
||||
}
|
||||
let original_tx_weight = tx.weight();
|
||||
original_details.transaction = Some(tx);
|
||||
wallet
|
||||
.database
|
||||
@@ -3705,7 +3841,20 @@ pub(crate) mod test {
|
||||
.unwrap();
|
||||
|
||||
let mut builder = wallet.build_fee_bump(txid).unwrap();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(141.0));
|
||||
// We set a fee high enough that during rbf we are forced to add
|
||||
// a new input and also that we have to remove the change
|
||||
// that we had previously
|
||||
|
||||
// We calculate the new weight as:
|
||||
// original weight
|
||||
// + extra input weight: 160 WU = (32 (prevout) + 4 (vout) + 4 (nsequence)) * 4
|
||||
// + input satisfaction weight: 112 WU = 106 (witness) + 2 (witness len) + (1 (script len)) * 4
|
||||
// - change output weight: 124 WU = (8 (value) + 1 (script len) + 22 (script)) * 4
|
||||
let new_tx_weight = original_tx_weight + 160 + 112 - 124;
|
||||
// two inputs (50k, 25k) and one output (45k) - epsilon
|
||||
// We use epsilon here to avoid asking for a slightly too high feerate
|
||||
let fee_abs = 50_000 + 25_000 - 45_000 - 10;
|
||||
builder.fee_rate(FeeRate::from_wu(fee_abs, new_tx_weight));
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
@@ -3729,7 +3878,7 @@ pub(crate) mod test {
|
||||
45_000
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3751,7 +3900,7 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the new utxos, we know they can't be used anyways
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3800,7 +3949,7 @@ pub(crate) mod test {
|
||||
details.received
|
||||
);
|
||||
|
||||
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3822,7 +3971,7 @@ pub(crate) mod test {
|
||||
let txid = tx.txid();
|
||||
// skip saving the new utxos, we know they can't be used anyways
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3901,7 +4050,7 @@ pub(crate) mod test {
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3945,7 +4094,7 @@ pub(crate) mod test {
|
||||
let mut tx = psbt.extract_tx();
|
||||
let txid = tx.txid();
|
||||
for txin in &mut tx.input {
|
||||
txin.witness.push([0x00; 108]); // fake signature
|
||||
txin.witness.push([0x00; P2WPKH_FAKE_WITNESS_SIZE]); // fake signature
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
@@ -3967,6 +4116,38 @@ pub(crate) mod test {
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_amount_negative_drain_val() {
|
||||
// While building the transaction, bdk would calculate the drain_value
|
||||
// as
|
||||
// current_delta - fee_amount - drain_fee
|
||||
// using saturating_sub, meaning that if the result would end up negative,
|
||||
// it'll remain to zero instead.
|
||||
// This caused a bug in master where we would calculate the wrong fee
|
||||
// for a transaction.
|
||||
// See https://github.com/bitcoindevkit/bdk/issues/660
|
||||
let (wallet, descriptors, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = Address::from_str("tb1ql7w62elx9ucw4pj5lgw4l028hmuw80sndtntxt").unwrap();
|
||||
let fee_rate = FeeRate::from_sat_per_vb(2.01);
|
||||
let incoming_txid = crate::populate_test_db!(
|
||||
wallet.database.borrow_mut(),
|
||||
testutils! (@tx ( (@external descriptors, 0) => 8859 ) (@confirmations 1)),
|
||||
Some(100),
|
||||
);
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(send_to.script_pubkey(), 8630)
|
||||
.add_utxo(OutPoint::new(incoming_txid, 0))
|
||||
.unwrap()
|
||||
.enable_rbf()
|
||||
.fee_rate(fee_rate);
|
||||
let (psbt, details) = builder.finish().unwrap();
|
||||
|
||||
assert!(psbt.inputs.len() == 1);
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate, @add_signature);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sign_single_xprv() {
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
@@ -4499,6 +4680,34 @@ pub(crate) mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_address_no_reuse_single_descriptor() {
|
||||
use crate::descriptor::template::Bip84;
|
||||
use std::collections::HashSet;
|
||||
|
||||
let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let wallet = Wallet::new(
|
||||
Bip84(key, KeychainKind::External),
|
||||
None,
|
||||
Network::Regtest,
|
||||
MemoryDatabase::default(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut used_set = HashSet::new();
|
||||
|
||||
(0..3).for_each(|_| {
|
||||
let external_addr = wallet.get_address(AddressIndex::New).unwrap().address;
|
||||
assert!(used_set.insert(external_addr));
|
||||
|
||||
let internal_addr = wallet
|
||||
.get_internal_address(AddressIndex::New)
|
||||
.unwrap()
|
||||
.address;
|
||||
assert!(used_set.insert(internal_addr));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_psbt_populate_tap_key_origins() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
|
||||
@@ -4753,6 +4962,31 @@ pub(crate) mod test {
|
||||
test_spend_from_wallet(wallet);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_no_key_spend() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
|
||||
assert!(
|
||||
wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
sign_with_tap_internal_key: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
"Unable to finalize tx"
|
||||
);
|
||||
|
||||
assert!(psbt.inputs.iter().all(|i| i.tap_key_sig.is_none()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree());
|
||||
@@ -4762,6 +4996,142 @@ pub(crate) mod test {
|
||||
test_spend_from_wallet(wallet);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend_sign_all_leaves() {
|
||||
use crate::signer::TapLeavesOptions;
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
|
||||
assert!(
|
||||
wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
tap_leaves_options: TapLeavesOptions::All,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
"Unable to finalize tx"
|
||||
);
|
||||
|
||||
assert!(psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.all(|i| i.tap_script_sigs.len() == i.tap_scripts.len()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend_sign_include_some_leaves() {
|
||||
use crate::signer::TapLeavesOptions;
|
||||
use crate::wallet::taproot::TapLeafHash;
|
||||
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
let mut script_leaves: Vec<_> = psbt.inputs[0]
|
||||
.tap_scripts
|
||||
.clone()
|
||||
.values()
|
||||
.map(|(script, version)| TapLeafHash::from_script(script, *version))
|
||||
.collect();
|
||||
let included_script_leaves = vec![script_leaves.pop().unwrap()];
|
||||
let excluded_script_leaves = script_leaves;
|
||||
|
||||
assert!(
|
||||
wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
tap_leaves_options: TapLeavesOptions::Include(
|
||||
included_script_leaves.clone()
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
"Unable to finalize tx"
|
||||
);
|
||||
|
||||
assert!(psbt.inputs[0]
|
||||
.tap_script_sigs
|
||||
.iter()
|
||||
.all(|s| included_script_leaves.contains(&s.0 .1)
|
||||
&& !excluded_script_leaves.contains(&s.0 .1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend_sign_exclude_some_leaves() {
|
||||
use crate::signer::TapLeavesOptions;
|
||||
use crate::wallet::taproot::TapLeafHash;
|
||||
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
let mut script_leaves: Vec<_> = psbt.inputs[0]
|
||||
.tap_scripts
|
||||
.clone()
|
||||
.values()
|
||||
.map(|(script, version)| TapLeafHash::from_script(script, *version))
|
||||
.collect();
|
||||
let included_script_leaves = vec![script_leaves.pop().unwrap()];
|
||||
let excluded_script_leaves = script_leaves;
|
||||
|
||||
assert!(
|
||||
wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
tap_leaves_options: TapLeavesOptions::Exclude(
|
||||
excluded_script_leaves.clone()
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
"Unable to finalize tx"
|
||||
);
|
||||
|
||||
assert!(psbt.inputs[0]
|
||||
.tap_script_sigs
|
||||
.iter()
|
||||
.all(|s| included_script_leaves.contains(&s.0 .1)
|
||||
&& !excluded_script_leaves.contains(&s.0 .1)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_script_spend_sign_no_leaves() {
|
||||
use crate::signer::TapLeavesOptions;
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_with_taptree_both_priv());
|
||||
let addr = wallet.get_address(AddressIndex::New).unwrap();
|
||||
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(addr.script_pubkey(), 25_000);
|
||||
let (mut psbt, _) = builder.finish().unwrap();
|
||||
|
||||
wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
tap_leaves_options: TapLeavesOptions::None,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(psbt.inputs.iter().all(|i| i.tap_script_sigs.is_empty()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_taproot_sign_derive_index_from_psbt() {
|
||||
let (wallet, _, _) = get_funded_wallet(get_test_tr_single_sig_xprv());
|
||||
@@ -4899,23 +5269,38 @@ pub(crate) mod test {
|
||||
Some(confirmation_time),
|
||||
(@coinbase true)
|
||||
);
|
||||
let sync_time = SyncTime {
|
||||
block_time: BlockTime {
|
||||
height: confirmation_time,
|
||||
timestamp: 0,
|
||||
},
|
||||
};
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_sync_time(sync_time)
|
||||
.unwrap();
|
||||
|
||||
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);
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 25_000,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 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)
|
||||
.add_recipient(addr.script_pubkey(), balance.immature / 2)
|
||||
.current_height(confirmation_time);
|
||||
assert!(matches!(
|
||||
builder.finish().unwrap_err(),
|
||||
@@ -4928,7 +5313,7 @@ pub(crate) mod test {
|
||||
// Still unspendable...
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), balance / 2)
|
||||
.add_recipient(addr.script_pubkey(), balance.immature / 2)
|
||||
.current_height(not_yet_mature_time);
|
||||
assert!(matches!(
|
||||
builder.finish().unwrap_err(),
|
||||
@@ -4939,10 +5324,94 @@ pub(crate) mod test {
|
||||
));
|
||||
|
||||
// ...Now the coinbase is mature :)
|
||||
let sync_time = SyncTime {
|
||||
block_time: BlockTime {
|
||||
height: maturity_time,
|
||||
timestamp: 0,
|
||||
},
|
||||
};
|
||||
wallet
|
||||
.database
|
||||
.borrow_mut()
|
||||
.set_sync_time(sync_time)
|
||||
.unwrap();
|
||||
|
||||
let balance = wallet.get_balance().unwrap();
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 25_000
|
||||
}
|
||||
);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(addr.script_pubkey(), balance / 2)
|
||||
.add_recipient(addr.script_pubkey(), balance.confirmed / 2)
|
||||
.current_height(maturity_time);
|
||||
builder.finish().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_rate_sign_no_grinding_high_r() {
|
||||
// Our goal is to obtain a transaction with a signature with high-R (71 bytes
|
||||
// instead of 70). We then check that our fee rate and fee calculation is
|
||||
// alright.
|
||||
let (wallet, _, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New).unwrap();
|
||||
let fee_rate = FeeRate::from_sat_per_vb(1.0);
|
||||
let mut builder = wallet.build_tx();
|
||||
let mut data = vec![0];
|
||||
builder
|
||||
.drain_to(addr.script_pubkey())
|
||||
.drain_wallet()
|
||||
.fee_rate(fee_rate)
|
||||
.add_data(&data);
|
||||
let (mut psbt, details) = builder.finish().unwrap();
|
||||
let (op_return_vout, _) = psbt
|
||||
.unsigned_tx
|
||||
.output
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_n, i)| i.script_pubkey.is_op_return())
|
||||
.unwrap();
|
||||
|
||||
let mut sig_len: usize = 0;
|
||||
// We try to sign many different times until we find a longer signature (71 bytes)
|
||||
while sig_len < 71 {
|
||||
// Changing the OP_RETURN data will make the signature change (but not the fee, until
|
||||
// data[0] is small enough)
|
||||
data[0] += 1;
|
||||
psbt.unsigned_tx.output[op_return_vout].script_pubkey = Script::new_op_return(&data);
|
||||
// Clearing the previous signature
|
||||
psbt.inputs[0].partial_sigs.clear();
|
||||
// Signing
|
||||
wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
remove_partial_sigs: false,
|
||||
try_finalize: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
// We only have one key in the partial_sigs map, this is a trick to retrieve it
|
||||
let key = psbt.inputs[0].partial_sigs.keys().next().unwrap();
|
||||
sig_len = psbt.inputs[0].partial_sigs[key].sig.serialize_der().len();
|
||||
}
|
||||
// Actually finalizing the transaction...
|
||||
wallet
|
||||
.sign(
|
||||
&mut psbt,
|
||||
SignOptions {
|
||||
remove_partial_sigs: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
// ...and checking that everything is fine
|
||||
assert_fee_rate!(psbt, details.fee.unwrap_or(0), fee_rate);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@
|
||||
//! &self,
|
||||
//! psbt: &mut psbt::PartiallySignedTransaction,
|
||||
//! input_index: usize,
|
||||
//! _sign_options: &SignOptions,
|
||||
//! _secp: &Secp256k1<All>,
|
||||
//! ) -> Result<(), SignerError> {
|
||||
//! self.device.hsm_sign_input(psbt, input_index)?;
|
||||
@@ -241,6 +242,7 @@ pub trait InputSigner: SignerCommon {
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError>;
|
||||
}
|
||||
@@ -254,6 +256,7 @@ pub trait TransactionSigner: SignerCommon {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError>;
|
||||
}
|
||||
@@ -262,10 +265,11 @@ impl<T: InputSigner> TransactionSigner for T {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
for input_index in 0..psbt.inputs.len() {
|
||||
self.sign_input(psbt, input_index, secp)?;
|
||||
self.sign_input(psbt, input_index, sign_options, secp)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -287,6 +291,7 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
if input_index >= psbt.inputs.len() {
|
||||
@@ -346,7 +351,7 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
inner: derived_key.private_key,
|
||||
};
|
||||
|
||||
SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, secp)
|
||||
SignerWrapper::new(priv_key, self.ctx).sign_input(psbt, input_index, sign_options, secp)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,6 +374,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||
@@ -385,7 +391,10 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
|
||||
|
||||
if let SignerContext::Tap { is_internal_key } = self.ctx {
|
||||
if is_internal_key && psbt.inputs[input_index].tap_key_sig.is_none() {
|
||||
if is_internal_key
|
||||
&& psbt.inputs[input_index].tap_key_sig.is_none()
|
||||
&& sign_options.sign_with_tap_internal_key
|
||||
{
|
||||
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
|
||||
sign_psbt_schnorr(
|
||||
&self.inner,
|
||||
@@ -404,9 +413,18 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
let leaf_hashes = leaf_hashes
|
||||
.iter()
|
||||
.filter(|lh| {
|
||||
!psbt.inputs[input_index]
|
||||
.tap_script_sigs
|
||||
.contains_key(&(x_only_pubkey, **lh))
|
||||
// Removing the leaves we shouldn't sign for
|
||||
let should_sign = match &sign_options.tap_leaves_options {
|
||||
TapLeavesOptions::All => true,
|
||||
TapLeavesOptions::Include(v) => v.contains(lh),
|
||||
TapLeavesOptions::Exclude(v) => !v.contains(lh),
|
||||
TapLeavesOptions::None => false,
|
||||
};
|
||||
// Filtering out the leaves without our key
|
||||
should_sign
|
||||
&& !psbt.inputs[input_index]
|
||||
.tap_script_sigs
|
||||
.contains_key(&(x_only_pubkey, **lh))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
@@ -677,6 +695,38 @@ pub struct SignOptions {
|
||||
///
|
||||
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
|
||||
pub try_finalize: bool,
|
||||
|
||||
/// Specifies which Taproot script-spend leaves we should sign for. This option is
|
||||
/// ignored if we're signing a non-taproot PSBT.
|
||||
///
|
||||
/// Defaults to All, i.e., the wallet will sign all the leaves it has a key for.
|
||||
pub tap_leaves_options: TapLeavesOptions,
|
||||
|
||||
/// Whether we should try to sign a taproot transaction with the taproot internal key
|
||||
/// or not. This option is ignored if we're signing a non-taproot PSBT.
|
||||
///
|
||||
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
|
||||
pub sign_with_tap_internal_key: bool,
|
||||
}
|
||||
|
||||
/// Customize which taproot script-path leaves the signer should sign.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum TapLeavesOptions {
|
||||
/// The signer will sign all the leaves it has a key for.
|
||||
All,
|
||||
/// The signer won't sign leaves other than the ones specified. Note that it could still ignore
|
||||
/// some of the specified leaves, if it doesn't have the right key to sign them.
|
||||
Include(Vec<taproot::TapLeafHash>),
|
||||
/// The signer won't sign the specified leaves.
|
||||
Exclude(Vec<taproot::TapLeafHash>),
|
||||
/// The signer won't sign any leaf.
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for TapLeavesOptions {
|
||||
fn default() -> Self {
|
||||
TapLeavesOptions::All
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
@@ -688,6 +738,8 @@ impl Default for SignOptions {
|
||||
allow_all_sighashes: false,
|
||||
remove_partial_sigs: true,
|
||||
try_finalize: true,
|
||||
tap_leaves_options: TapLeavesOptions::default(),
|
||||
sign_with_tap_internal_key: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1018,6 +1070,7 @@ mod signers_container_tests {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
_psbt: &mut psbt::PartiallySignedTransaction,
|
||||
_sign_options: &SignOptions,
|
||||
_secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
Ok(())
|
||||
|
||||
@@ -516,7 +516,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish the building the transaction.
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user