Compare commits

...

26 Commits

Author SHA1 Message Date
William Casarin
476fa3fd7d add Copy trait to Progress types 2021-06-23 08:31:55 -07:00
Alekos Filini
2755b09e7b Bump CI stable version to 1.53
Fixes #374
2021-06-21 12:16:54 +02:00
Alekos Filini
5e6286a493 Fix clippy warnings on 1.53
Fix `clippy::inconsistent_struct_constructor`: the constructor field
order was inconsistent with the struct declaration.
2021-06-21 12:16:45 +02:00
Alekos Filini
67714adc80 Fix CHANGELOG
The `Rpc` backend is not part of the release but it accidentally ended
up there during the merge
2021-06-21 09:07:15 +02:00
Alekos Filini
9ff86ea37c Merge commit 'refs/pull/370/head' of github.com:bitcoindevkit/bdk 2021-06-18 12:54:11 +02:00
Steve Myers
ceeb3a40cf [ci] Revert change to run_blockchain_tests.sh back to using container id 2021-06-15 15:57:14 -07:00
Steve Myers
e3316aee4c [ci] Change blockchain tests to use bitcoind rpc cookie authentication 2021-06-15 15:39:54 -07:00
Steve Myers
c2567b61aa Merge branch 'release/0.8.0' 2021-06-14 11:47:39 -07:00
Steve Myers
e1a77b87ab Fix CHANGELOG unreleased link 2021-06-14 11:43:48 -07:00
Steve Myers
5bf758b03a Add CHANGELOG v0.8.0 link 2021-06-14 11:40:50 -07:00
Riccardo Casatta
0bbfa5f989 make fee in TransactionDetails Option, add confirmation_time field as Option
confirmation_time contains both a block height and block timestamp and is
Some only for confirmed transaction
2021-06-14 15:29:24 +02:00
Alekos Filini
18254110c6 Merge commit 'refs/pull/348/head' of github.com:bitcoindevkit/bdk 2021-06-11 11:41:23 +02:00
Alekos Filini
44217539e5 Bump version to 0.8.1-dev 2021-06-11 11:29:42 +02:00
Riccardo Casatta
fe371f9d92 Use bitcoin's base64 feature for Psbts 2021-06-10 15:50:44 +02:00
Tobin Harding
12de13b95c Remove redundant borrows
Clippy emits:

  warning: this expression borrows a reference

As suggested remove the borrows from the front of vars that are already references.
2021-06-10 13:16:07 +10:00
Riccardo Casatta
ba2e3042cc add details to TODO, format doc example 2021-06-04 15:05:35 +02:00
Riccardo Casatta
1639984b56 move scan in setup 2021-06-03 15:26:47 +02:00
Riccardo Casatta
ab54a17eb7 update bitcoind dep 2021-06-03 11:07:39 +02:00
Riccardo Casatta
ae5aa06586 use storage address instead of satoshi's 2021-06-03 11:06:24 +02:00
Riccardo Casatta
ab98283159 always ask node for tx no matter capabilities 2021-06-03 10:56:02 +02:00
Riccardo Casatta
81851190f0 correctly initialize UTXO keychain kind 2021-06-03 10:56:02 +02:00
Riccardo Casatta
e1b037a921 change feature to execute sync from rpc to test-rpc 2021-06-03 10:56:01 +02:00
Riccardo Casatta
9b7ed08891 rename struct to CallResult 2021-06-03 10:56:01 +02:00
Riccardo Casatta
dffb753ce3 match also on signet 2021-06-03 10:56:00 +02:00
Riccardo Casatta
0b969657cd update changelog with rpc feature 2021-06-03 10:55:59 +02:00
Riccardo Casatta
bfef2e3cfe Implements RPC Backend 2021-06-03 10:55:58 +02:00
23 changed files with 1065 additions and 261 deletions

View File

@@ -10,7 +10,7 @@ jobs:
strategy:
matrix:
rust:
- 1.51.0 # STABLE
- 1.53.0 # STABLE
- 1.46.0 # MSRV
features:
- default
@@ -22,6 +22,7 @@ jobs:
- compact_filters
- esplora,key-value-db,electrum
- compiler
- rpc
steps:
- name: checkout
uses: actions/checkout@v2
@@ -77,19 +78,22 @@ jobs:
name: Test ${{ matrix.blockchain.name }}
runs-on: ubuntu-16.04
strategy:
fail-fast: false
matrix:
blockchain:
- name: electrum
container: bitcoindevkit/electrs
start: /root/electrs --network regtest --jsonrpc-import
container: bitcoindevkit/electrs:0.4.0
start: /root/electrs --network regtest --cookie-file $GITHUB_WORKSPACE/.bitcoin/regtest/.cookie --jsonrpc-import
- name: esplora
container: bitcoindevkit/esplora
start: /root/electrs --network regtest -vvv --cookie admin:passw --jsonrpc-import --electrum-rpc-addr=0.0.0.0:60401 --http-addr 0.0.0.0:3002
container: bitcoindevkit/esplora:0.4.0
start: /root/electrs --network regtest -vvv --daemon-dir $GITHUB_WORKSPACE/.bitcoin --jsonrpc-import --electrum-rpc-addr=0.0.0.0:60401 --http-addr 0.0.0.0:3002
- name: rpc
container: bitcoindevkit/electrs:0.4.0
start: /root/electrs --network regtest --cookie-file $GITHUB_WORKSPACE/.bitcoin/regtest/.cookie --jsonrpc-import
container: ${{ matrix.blockchain.container }}
env:
BDK_RPC_AUTH: USER_PASS
BDK_RPC_USER: admin
BDK_RPC_PASS: passw
BDK_RPC_AUTH: COOKIEFILE
BDK_RPC_COOKIEFILE: ${{ github.workspace }}/.bitcoin/regtest/.cookie
BDK_RPC_URL: 127.0.0.1:18443
BDK_RPC_WALLET: bdk-test
BDK_ELECTRUM_URL: tcp://127.0.0.1:60401
@@ -105,12 +109,12 @@ jobs:
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: get pkg-config # running eslpora tests seems to need this
- name: get pkg-config # running esplora tests seems to need this
run: apt update && apt install -y --fix-missing pkg-config libssl-dev
- name: Install rustup
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
- name: Set default toolchain
run: $HOME/.cargo/bin/rustup default 1.51.0 # STABLE
run: $HOME/.cargo/bin/rustup default 1.53.0 # STABLE
- name: Set profile
run: $HOME/.cargo/bin/rustup set profile minimal
- name: Update toolchain
@@ -120,7 +124,7 @@ jobs:
- name: start ${{ matrix.blockchain.name }}
run: nohup ${{ matrix.blockchain.start }} & sleep 5
- name: Test
run: $HOME/.cargo/bin/cargo test --features ${{ matrix.blockchain.name }},test-blockchains --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests
run: $HOME/.cargo/bin/cargo test --features test-${{ matrix.blockchain.name }},test-blockchains --no-default-features ${{ matrix.blockchain.name }}::bdk_blockchain_tests
check-wasm:
name: Check WASM
@@ -145,7 +149,7 @@ jobs:
- run: sudo apt-get update || exit 1
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
- name: Set default toolchain
run: rustup default 1.51.0 # STABLE
run: rustup default 1.53.0 # STABLE
- name: Set profile
run: rustup set profile minimal
- name: Add target wasm32

View File

@@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Wallet
#### Added
- Bitcoin core RPC added as blockchain backend
## [v0.8.0] - [v0.7.0]
### Wallet
@@ -339,7 +343,7 @@ final transaction is created by calling `finish` on the builder.
- Use `MemoryDatabase` in the compiler example
- Make the REPL return JSON
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.4.0...HEAD
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.8.0...HEAD
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1
[v0.2.0]: https://github.com/bitcoindevkit/bdk/compare/0.1.0-beta.1...v0.2.0
[v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
@@ -348,3 +352,4 @@ final transaction is created by calling `finish` on the builder.
[v0.5.1]: https://github.com/bitcoindevkit/bdk/compare/v0.5.0...v0.5.1
[v0.6.0]: https://github.com/bitcoindevkit/bdk/compare/v0.5.1...v0.6.0
[v0.7.0]: https://github.com/bitcoindevkit/bdk/compare/v0.6.0...v0.7.0
[v0.8.0]: https://github.com/bitcoindevkit/bdk/compare/v0.7.0...v0.8.0

View File

@@ -1,6 +1,6 @@
[package]
name = "bdk"
version = "0.8.0"
version = "0.8.1-dev"
edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org"
@@ -15,7 +15,7 @@ license = "MIT OR Apache-2.0"
bdk-macros = "^0.4"
log = "^0.4"
miniscript = "5.1"
bitcoin = { version = "^0.26", features = ["use-serde"] }
bitcoin = { version = "~0.26.2", features = ["use-serde", "base64"] }
serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" }
rand = "^0.7"
@@ -33,7 +33,7 @@ lazy_static = { version = "1.4", optional = true }
tiny-bip39 = { version = "^0.8", optional = true }
# Needed by bdk_blockchain_tests macro
bitcoincore-rpc = { version = "0.13", optional = true }
bitcoincore-rpc = { version = "0.13", optional = true }
serial_test = { version = "0.4", optional = true }
# Platform-specific dependencies
@@ -56,17 +56,22 @@ key-value-db = ["sled"]
async-interface = ["async-trait"]
all-keys = ["keys-bip39"]
keys-bip39 = ["tiny-bip39"]
rpc = ["bitcoincore-rpc"]
# Debug/Test features
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
test-electrum = ["electrum"]
test-rpc = ["rpc"]
test-esplora = ["esplora"]
test-md-docs = ["electrum"]
[dev-dependencies]
lazy_static = "1.4"
env_logger = "0.7"
base64 = "^0.11"
clap = "2.33"
serial_test = "0.4"
bitcoind = "0.10.0"
[[example]]
name = "address_validator"

View File

@@ -1,13 +1,14 @@
#!/usr/bin/env sh
echo "Starting bitcoin node."
/root/bitcoind -regtest -server -daemon -fallbackfee=0.0002 -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 -blockfilterindex=1 -peerblockfilters=1
mkdir $GITHUB_WORKSPACE/.bitcoin
/root/bitcoind -regtest -server -daemon -datadir=$GITHUB_WORKSPACE/.bitcoin -fallbackfee=0.0002 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0 -blockfilterindex=1 -peerblockfilters=1
echo "Waiting for bitcoin node."
until /root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS getblockchaininfo; do
until /root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin getblockchaininfo; do
sleep 1
done
/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS createwallet $BDK_RPC_WALLET
/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin createwallet $BDK_RPC_WALLET
echo "Generating 150 bitcoin blocks."
ADDR=$(/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS -rpcwallet=$BDK_RPC_WALLET getnewaddress)
/root/bitcoin-cli -regtest -rpcuser=$BDK_RPC_USER -rpcpassword=$BDK_RPC_PASS generatetoaddress 150 $ADDR
ADDR=$(/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin -rpcwallet=$BDK_RPC_WALLET getnewaddress)
/root/bitcoin-cli -regtest -datadir=$GITHUB_WORKSPACE/.bitcoin generatetoaddress 150 $ADDR

View File

@@ -4,7 +4,7 @@ usage() {
cat <<'EOF'
Script for running the bdk blockchain tests for a specific blockchain by starting up the backend in docker.
Usage: ./run_blockchain_tests.sh [esplora|electrum] [test name].
Usage: ./run_blockchain_tests.sh [esplora|electrum|rpc] [test name].
EOF
}
@@ -17,6 +17,7 @@ cleanup() {
if test "$id"; then
eprintln "cleaning up $blockchain docker container $id";
docker rm -fv "$id" > /dev/null;
rm /tmp/regtest-"$id".cookie;
fi
trap - EXIT INT
}
@@ -30,13 +31,17 @@ test_name="$2"
case "$blockchain" in
electrum)
eprintln "starting electrs docker container"
id="$(docker run -d -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs)"
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs:0.4.0)"
;;
esplora)
eprintln "starting esplora docker container"
id="$(docker run -d -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp -p 127.0.0.1:3002:3002/tcp bitcoindevkit/esplora)"
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp -p 127.0.0.1:3002:3002/tcp bitcoindevkit/esplora:0.4.0)"
export BDK_ESPLORA_URL=http://127.0.0.1:3002
;;
rpc)
eprintln "starting bitcoind docker container (via electrs container)"
id="$(docker run --detach -p 127.0.0.1:18443-18444:18443-18444/tcp -p 127.0.0.1:60401:60401/tcp bitcoindevkit/electrs:0.4.0)"
;;
*)
usage;
exit 1;
@@ -44,21 +49,23 @@ case "$blockchain" in
esac
# taken from https://github.com/bitcoindevkit/bitcoin-regtest-box
export BDK_RPC_AUTH=USER_PASS
export BDK_RPC_USER=admin
export BDK_RPC_PASS=passw
export BDK_RPC_AUTH=COOKIEFILE
export BDK_RPC_COOKIEFILE=/tmp/regtest-"$id".cookie
export BDK_RPC_URL=127.0.0.1:18443
export BDK_RPC_WALLET=bdk-test
export BDK_ELECTRUM_URL=tcp://127.0.0.1:60401
cli(){
docker exec -it "$id" /root/bitcoin-cli -regtest -rpcuser=admin -rpcpassword=passw $@
docker exec -it "$id" /root/bitcoin-cli -regtest -datadir=/root/.bitcoin $@
}
eprintln "running getwalletinfo until bitcoind seems to be alive"
#eprintln "running getwalletinfo until bitcoind seems to be alive"
while ! cli getwalletinfo >/dev/null; do sleep 1; done
# sleep again for good measure!
sleep 1;
cargo test --features "test-blockchains,$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests::$test_name"
# copy bitcoind cookie file to /tmp
docker cp "$id":/root/.bitcoin/regtest/.cookie /tmp/regtest-"$id".cookie
cargo test --features "test-blockchains,test-$blockchain" --no-default-features "$blockchain::bdk_blockchain_tests::$test_name"

View File

@@ -71,7 +71,7 @@ use super::{Blockchain, Capability, ConfigurableBlockchain, Progress};
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::error::Error;
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
use crate::FeeRate;
use crate::{ConfirmationTime, FeeRate};
use peer::*;
use store::*;
@@ -146,7 +146,7 @@ impl CompactFiltersBlockchain {
database: &mut D,
tx: &Transaction,
height: Option<u32>,
timestamp: u64,
timestamp: Option<u64>,
internal_max_deriv: &mut Option<u32>,
external_max_deriv: &mut Option<u32>,
) -> Result<(), Error> {
@@ -206,9 +206,8 @@ impl CompactFiltersBlockchain {
transaction: Some(tx.clone()),
received: incoming,
sent: outgoing,
height,
timestamp,
fees: inputs_sum.saturating_sub(outputs_sum),
confirmation_time: ConfirmationTime::new(height, timestamp),
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
};
info!("Saving tx {}", tx.txid);
@@ -364,8 +363,8 @@ impl Blockchain for CompactFiltersBlockchain {
);
let mut updates = database.begin_batch();
for details in database.iter_txs(false)? {
match details.height {
Some(height) if (height as usize) < last_synced_block => continue,
match details.confirmation_time {
Some(c) if (c.height as usize) < last_synced_block => continue,
_ => updates.del_tx(&details.txid, false)?,
};
}
@@ -387,7 +386,7 @@ impl Blockchain for CompactFiltersBlockchain {
database,
tx,
Some(height as u32),
0,
None,
&mut internal_max_deriv,
&mut external_max_deriv,
)?;
@@ -398,7 +397,7 @@ impl Blockchain for CompactFiltersBlockchain {
database,
tx,
None,
0,
None,
&mut internal_max_deriv,
&mut external_max_deriv,
)?;

View File

@@ -227,12 +227,12 @@ impl Peer {
Ok(Peer {
writer,
reader_thread,
responses,
reader_thread,
connected,
mempool,
network,
version,
network,
})
}

View File

@@ -43,6 +43,13 @@ pub use self::electrum::ElectrumBlockchain;
#[cfg(feature = "electrum")]
pub use self::electrum::ElectrumBlockchainConfig;
#[cfg(feature = "rpc")]
pub mod rpc;
#[cfg(feature = "rpc")]
pub use self::rpc::RpcBlockchain;
#[cfg(feature = "rpc")]
pub use self::rpc::RpcConfig;
#[cfg(feature = "esplora")]
#[cfg_attr(docsrs, doc(cfg(feature = "esplora")))]
pub mod esplora;
@@ -52,6 +59,7 @@ pub use self::esplora::EsploraBlockchain;
#[cfg(feature = "compact_filters")]
#[cfg_attr(docsrs, doc(cfg(feature = "compact_filters")))]
pub mod compact_filters;
#[cfg(feature = "compact_filters")]
pub use self::compact_filters::CompactFiltersBlockchain;
@@ -166,7 +174,7 @@ impl Progress for Sender<ProgressData> {
}
/// Type that implements [`Progress`] and drops every update received
#[derive(Clone)]
#[derive(Clone, Copy)]
pub struct NoopProgress;
/// Create a new instance of [`NoopProgress`]
@@ -181,7 +189,7 @@ impl Progress for NoopProgress {
}
/// Type that implements [`Progress`] and logs at level `INFO` every update received
#[derive(Clone)]
#[derive(Clone, Copy)]
pub struct LogProgress;
/// Create a nwe instance of [`LogProgress`]

673
src/blockchain/rpc.rs Normal file
View File

@@ -0,0 +1,673 @@
// Bitcoin Dev Kit
// Written in 2021 by Riccardo Casatta <riccardo@casatta.it>
//
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
//
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
// You may not use this file except in accordance with one or both of these
// licenses.
//! Rpc Blockchain
//!
//! Backend that gets blockchain data from Bitcoin Core RPC
//!
//! ## Example
//!
//! ```no_run
//! # use bdk::blockchain::{RpcConfig, RpcBlockchain, ConfigurableBlockchain};
//! let config = RpcConfig {
//! url: "127.0.0.1:18332".to_string(),
//! auth: bitcoincore_rpc::Auth::CookieFile("/home/user/.bitcoin/.cookie".into()),
//! network: bdk::bitcoin::Network::Testnet,
//! wallet_name: "wallet_name".to_string(),
//! skip_blocks: None,
//! };
//! let blockchain = RpcBlockchain::from_config(&config);
//! ```
use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
use crate::blockchain::{Blockchain, Capability, ConfigurableBlockchain, Progress};
use crate::database::{BatchDatabase, DatabaseUtils};
use crate::descriptor::{get_checksum, IntoWalletDescriptor};
use crate::wallet::utils::SecpCtx;
use crate::{ConfirmationTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
use bitcoincore_rpc::json::{
GetAddressInfoResultLabel, ImportMultiOptions, ImportMultiRequest,
ImportMultiRequestScriptPubkey, ImportMultiRescanSince,
};
use bitcoincore_rpc::jsonrpc::serde_json::Value;
use bitcoincore_rpc::{Auth, Client, RpcApi};
use log::debug;
use serde::Deserialize;
use std::collections::{HashMap, HashSet};
use std::str::FromStr;
/// The main struct for RPC backend implementing the [crate::blockchain::Blockchain] trait
#[derive(Debug)]
pub struct RpcBlockchain {
/// Rpc client to the node, includes the wallet name
client: Client,
/// Network used
network: Network,
/// Blockchain capabilities, cached here at startup
capabilities: HashSet<Capability>,
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
skip_blocks: Option<u32>,
/// This is a fixed Address used as a hack key to store information on the node
_storage_address: Address,
}
/// RpcBlockchain configuration options
#[derive(Debug)]
pub struct RpcConfig {
/// The bitcoin node url
pub url: String,
/// The bitcoin node authentication mechanism
pub auth: Auth,
/// The network we are using (it will be checked the bitcoin node network matches this)
pub network: Network,
/// The wallet name in the bitcoin node, consider using [wallet_name_from_descriptor] for this
pub wallet_name: String,
/// Skip this many blocks of the blockchain at the first rescan, if None the rescan is done from the genesis block
pub skip_blocks: Option<u32>,
}
impl RpcBlockchain {
fn get_node_synced_height(&self) -> Result<u32, Error> {
let info = self.client.get_address_info(&self._storage_address)?;
if let Some(GetAddressInfoResultLabel::Simple(label)) = info.labels.first() {
Ok(label
.parse::<u32>()
.unwrap_or_else(|_| self.skip_blocks.unwrap_or(0)))
} else {
Ok(self.skip_blocks.unwrap_or(0))
}
}
/// Set the synced height in the core node by using a label of a fixed address so that
/// another client with the same descriptor doesn't rescan the blockchain
fn set_node_synced_height(&self, height: u32) -> Result<(), Error> {
Ok(self
.client
.set_label(&self._storage_address, &height.to_string())?)
}
}
impl Blockchain for RpcBlockchain {
fn get_capabilities(&self) -> HashSet<Capability> {
self.capabilities.clone()
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
&self,
stop_gap: Option<usize>,
database: &mut D,
progress_update: P,
) -> Result<(), Error> {
let mut scripts_pubkeys = database.iter_script_pubkeys(Some(KeychainKind::External))?;
scripts_pubkeys.extend(database.iter_script_pubkeys(Some(KeychainKind::Internal))?);
debug!(
"importing {} script_pubkeys (some maybe already imported)",
scripts_pubkeys.len()
);
let requests: Vec<_> = scripts_pubkeys
.iter()
.map(|s| ImportMultiRequest {
timestamp: ImportMultiRescanSince::Timestamp(0),
script_pubkey: Some(ImportMultiRequestScriptPubkey::Script(&s)),
watchonly: Some(true),
..Default::default()
})
.collect();
let options = ImportMultiOptions {
rescan: Some(false),
};
// Note we use import_multi because as of bitcoin core 0.21.0 many descriptors are not supported
// https://bitcoindevkit.org/descriptors/#compatibility-matrix
//TODO maybe convenient using import_descriptor for compatible descriptor and import_multi as fallback
self.client.import_multi(&requests, Some(&options))?;
let current_height = self.get_height()?;
// min because block invalidate may cause height to go down
let node_synced = self.get_node_synced_height()?.min(current_height);
//TODO call rescan in chunks (updating node_synced_height) so that in case of
// interruption work can be partially recovered
debug!(
"rescan_blockchain from:{} to:{}",
node_synced, current_height
);
self.client
.rescan_blockchain(Some(node_synced as usize), Some(current_height as usize))?;
progress_update.update(1.0, None)?;
self.set_node_synced_height(current_height)?;
self.sync(stop_gap, database, progress_update)
}
fn sync<D: BatchDatabase, P: 'static + Progress>(
&self,
_stop_gap: Option<usize>,
db: &mut D,
_progress_update: P,
) -> Result<(), Error> {
let mut indexes = HashMap::new();
for keykind in &[KeychainKind::External, KeychainKind::Internal] {
indexes.insert(*keykind, db.get_last_index(*keykind)?.unwrap_or(0));
}
let mut known_txs: HashMap<_, _> = db
.iter_txs(true)?
.into_iter()
.map(|tx| (tx.txid, tx))
.collect();
let known_utxos: HashSet<_> = db.iter_utxos()?.into_iter().collect();
//TODO list_since_blocks would be more efficient
let current_utxo = self
.client
.list_unspent(Some(0), None, None, Some(true), None)?;
debug!("current_utxo len {}", current_utxo.len());
//TODO supported up to 1_000 txs, should use since_blocks or do paging
let list_txs = self
.client
.list_transactions(None, Some(1_000), None, Some(true))?;
let mut list_txs_ids = HashSet::new();
for tx_result in list_txs.iter().filter(|t| {
// list_txs returns all conflicting tx we want to
// filter out replaced tx => unconfirmed and not in the mempool
t.info.confirmations > 0 || self.client.get_mempool_entry(&t.info.txid).is_ok()
}) {
let txid = tx_result.info.txid;
list_txs_ids.insert(txid);
if let Some(mut known_tx) = known_txs.get_mut(&txid) {
let confirmation_time =
ConfirmationTime::new(tx_result.info.blockheight, tx_result.info.blocktime);
if confirmation_time != known_tx.confirmation_time {
// reorg may change tx height
debug!(
"updating tx({}) confirmation time to: {:?}",
txid, confirmation_time
);
known_tx.confirmation_time = confirmation_time;
db.set_tx(&known_tx)?;
}
} else {
//TODO check there is already the raw tx in db?
let tx_result = self.client.get_transaction(&txid, Some(true))?;
let tx: Transaction = deserialize(&tx_result.hex)?;
let mut received = 0u64;
let mut sent = 0u64;
for output in tx.output.iter() {
if let Ok(Some((kind, index))) =
db.get_path_from_script_pubkey(&output.script_pubkey)
{
if index > *indexes.get(&kind).unwrap() {
indexes.insert(kind, index);
}
received += output.value;
}
}
for input in tx.input.iter() {
if let Some(previous_output) = db.get_previous_output(&input.previous_output)? {
sent += previous_output.value;
}
}
let td = TransactionDetails {
transaction: Some(tx),
txid: tx_result.info.txid,
confirmation_time: ConfirmationTime::new(
tx_result.info.blockheight,
tx_result.info.blocktime,
),
received,
sent,
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
};
debug!(
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
td.txid, tx_result.fee, td.fee
);
db.set_tx(&td)?;
}
}
for known_txid in known_txs.keys() {
if !list_txs_ids.contains(known_txid) {
debug!("removing tx: {}", known_txid);
db.del_tx(known_txid, false)?;
}
}
let current_utxos: HashSet<_> = current_utxo
.into_iter()
.map(|u| {
Ok(LocalUtxo {
outpoint: OutPoint::new(u.txid, u.vout),
keychain: db
.get_path_from_script_pubkey(&u.script_pub_key)?
.ok_or(Error::TransactionNotFound)?
.0,
txout: TxOut {
value: u.amount.as_sat(),
script_pubkey: u.script_pub_key,
},
})
})
.collect::<Result<_, Error>>()?;
let spent: HashSet<_> = known_utxos.difference(&current_utxos).collect();
for s in spent {
debug!("removing utxo: {:?}", s);
db.del_utxo(&s.outpoint)?;
}
let received: HashSet<_> = current_utxos.difference(&known_utxos).collect();
for s in received {
debug!("adding utxo: {:?}", s);
db.set_utxo(s)?;
}
for (keykind, index) in indexes {
debug!("{:?} max {}", keykind, index);
db.set_last_index(keykind, index)?;
}
Ok(())
}
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(Some(self.client.get_raw_transaction(txid, None)?))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
}
fn get_height(&self) -> Result<u32, Error> {
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
}
fn estimate_fee(&self, target: usize) -> Result<FeeRate, Error> {
let sat_per_kb = self
.client
.estimate_smart_fee(target as u16, None)?
.fee_rate
.ok_or(Error::FeeRateUnavailable)?
.as_sat() as f64;
Ok(FeeRate::from_sat_per_vb((sat_per_kb / 1000f64) as f32))
}
}
impl ConfigurableBlockchain for RpcBlockchain {
type Config = RpcConfig;
/// Returns RpcBlockchain backend creating an RPC client to a specific wallet named as the descriptor's checksum
/// if it's the first time it creates the wallet in the node and upon return is granted the wallet is loaded
fn from_config(config: &Self::Config) -> Result<Self, Error> {
let wallet_name = config.wallet_name.clone();
let wallet_url = format!("{}/wallet/{}", config.url, &wallet_name);
debug!("connecting to {} auth:{:?}", wallet_url, config.auth);
let client = Client::new(wallet_url, config.auth.clone())?;
let loaded_wallets = client.list_wallets()?;
if loaded_wallets.contains(&wallet_name) {
debug!("wallet already loaded {:?}", wallet_name);
} else {
let existing_wallets = list_wallet_dir(&client)?;
if existing_wallets.contains(&wallet_name) {
client.load_wallet(&wallet_name)?;
debug!("wallet loaded {:?}", wallet_name);
} else {
client.create_wallet(&wallet_name, Some(true), None, None, None)?;
debug!("wallet created {:?}", wallet_name);
}
}
let blockchain_info = client.get_blockchain_info()?;
let network = match blockchain_info.chain.as_str() {
"main" => Network::Bitcoin,
"test" => Network::Testnet,
"regtest" => Network::Regtest,
"signet" => Network::Signet,
_ => return Err(Error::Generic("Invalid network".to_string())),
};
if network != config.network {
return Err(Error::InvalidNetwork {
requested: config.network,
found: network,
});
}
let mut capabilities: HashSet<_> = vec![Capability::FullHistory].into_iter().collect();
let rpc_version = client.version()?;
if rpc_version >= 210_000 {
let info: HashMap<String, Value> = client.call("getindexinfo", &[]).unwrap();
if info.contains_key("txindex") {
capabilities.insert(Capability::GetAnyTx);
capabilities.insert(Capability::AccurateFees);
}
}
// this is just a fixed address used only to store a label containing the synced height in the node
let mut storage_address =
Address::from_str("bc1qst0rewf0wm4kw6qn6kv0e5tc56nkf9yhcxlhqv").unwrap();
storage_address.network = network;
Ok(RpcBlockchain {
client,
network,
capabilities,
_storage_address: storage_address,
skip_blocks: config.skip_blocks,
})
}
}
/// Deterministically generate a unique name given the descriptors defining the wallet
pub fn wallet_name_from_descriptor<T>(
descriptor: T,
change_descriptor: Option<T>,
network: Network,
secp: &SecpCtx,
) -> Result<String, Error>
where
T: IntoWalletDescriptor,
{
//TODO check descriptors contains only public keys
let descriptor = descriptor
.into_wallet_descriptor(&secp, network)?
.0
.to_string();
let mut wallet_name = get_checksum(&descriptor[..descriptor.find('#').unwrap()])?;
if let Some(change_descriptor) = change_descriptor {
let change_descriptor = change_descriptor
.into_wallet_descriptor(&secp, network)?
.0
.to_string();
wallet_name.push_str(
get_checksum(&change_descriptor[..change_descriptor.find('#').unwrap()])?.as_str(),
);
}
Ok(wallet_name)
}
/// return the wallets available in default wallet directory
//TODO use bitcoincore_rpc method when PR #179 lands
fn list_wallet_dir(client: &Client) -> Result<Vec<String>, Error> {
#[derive(Deserialize)]
struct Name {
name: String,
}
#[derive(Deserialize)]
struct CallResult {
wallets: Vec<Name>,
}
let result: CallResult = client.call("listwalletdir", &[])?;
Ok(result.wallets.into_iter().map(|n| n.name).collect())
}
#[cfg(feature = "test-blockchains")]
crate::bdk_blockchain_tests! {
fn test_instance() -> RpcBlockchain {
let url = std::env::var("BDK_RPC_URL").unwrap_or_else(|_| "127.0.0.1:18443".to_string());
let url = format!("http://{}", url);
// TODO same code in `fn get_auth` in testutils, make it public there
let auth = match std::env::var("BDK_RPC_AUTH").as_ref().map(String::as_ref) {
Ok("USER_PASS") => Auth::UserPass(
std::env::var("BDK_RPC_USER").unwrap(),
std::env::var("BDK_RPC_PASS").unwrap(),
),
_ => Auth::CookieFile(std::path::PathBuf::from(
std::env::var("BDK_RPC_COOKIEFILE")
.unwrap_or_else(|_| "/home/user/.bitcoin/regtest/.cookie".to_string()),
)),
};
let config = RpcConfig {
url,
auth,
network: Network::Regtest,
wallet_name: format!("client-wallet-test-{:?}", std::time::SystemTime::now() ),
skip_blocks: None,
};
RpcBlockchain::from_config(&config).unwrap()
}
}
#[cfg(feature = "test-rpc")]
#[cfg(test)]
mod test {
use super::{RpcBlockchain, RpcConfig};
use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::{Address, Amount, Network, Transaction};
use crate::blockchain::rpc::wallet_name_from_descriptor;
use crate::blockchain::{noop_progress, Blockchain, Capability, ConfigurableBlockchain};
use crate::database::MemoryDatabase;
use crate::wallet::AddressIndex;
use crate::Wallet;
use bitcoin::secp256k1::Secp256k1;
use bitcoin::Txid;
use bitcoincore_rpc::json::CreateRawTransactionInput;
use bitcoincore_rpc::RawTx;
use bitcoincore_rpc::{Auth, RpcApi};
use bitcoind::BitcoinD;
use std::collections::HashMap;
fn create_rpc(
bitcoind: &BitcoinD,
desc: &str,
network: Network,
) -> Result<RpcBlockchain, crate::Error> {
let secp = Secp256k1::new();
let wallet_name = wallet_name_from_descriptor(desc, None, network, &secp).unwrap();
let config = RpcConfig {
url: bitcoind.rpc_url(),
auth: Auth::CookieFile(bitcoind.config.cookie_file.clone()),
network,
wallet_name,
skip_blocks: None,
};
RpcBlockchain::from_config(&config)
}
fn create_bitcoind(args: Vec<String>) -> BitcoinD {
let exe = std::env::var("BITCOIND_EXE").unwrap();
bitcoind::BitcoinD::with_args(exe, args, false, bitcoind::P2P::No).unwrap()
}
const DESCRIPTOR_PUB: &'static str = "wpkh(tpubD6NzVbkrYhZ4X2yy78HWrr1M9NT8dKeWfzNiQqDdMqqa9UmmGztGGz6TaLFGsLfdft5iu32gxq1T4eMNxExNNWzVCpf9Y6JZi5TnqoC9wJq/*)";
const DESCRIPTOR_PRIV: &'static str = "wpkh(tprv8ZgxMBicQKsPdZxBDUcvTSMEaLwCTzTc6gmw8KBKwa3BJzWzec4g6VUbQBHJcutDH6mMEmBeVyN27H1NF3Nu8isZ1Sts4SufWyfLE6Mf1MB/*)";
#[test]
fn test_rpc_wallet_setup() {
env_logger::try_init().unwrap();
let bitcoind = create_bitcoind(vec![]);
let node_address = bitcoind.client.get_new_address(None, None).unwrap();
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let db = MemoryDatabase::new();
let wallet = Wallet::new(DESCRIPTOR_PRIV, None, Network::Regtest, db, blockchain).unwrap();
wallet.sync(noop_progress(), None).unwrap();
generate(&bitcoind, 101);
wallet.sync(noop_progress(), None).unwrap();
let address = wallet.get_address(AddressIndex::New).unwrap();
let expected_address = "bcrt1q8dyvgt4vhr8ald4xuwewcxhdjha9a5k78wxm5t";
assert_eq!(expected_address, address.to_string());
send_to_address(&bitcoind, &address, 100_000);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
let mut builder = wallet.build_tx();
builder.add_recipient(node_address.script_pubkey(), 50_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let tx = psbt.extract_tx();
wallet.broadcast(tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(
wallet.get_balance().unwrap(),
100_000 - 50_000 - details.fee.unwrap_or(0)
);
drop(wallet);
// test skip_blocks
generate(&bitcoind, 5);
let config = RpcConfig {
url: bitcoind.rpc_url(),
auth: Auth::CookieFile(bitcoind.config.cookie_file.clone()),
network: Network::Regtest,
wallet_name: "another-name".to_string(),
skip_blocks: Some(103),
};
let blockchain_skip = RpcBlockchain::from_config(&config).unwrap();
let db = MemoryDatabase::new();
let wallet_skip =
Wallet::new(DESCRIPTOR_PRIV, None, Network::Regtest, db, blockchain_skip).unwrap();
wallet_skip.sync(noop_progress(), None).unwrap();
send_to_address(&bitcoind, &address, 100_000);
wallet_skip.sync(noop_progress(), None).unwrap();
assert_eq!(wallet_skip.get_balance().unwrap(), 100_000);
}
#[test]
fn test_rpc_from_config() {
let bitcoind = create_bitcoind(vec![]);
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest);
assert!(blockchain.is_ok());
let blockchain = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Testnet);
assert!(blockchain.is_err(), "wrong network doesn't error");
}
#[test]
fn test_rpc_capabilities_get_tx() {
let bitcoind = create_bitcoind(vec![]);
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let capabilities = rpc.get_capabilities();
assert!(capabilities.contains(&Capability::FullHistory) && capabilities.len() == 1);
let bitcoind_indexed = create_bitcoind(vec!["-txindex".to_string()]);
let rpc_indexed = create_rpc(&bitcoind_indexed, DESCRIPTOR_PUB, Network::Regtest).unwrap();
assert_eq!(rpc_indexed.get_capabilities().len(), 3);
let address = generate(&bitcoind_indexed, 101);
let txid = send_to_address(&bitcoind_indexed, &address, 100_000);
assert!(rpc_indexed.get_tx(&txid).unwrap().is_some());
assert!(rpc.get_tx(&txid).is_err());
}
#[test]
fn test_rpc_estimate_fee_get_height() {
let bitcoind = create_bitcoind(vec![]);
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let result = rpc.estimate_fee(2);
assert!(result.is_err());
let address = generate(&bitcoind, 100);
// create enough tx so that core give some fee estimation
for _ in 0..15 {
let _ = bitcoind.client.generate_to_address(1, &address).unwrap();
for _ in 0..2 {
send_to_address(&bitcoind, &address, 100_000);
}
}
let result = rpc.estimate_fee(2);
assert!(result.is_ok());
assert_eq!(rpc.get_height().unwrap(), 115);
}
#[test]
fn test_rpc_node_synced_height() {
let bitcoind = create_bitcoind(vec![]);
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let synced_height = rpc.get_node_synced_height().unwrap();
assert_eq!(synced_height, 0);
rpc.set_node_synced_height(1).unwrap();
let synced_height = rpc.get_node_synced_height().unwrap();
assert_eq!(synced_height, 1);
}
#[test]
fn test_rpc_broadcast() {
let bitcoind = create_bitcoind(vec![]);
let rpc = create_rpc(&bitcoind, DESCRIPTOR_PUB, Network::Regtest).unwrap();
let address = generate(&bitcoind, 101);
let utxo = bitcoind
.client
.list_unspent(None, None, None, None, None)
.unwrap();
let input = CreateRawTransactionInput {
txid: utxo[0].txid,
vout: utxo[0].vout,
sequence: None,
};
let out: HashMap<_, _> = vec![(
address.to_string(),
utxo[0].amount - Amount::from_sat(100_000),
)]
.into_iter()
.collect();
let tx = bitcoind
.client
.create_raw_transaction(&[input], &out, None, None)
.unwrap();
let signed_tx = bitcoind
.client
.sign_raw_transaction_with_wallet(tx.raw_hex(), None, None)
.unwrap();
let parsed_tx: Transaction = deserialize(&signed_tx.hex).unwrap();
rpc.broadcast(&parsed_tx).unwrap();
assert!(bitcoind
.client
.get_raw_mempool()
.unwrap()
.contains(&tx.txid()));
}
#[test]
fn test_rpc_wallet_name() {
let secp = Secp256k1::new();
let name =
wallet_name_from_descriptor(DESCRIPTOR_PUB, None, Network::Regtest, &secp).unwrap();
assert_eq!("tmg7aqay", name);
}
fn generate(bitcoind: &BitcoinD, blocks: u64) -> Address {
let address = bitcoind.client.get_new_address(None, None).unwrap();
bitcoind
.client
.generate_to_address(blocks, &address)
.unwrap();
address
}
fn send_to_address(bitcoind: &BitcoinD, address: &Address, amount: u64) -> Txid {
bitcoind
.client
.send_to_address(
&address,
Amount::from_sat(amount),
None,
None,
None,
None,
None,
None,
)
.unwrap()
}
}

View File

@@ -21,7 +21,7 @@ use bitcoin::{BlockHeader, OutPoint, Script, Transaction, Txid};
use super::*;
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::error::Error;
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
use crate::types::{ConfirmationTime, KeychainKind, LocalUtxo, TransactionDetails};
use crate::wallet::time::Instant;
use crate::wallet::utils::ChunksIterator;
@@ -147,18 +147,19 @@ pub trait ElectrumLikeSync {
// save any tx details not in db but in history_txs_id or with different height/timestamp
for txid in history_txs_id.iter() {
let height = txid_height.get(txid).cloned().flatten();
let timestamp = *new_timestamps.get(txid).unwrap_or(&0u64);
let timestamp = new_timestamps.get(txid).cloned();
if let Some(tx_details) = txs_details_in_db.get(txid) {
// check if height matches, otherwise updates it
if tx_details.height != height {
// check if tx height matches, otherwise updates it. timestamp is not in the if clause
// because we are not asking headers for confirmed tx we know about
if tx_details.confirmation_time.as_ref().map(|c| c.height) != height {
let confirmation_time = ConfirmationTime::new(height, timestamp);
let mut new_tx_details = tx_details.clone();
new_tx_details.height = height;
new_tx_details.timestamp = timestamp;
new_tx_details.confirmation_time = confirmation_time;
batch.set_tx(&new_tx_details)?;
}
} else {
save_transaction_details_and_utxos(
&txid,
txid,
db,
timestamp,
height,
@@ -171,7 +172,7 @@ pub trait ElectrumLikeSync {
// remove any tx details in db but not in history_txs_id
for txid in txs_details_in_db.keys() {
if !history_txs_id.contains(txid) {
batch.del_tx(&txid, false)?;
batch.del_tx(txid, false)?;
}
}
@@ -238,9 +239,13 @@ pub trait ElectrumLikeSync {
chunk_size: usize,
) -> Result<HashMap<Txid, u64>, Error> {
let mut txid_timestamp = HashMap::new();
let txid_in_db_with_conf: HashSet<_> = txs_details_in_db
.values()
.filter_map(|details| details.confirmation_time.as_ref().map(|_| details.txid))
.collect();
let needed_txid_height: HashMap<&Txid, u32> = txid_height
.iter()
.filter(|(t, _)| txs_details_in_db.get(*t).is_none())
.filter(|(t, _)| !txid_in_db_with_conf.contains(*t))
.filter_map(|(t, o)| o.map(|h| (t, h)))
.collect();
let needed_heights: HashSet<u32> = needed_txid_height.values().cloned().collect();
@@ -292,7 +297,7 @@ pub trait ElectrumLikeSync {
fn save_transaction_details_and_utxos<D: BatchDatabase>(
txid: &Txid,
db: &mut D,
timestamp: u64,
timestamp: Option<u64>,
height: Option<u32>,
updates: &mut dyn BatchOperations,
utxo_deps: &HashMap<OutPoint, OutPoint>,
@@ -329,7 +334,7 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
// removes conflicting UTXO if any (generated from same inputs, like for example RBF)
if let Some(outpoint) = utxo_deps.get(&input.previous_output) {
updates.del_utxo(&outpoint)?;
updates.del_utxo(outpoint)?;
}
}
@@ -355,9 +360,8 @@ fn save_transaction_details_and_utxos<D: BatchDatabase>(
transaction: Some(tx),
received: incoming,
sent: outgoing,
height,
timestamp,
fees: inputs_sum.saturating_sub(outputs_sum), /* if the tx is a coinbase, fees would be negative */
confirmation_time: ConfirmationTime::new(height, timestamp),
fee: Some(inputs_sum.saturating_sub(outputs_sum)), /* if the tx is a coinbase, fees would be negative */
};
updates.set_tx(&tx_details)?;

View File

@@ -320,7 +320,7 @@ impl Database for Tree {
.map(|b| -> Result<_, Error> {
let mut txdetails: TransactionDetails = serde_json::from_slice(&b)?;
if include_raw {
txdetails.transaction = self.get_raw_tx(&txid)?;
txdetails.transaction = self.get_raw_tx(txid)?;
}
Ok(txdetails)

View File

@@ -394,7 +394,7 @@ impl Database for MemoryDatabase {
Ok(self.map.get(&key).map(|b| {
let mut txdetails: TransactionDetails = b.downcast_ref().cloned().unwrap();
if include_raw {
txdetails.transaction = self.get_raw_tx(&txid).unwrap();
txdetails.transaction = self.get_raw_tx(txid).unwrap();
}
txdetails
@@ -429,8 +429,8 @@ impl BatchDatabase for MemoryDatabase {
}
fn commit_batch(&mut self, mut batch: Self::Batch) -> Result<(), Error> {
for key in batch.deleted_keys {
self.map.remove(&key);
for key in batch.deleted_keys.iter() {
self.map.remove(key);
}
self.map.append(&mut batch.map);
Ok(())
@@ -473,18 +473,18 @@ macro_rules! populate_test_db {
};
let txid = tx.txid();
let height = tx_meta
.min_confirmations
.map(|conf| current_height.unwrap().checked_sub(conf as u32).unwrap());
let confirmation_time = tx_meta.min_confirmations.map(|conf| ConfirmationTime {
height: current_height.unwrap().checked_sub(conf as u32).unwrap(),
timestamp: 0,
});
let tx_details = TransactionDetails {
transaction: Some(tx.clone()),
txid,
timestamp: 0,
height,
fee: Some(0),
received: 0,
sent: 0,
fees: 0,
confirmation_time,
};
db.set_tx(&tx_details).unwrap();

View File

@@ -314,11 +314,13 @@ pub mod test {
let mut tx_details = TransactionDetails {
transaction: Some(tx),
txid,
timestamp: 123456,
received: 1337,
sent: 420420,
fees: 140,
height: Some(1000),
fee: Some(140),
confirmation_time: Some(ConfirmationTime {
timestamp: 123456,
height: 1000,
}),
};
tree.set_tx(&tx_details).unwrap();

View File

@@ -128,11 +128,11 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
let (pk, _, networks) = if self.0.is_witness() {
let desciptor_key: DescriptorKey<miniscript::Segwitv0> =
pk.clone().into_descriptor_key()?;
desciptor_key.extract(&secp)?
desciptor_key.extract(secp)?
} else {
let desciptor_key: DescriptorKey<miniscript::Legacy> =
pk.clone().into_descriptor_key()?;
desciptor_key.extract(&secp)?
desciptor_key.extract(secp)?
};
if networks.contains(&network) {

View File

@@ -336,7 +336,7 @@ impl Satisfaction {
items.push(inner_index);
let conditions_set = other_conditions
.values()
.fold(HashSet::new(), |set, i| set.union(&i).cloned().collect());
.fold(HashSet::new(), |set, i| set.union(i).cloned().collect());
conditions.insert(inner_index, conditions_set);
}
}
@@ -1007,7 +1007,6 @@ mod test {
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
use super::*;
use crate::bitcoin::consensus::deserialize;
use crate::descriptor::derived::AsDerived;
use crate::descriptor::policy::SatisfiableItem::{Multisig, Signature, Thresh};
use crate::keys::{DescriptorKey, IntoDescriptorKey};
@@ -1031,8 +1030,8 @@ mod test {
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
let path = bip32::DerivationPath::from_str(path).unwrap();
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
let tpub = bip32::ExtendedPubKey::from_private(&secp, &tprv);
let fingerprint = tprv.fingerprint(&secp);
let tpub = bip32::ExtendedPubKey::from_private(secp, &tprv);
let fingerprint = tprv.fingerprint(secp);
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
let pubkey = (tpub, path).into_descriptor_key().unwrap();
@@ -1475,7 +1474,7 @@ mod test {
let signers_container = Arc::new(SignersContainer::from(keymap));
let psbt: Psbt = deserialize(&base64::decode(ALICE_SIGNED_PSBT).unwrap()).unwrap();
let psbt = Psbt::from_str(ALICE_SIGNED_PSBT).unwrap();
let policy_alice_psbt = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
@@ -1490,7 +1489,7 @@ mod test {
)
);
let psbt: Psbt = deserialize(&base64::decode(BOB_SIGNED_PSBT).unwrap()).unwrap();
let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap();
let policy_bob_psbt = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.unwrap()
@@ -1504,7 +1503,7 @@ mod test {
)
);
let psbt: Psbt = deserialize(&base64::decode(ALICE_BOB_SIGNED_PSBT).unwrap()).unwrap();
let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap();
let policy_alice_bob_psbt = wallet_desc
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
.unwrap()
@@ -1545,8 +1544,7 @@ mod test {
addr.to_string()
);
let psbt: Psbt =
deserialize(&base64::decode(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).unwrap()).unwrap();
let psbt = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED).unwrap();
let build_sat = BuildSatisfaction::PsbtTimelocks {
psbt: &psbt,
@@ -1584,9 +1582,7 @@ mod test {
);
//println!("{}", serde_json::to_string(&policy_expired).unwrap());
let psbt_signed: Psbt =
deserialize(&base64::decode(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).unwrap())
.unwrap();
let psbt_signed = Psbt::from_str(PSBT_POLICY_CONSIDER_TIMELOCK_EXPIRED_SIGNED).unwrap();
let build_sat_expired_signed = BuildSatisfaction::PsbtTimelocks {
psbt: &psbt_signed,

View File

@@ -11,6 +11,7 @@
use std::fmt;
use crate::bitcoin::Network;
use crate::{descriptor, wallet, wallet::address_validator};
use bitcoin::OutPoint;
@@ -64,6 +65,8 @@ pub enum Error {
/// Required fee absolute value (satoshi)
required: u64,
},
/// Node doesn't have data to estimate a fee rate
FeeRateUnavailable,
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
/// explicit origin provided
@@ -80,7 +83,13 @@ pub enum Error {
InvalidPolicyPathError(crate::descriptor::policy::PolicyError),
/// Signing error
Signer(crate::wallet::signer::SignerError),
/// Invalid network
InvalidNetwork {
/// requested network, for example what is given as bdk-cli option
requested: Network,
/// found network, for example the network of the bitcoin node
found: Network,
},
/// Progress value must be between `0.0` (included) and `100.0` (included)
InvalidProgressValue(f32),
/// Progress update error (maybe the channel has been closed)
@@ -106,6 +115,8 @@ pub enum Error {
Hex(bitcoin::hashes::hex::Error),
/// Partially signed bitcoin transaction error
Psbt(bitcoin::util::psbt::Error),
/// Partially signed bitcoin transaction parseerror
PsbtParse(bitcoin::util::psbt::PsbtParseError),
//KeyMismatch(bitcoin::secp256k1::PublicKey, bitcoin::secp256k1::PublicKey),
//MissingInputUTXO(usize),
@@ -126,6 +137,9 @@ pub enum Error {
#[cfg(feature = "key-value-db")]
/// Sled database error
Sled(sled::Error),
#[cfg(feature = "rpc")]
/// Rpc client error
Rpc(bitcoincore_rpc::Error),
}
impl fmt::Display for Error {
@@ -172,6 +186,7 @@ impl_error!(bitcoin::secp256k1::Error, Secp256k1);
impl_error!(serde_json::Error, Json);
impl_error!(bitcoin::hashes::hex::Error, Hex);
impl_error!(bitcoin::util::psbt::Error, Psbt);
impl_error!(bitcoin::util::psbt::PsbtParseError, PsbtParse);
#[cfg(feature = "electrum")]
impl_error!(electrum_client::Error, Electrum);
@@ -179,6 +194,8 @@ impl_error!(electrum_client::Error, Electrum);
impl_error!(crate::blockchain::esplora::EsploraError, Esplora);
#[cfg(feature = "key-value-db")]
impl_error!(sled::Error, Sled);
#[cfg(feature = "rpc")]
impl_error!(bitcoincore_rpc::Error, Rpc);
#[cfg(feature = "compact_filters")]
impl From<crate::blockchain::compact_filters::CompactFiltersError> for Error {

View File

@@ -104,8 +104,6 @@ fn main() -> Result<(), bdk::Error> {
### Example
```no_run
use base64::decode;
use bdk::{FeeRate, Wallet};
use bdk::database::MemoryDatabase;
use bdk::blockchain::{noop_progress, ElectrumBlockchain};
@@ -138,7 +136,7 @@ fn main() -> Result<(), bdk::Error> {
};
println!("Transaction details: {:#?}", details);
println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
println!("Unsigned PSBT: {}", &psbt);
Ok(())
}
@@ -150,8 +148,9 @@ fn main() -> Result<(), bdk::Error> {
//!
//! ### Example
//! ```no_run
//! use base64::decode;
//! use bitcoin::consensus::deserialize;
//! use std::str::FromStr;
//!
//! use bitcoin::util::psbt::PartiallySignedTransaction as Psbt;
//!
//! use bdk::{Wallet, SignOptions};
//! use bdk::database::MemoryDatabase;
@@ -165,7 +164,7 @@ fn main() -> Result<(), bdk::Error> {
//! )?;
//!
//! let psbt = "...";
//! let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
//! let mut psbt = Psbt::from_str(psbt)?;
//!
//! let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
//!
@@ -223,6 +222,9 @@ extern crate bdk_macros;
#[cfg(feature = "compact_filters")]
extern crate lazy_static;
#[cfg(feature = "rpc")]
pub extern crate bitcoincore_rpc;
#[cfg(feature = "electrum")]
pub extern crate electrum_client;

View File

@@ -41,12 +41,12 @@ impl PsbtUtils for Psbt {
#[cfg(test)]
mod test {
use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::TxIn;
use crate::psbt::Psbt;
use crate::wallet::test::{get_funded_wallet, get_test_wpkh};
use crate::wallet::AddressIndex;
use crate::SignOptions;
use std::str::FromStr;
// from bip 174
const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
@@ -54,7 +54,7 @@ mod test {
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_psbt_input_legacy() {
let psbt_bip: Psbt = deserialize(&base64::decode(PSBT_STR).unwrap()).unwrap();
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
@@ -71,7 +71,7 @@ mod test {
#[test]
#[should_panic(expected = "InputIndexOutOfRange")]
fn test_psbt_malformed_psbt_input_segwit() {
let psbt_bip: Psbt = deserialize(&base64::decode(PSBT_STR).unwrap()).unwrap();
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();
@@ -103,7 +103,7 @@ mod test {
#[test]
fn test_psbt_sign_with_finalized() {
let psbt_bip: Psbt = deserialize(&base64::decode(PSBT_STR).unwrap()).unwrap();
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
let (wallet, _, _) = get_funded_wallet(get_test_wpkh());
let send_to = wallet.get_address(AddressIndex::New).unwrap();
let mut builder = wallet.build_tx();

View File

@@ -346,7 +346,6 @@ macro_rules! bdk_blockchain_tests {
use $crate::database::MemoryDatabase;
use $crate::types::KeychainKind;
use $crate::{Wallet, FeeRate};
use $crate::wallet::AddressIndex::New;
use $crate::testutils;
use $crate::serial_test::serial;
@@ -370,6 +369,10 @@ macro_rules! bdk_blockchain_tests {
let test_client = TestClient::default();
let wallet = get_wallet_from_descriptors(&descriptors);
// rpc need to call import_multi before receiving any tx, otherwise will not see tx in the mempool
#[cfg(feature = "test-rpc")]
wallet.sync(noop_progress(), None).unwrap();
(wallet, descriptors, test_client)
}
@@ -386,14 +389,14 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_unspent().unwrap()[0].keychain, KeychainKind::External);
assert_eq!(wallet.get_balance().unwrap(), 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];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
assert_eq!(list_tx_item.received, 50_000, "incorrect received");
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation time");
}
#[test]
@@ -410,8 +413,8 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 100_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.get_balance().unwrap(), 100_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2, "incorrect number of txs");
}
#[test]
@@ -428,8 +431,8 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1, "incorrect number of txs");
}
#[test]
@@ -443,15 +446,15 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 105_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 3);
assert_eq!(wallet.get_balance().unwrap(), 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");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 105_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
assert_eq!(list_tx_item.received, 105_000, "incorrect received");
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation_time");
}
#[test]
@@ -468,9 +471,9 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.list_unspent().unwrap().len(), 2);
assert_eq!(wallet.get_balance().unwrap(), 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");
}
#[test]
@@ -490,7 +493,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
}
#[test]
@@ -504,29 +507,29 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
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.list_unspent().unwrap().len(), 1, "incorrect unspent");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
assert_eq!(list_tx_item.received, 50_000, "incorrect received");
assert_eq!(list_tx_item.sent, 0, "incorrect sent");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation_time");
let new_txid = test_client.bump_fee(&txid);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
assert_eq!(wallet.get_balance().unwrap(), 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");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, new_txid);
assert_eq!(list_tx_item.received, 50_000);
assert_eq!(list_tx_item.sent, 0);
assert_eq!(list_tx_item.height, None);
assert_eq!(list_tx_item.txid, new_txid, "incorrect txid after bump");
assert_eq!(list_tx_item.received, 50_000, "incorrect received after bump");
assert_eq!(list_tx_item.sent, 0, "incorrect sent after bump");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect height after bump");
}
// FIXME: I would like this to be cfg_attr(not(feature = "test-esplora"), ignore) but it
@@ -543,24 +546,24 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.list_transactions(false).unwrap().len(), 1);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
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.list_unspent().unwrap().len(), 1, "incorrect number of unspents");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert!(list_tx_item.height.is_some());
assert_eq!(list_tx_item.txid, txid, "incorrect txid");
assert!(list_tx_item.confirmation_time.is_some(), "incorrect confirmation_time");
// Invalidate 1 block
test_client.invalidate(1);
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance after invalidate");
let list_tx_item = &wallet.list_transactions(false).unwrap()[0];
assert_eq!(list_tx_item.txid, txid);
assert_eq!(list_tx_item.height, None);
assert_eq!(list_tx_item.txid, txid, "incorrect txid after invalidate");
assert_eq!(list_tx_item.confirmation_time, None, "incorrect confirmation time after invalidate");
}
#[test]
@@ -575,7 +578,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
@@ -587,16 +590,17 @@ macro_rules! bdk_blockchain_tests {
wallet.broadcast(tx).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received);
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after send");
assert_eq!(wallet.list_transactions(false).unwrap().len(), 2);
assert_eq!(wallet.list_unspent().unwrap().len(), 1);
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");
}
#[test]
#[serial]
fn test_sync_outgoing_from_scratch() {
fn test_update_confirmation_time_after_generate() {
let (wallet, descriptors, mut test_client) = init_single_sig();
println!("{}", descriptors.0);
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@@ -604,32 +608,62 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap(), 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();
assert!(details.confirmation_time.is_none());
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
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();
assert!(details.confirmation_time.is_some());
}
#[test]
#[serial]
fn test_sync_outgoing_from_scratch() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
let received_txid = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey(), 25_000);
let (mut psbt, details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction");
let sent_txid = wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), details.received);
assert_eq!(wallet.get_balance().unwrap(), details.received, "incorrect balance after receive");
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
wallet.sync(noop_progress(), None).unwrap();
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
let tx_map = wallet.list_transactions(false).unwrap().into_iter().map(|tx| (tx.txid, tx)).collect::<std::collections::HashMap<_, _>>();
let received = tx_map.get(&received_txid).unwrap();
assert_eq!(received.received, 50_000);
assert_eq!(received.sent, 0);
assert_eq!(received.received, 50_000, "incorrect received from receiver");
assert_eq!(received.sent, 0, "incorrect sent from receiver");
let sent = tx_map.get(&sent_txid).unwrap();
assert_eq!(sent.received, details.received);
assert_eq!(sent.sent, details.sent);
assert_eq!(sent.fees, details.fees);
assert_eq!(sent.received, details.received, "incorrect received from sender");
assert_eq!(sent.sent, details.sent, "incorrect sent from sender");
assert_eq!(sent.fee.unwrap_or(0), details.fee.unwrap_or(0), "incorrect fees from sender");
}
#[test]
@@ -643,7 +677,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut total_sent = 0;
for _ in 0..5 {
@@ -656,21 +690,27 @@ macro_rules! bdk_blockchain_tests {
wallet.sync(noop_progress(), None).unwrap();
total_sent += 5_000 + details.fees;
total_sent += 5_000 + details.fee.unwrap_or(0);
}
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance after chain");
// empty wallet
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(feature = "rpc")] // rpc cannot see mempool tx before importmulti
test_client.generate(1, Some(node_addr));
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent);
assert_eq!(wallet.get_balance().unwrap(), 50_000 - total_sent, "incorrect balance empty wallet");
}
#[test]
#[serial]
fn test_sync_bump_fee() {
fn test_sync_bump_fee_basic() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
@@ -679,7 +719,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 5_000).enable_rbf();
@@ -688,8 +728,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - details.fees - 5_000);
assert_eq!(wallet.get_balance().unwrap(), details.received);
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");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
@@ -698,10 +738,10 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000 - new_details.fees - 5_000);
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
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!(new_details.fees > details.fees);
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
}
#[test]
@@ -715,7 +755,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
assert_eq!(wallet.get_balance().unwrap(), 50_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
@@ -724,8 +764,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 1_000 - details.fees);
assert_eq!(wallet.get_balance().unwrap(), details.received);
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");
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(5.0));
@@ -734,15 +774,15 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(new_details.received, 0);
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after change removal");
assert_eq!(new_details.received, 0, "incorrect received after change removal");
assert!(new_details.fees > details.fees);
assert!(new_details.fee.unwrap_or(0) > details.fee.unwrap_or(0), "incorrect fees");
}
#[test]
#[serial]
fn test_sync_bump_fee_add_input() {
fn test_sync_bump_fee_add_input_simple() {
let (wallet, descriptors, mut test_client) = init_single_sig();
let node_addr = test_client.get_node_address(None);
@@ -751,7 +791,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
@@ -760,8 +800,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
assert_eq!(details.received, 1_000 - details.fees);
assert_eq!(wallet.get_balance().unwrap(), 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();
builder.fee_rate(FeeRate::from_sat_per_vb(10.0));
@@ -770,8 +810,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000);
assert_eq!(wallet.get_balance().unwrap(), new_details.received);
assert_eq!(new_details.sent, 75_000, "incorrect sent");
assert_eq!(wallet.get_balance().unwrap(), new_details.received, "incorrect balance after add input");
}
#[test]
@@ -785,7 +825,7 @@ macro_rules! bdk_blockchain_tests {
});
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000);
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let mut builder = wallet.build_tx();
builder.add_recipient(node_addr.script_pubkey().clone(), 49_000).enable_rbf();
@@ -794,8 +834,8 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 26_000 - details.fees);
assert_eq!(details.received, 1_000 - details.fees);
assert_eq!(wallet.get_balance().unwrap(), 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();
builder.fee_rate(FeeRate::from_sat_per_vb(123.0));
@@ -806,24 +846,33 @@ macro_rules! bdk_blockchain_tests {
assert!(finalized, "Cannot finalize transaction");
wallet.broadcast(new_psbt.extract_tx()).unwrap();
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(new_details.sent, 75_000);
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(new_details.received, 0);
assert_eq!(new_details.sent, 75_000, "incorrect sent");
assert_eq!(wallet.get_balance().unwrap(), 0, "incorrect balance after add input");
assert_eq!(new_details.received, 0, "incorrect received after add input");
}
#[test]
#[serial]
fn test_sync_receive_coinbase() {
let (wallet, _, mut test_client) = init_single_sig();
let wallet_addr = wallet.get_address(New).unwrap().address;
let wallet_addr = wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
wallet.sync(noop_progress(), None).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 0);
assert_eq!(wallet.get_balance().unwrap(), 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(noop_progress(), None).unwrap();
assert!(wallet.get_balance().unwrap() > 0);
assert!(wallet.get_balance().unwrap() > 0, "incorrect balance after receiving coinbase");
}
}
}

View File

@@ -80,7 +80,7 @@ impl std::default::Default for FeeRate {
/// An unspent output owned by a [`Wallet`].
///
/// [`Wallet`]: crate::Wallet
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LocalUtxo {
/// Reference to a transaction output
pub outpoint: OutPoint,
@@ -139,7 +139,7 @@ impl Utxo {
}
if let Some(txout) = &psbt_input.witness_utxo {
return &txout;
return txout;
}
unreachable!("Foreign UTXOs will always have one of these set")
@@ -155,16 +155,35 @@ pub struct TransactionDetails {
pub transaction: Option<Transaction>,
/// Transaction id
pub txid: Txid,
/// Timestamp
pub timestamp: u64,
/// Received value (sats)
pub received: u64,
/// Sent value (sats)
pub sent: u64,
/// Fee value (sats)
pub fees: u64,
/// Confirmed in block height, `None` means unconfirmed
pub height: Option<u32>,
/// Fee value (sats) if available
pub fee: Option<u64>,
/// If the transaction is confirmed, contains height and timestamp of the block containing the
/// transaction, unconfirmed transaction contains `None`.
pub confirmation_time: Option<ConfirmationTime>,
}
/// Block height and timestamp of the block containing the confirmed transaction
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
pub struct ConfirmationTime {
/// confirmation block height
pub height: u32,
/// confirmation block timestamp
pub timestamp: u64,
}
impl ConfirmationTime {
/// Returns `Some` `ConfirmationTime` if both `height` and `timestamp` are `Some`
pub fn new(height: Option<u32>, timestamp: Option<u64>) -> Option<Self> {
match (height, timestamp) {
(Some(height), Some(timestamp)) => Some(ConfirmationTime { height, timestamp }),
_ => None,
}
}
}
#[cfg(test)]

View File

@@ -128,7 +128,7 @@ impl WalletExport {
Ok(txs) => {
let mut heights = txs
.into_iter()
.map(|tx| tx.height.unwrap_or(0))
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
.collect::<Vec<_>>();
heights.sort_unstable();
@@ -212,6 +212,7 @@ mod test {
use crate::database::{memory::MemoryDatabase, BatchOperations};
use crate::types::TransactionDetails;
use crate::wallet::Wallet;
use crate::ConfirmationTime;
fn get_test_db() -> MemoryDatabase {
let mut db = MemoryDatabase::new();
@@ -221,11 +222,14 @@ mod test {
"4ddff1fa33af17f377f62b72357b43107c19110a8009b36fb832af505efed98a",
)
.unwrap(),
timestamp: 12345678,
received: 100_000,
sent: 0,
fees: 500,
height: Some(5000),
fee: Some(500),
confirmation_time: Some(ConfirmationTime {
timestamp: 12345678,
height: 5000,
}),
})
.unwrap();

View File

@@ -277,7 +277,7 @@ where
self.descriptor
.as_derived(index, &self.secp)
.address(self.network)
.map(|address| AddressInfo { address, index })
.map(|address| AddressInfo { index, address })
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
}
@@ -289,7 +289,7 @@ where
self.descriptor
.as_derived(index, &self.secp)
.address(self.network)
.map(|address| AddressInfo { address, index })
.map(|address| AddressInfo { index, address })
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
}
@@ -398,7 +398,7 @@ where
/// [`TxBuilder`]: crate::TxBuilder
pub fn build_tx(&self) -> TxBuilder<'_, B, D, DefaultCoinSelectionAlgorithm, CreateTx> {
TxBuilder {
wallet: &self,
wallet: self,
params: TxParams::default(),
coin_selection: DefaultCoinSelectionAlgorithm::default(),
phantom: core::marker::PhantomData,
@@ -706,11 +706,10 @@ where
let transaction_details = TransactionDetails {
transaction: None,
txid,
timestamp: time::get_timestamp(),
confirmation_time: None,
received,
sent,
fees: fee_amount,
height: None,
fee: Some(fee_amount),
};
Ok((psbt, transaction_details))
@@ -769,7 +768,7 @@ where
let mut details = match self.database.borrow().get_tx(&txid, true)? {
None => return Err(Error::TransactionNotFound),
Some(tx) if tx.transaction.is_none() => return Err(Error::TransactionNotFound),
Some(tx) if tx.height.is_some() => return Err(Error::TransactionConfirmed),
Some(tx) if tx.confirmation_time.is_some() => return Err(Error::TransactionConfirmed),
Some(tx) => tx,
};
let mut tx = details.transaction.take().unwrap();
@@ -778,7 +777,7 @@ where
}
let vbytes = tx.get_weight() as f32 / 4.0;
let feerate = details.fees as f32 / vbytes;
let feerate = details.fee.ok_or(Error::FeeRateUnavailable)? as f32 / vbytes;
// remove the inputs from the tx and process them
let original_txin = tx.input.drain(..).collect::<Vec<_>>();
@@ -854,14 +853,14 @@ where
.collect(),
utxos: original_utxos,
bumping_fee: Some(tx_builder::PreviousFee {
absolute: details.fees,
absolute: details.fee.ok_or(Error::FeeRateUnavailable)?,
rate: feerate,
}),
..Default::default()
};
Ok(TxBuilder {
wallet: &self,
wallet: self,
params,
coin_selection: DefaultCoinSelectionAlgorithm::default(),
phantom: core::marker::PhantomData,
@@ -993,7 +992,7 @@ where
.database
.borrow()
.get_tx(&input.previous_output.txid, false)?
.map(|tx| tx.height.unwrap_or(std::u32::MAX));
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(u32::MAX));
let current_height = sign_options.assume_height.or(self.current_height);
debug!(
@@ -1031,7 +1030,7 @@ where
match desc.satisfy(
&mut tmp_input,
(
PsbtInputSatisfier::new(&psbt, n),
PsbtInputSatisfier::new(psbt, n),
After::new(current_height, false),
Older::new(current_height, create_height, false),
),
@@ -1231,7 +1230,7 @@ where
let satisfies_confirmed = match must_only_use_confirmed_tx {
true => {
let database = self.database.borrow_mut();
let database = self.database.borrow();
may_spend
.iter()
.map(|u| {
@@ -1239,7 +1238,7 @@ where
.get_tx(&u.0.outpoint.txid, true)
.map(|tx| match tx {
None => false,
Some(tx) => tx.height.is_some(),
Some(tx) => tx.confirmation_time.is_some(),
})
})
.collect::<Result<Vec<_>, _>>()?
@@ -1489,12 +1488,14 @@ where
false => 0,
true => max_address_param.unwrap_or(CACHE_ADDR_BATCH_SIZE),
};
debug!("max_address {}", max_address);
if self
.database
.borrow()
.get_script_pubkey_from_path(KeychainKind::External, max_address.saturating_sub(1))?
.is_none()
{
debug!("caching external addresses");
run_setup = true;
self.cache_addresses(KeychainKind::External, 0, max_address)?;
}
@@ -1511,11 +1512,13 @@ where
.get_script_pubkey_from_path(KeychainKind::Internal, max_address.saturating_sub(1))?
.is_none()
{
debug!("caching internal addresses");
run_setup = true;
self.cache_addresses(KeychainKind::Internal, 0, max_address)?;
}
}
debug!("run_setup: {}", run_setup);
// 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 {
@@ -1982,7 +1985,7 @@ pub(crate) mod test {
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
assert_eq!(
psbt.global.unsigned_tx.output[0].value,
50_000 - details.fees
50_000 - details.fee.unwrap_or(0)
);
}
@@ -1994,7 +1997,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.fees, FeeRate::default(), @add_signature);
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::default(), @add_signature);
}
#[test]
@@ -2007,7 +2010,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.fees, FeeRate::from_sat_per_vb(5.0), @add_signature);
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
}
#[test]
@@ -2021,11 +2024,11 @@ pub(crate) mod test {
.fee_absolute(100);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.fees, 100);
assert_eq!(details.fee.unwrap_or(0), 100);
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
assert_eq!(
psbt.global.unsigned_tx.output[0].value,
50_000 - details.fees
50_000 - details.fee.unwrap_or(0)
);
}
@@ -2040,11 +2043,11 @@ pub(crate) mod test {
.fee_absolute(0);
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.fees, 0);
assert_eq!(details.fee.unwrap_or(0), 0);
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
assert_eq!(
psbt.global.unsigned_tx.output[0].value,
50_000 - details.fees
50_000 - details.fee.unwrap_or(0)
);
}
@@ -2077,7 +2080,7 @@ pub(crate) mod test {
assert_eq!(psbt.global.unsigned_tx.output[0].value, 25_000);
assert_eq!(
psbt.global.unsigned_tx.output[1].value,
25_000 - details.fees
25_000 - details.fee.unwrap_or(0)
);
}
@@ -2091,7 +2094,7 @@ pub(crate) mod test {
assert_eq!(psbt.global.unsigned_tx.output.len(), 1);
assert_eq!(psbt.global.unsigned_tx.output[0].value, 49_800);
assert_eq!(details.fees, 200);
assert_eq!(details.fee.unwrap_or(0), 200);
}
#[test]
@@ -2122,7 +2125,7 @@ pub(crate) mod test {
assert_eq!(psbt.global.unsigned_tx.output.len(), 3);
assert_eq!(
psbt.global.unsigned_tx.output[0].value,
10_000 - details.fees
10_000 - details.fee.unwrap_or(0)
);
assert_eq!(psbt.global.unsigned_tx.output[1].value, 10_000);
assert_eq!(psbt.global.unsigned_tx.output[2].value, 30_000);
@@ -2492,7 +2495,7 @@ pub(crate) mod test {
assert_eq!(
details.sent - details.received,
10_000 + details.fees,
10_000 + details.fee.unwrap_or(0),
"we should have only net spent ~10_000"
);
@@ -2758,7 +2761,10 @@ pub(crate) mod test {
let txid = tx.txid();
// skip saving the utxos, we know they can't be used anyways
details.transaction = Some(tx);
details.height = Some(42);
details.confirmation_time = Some(ConfirmationTime {
timestamp: 12345678,
height: 42,
});
wallet.database.borrow_mut().set_tx(&details).unwrap();
wallet.build_fee_bump(txid).unwrap().finish().unwrap();
@@ -2863,10 +2869,10 @@ pub(crate) mod test {
assert_eq!(details.sent, original_details.sent);
assert_eq!(
details.received + details.fees,
original_details.received + original_details.fees
details.received + details.fee.unwrap_or(0),
original_details.received + original_details.fee.unwrap_or(0)
);
assert!(details.fees > original_details.fees);
assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0));
let tx = &psbt.global.unsigned_tx;
assert_eq!(tx.output.len(), 2);
@@ -2887,7 +2893,7 @@ pub(crate) mod test {
details.received
);
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(2.5), @add_signature);
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
}
#[test]
@@ -2924,14 +2930,14 @@ pub(crate) mod test {
assert_eq!(details.sent, original_details.sent);
assert_eq!(
details.received + details.fees,
original_details.received + original_details.fees
details.received + details.fee.unwrap_or(0),
original_details.received + original_details.fee.unwrap_or(0)
);
assert!(
details.fees > original_details.fees,
details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0),
"{} > {}",
details.fees,
original_details.fees
details.fee.unwrap_or(0),
original_details.fee.unwrap_or(0)
);
let tx = &psbt.global.unsigned_tx;
@@ -2953,7 +2959,7 @@ pub(crate) mod test {
details.received
);
assert_eq!(details.fees, 200);
assert_eq!(details.fee.unwrap_or(0), 200);
}
#[test]
@@ -2991,13 +2997,13 @@ pub(crate) mod test {
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent);
assert!(details.fees > original_details.fees);
assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0));
let tx = &psbt.global.unsigned_tx;
assert_eq!(tx.output.len(), 1);
assert_eq!(tx.output[0].value + details.fees, details.sent);
assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent);
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(2.5), @add_signature);
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(2.5), @add_signature);
}
#[test]
@@ -3035,13 +3041,13 @@ pub(crate) mod test {
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent);
assert!(details.fees > original_details.fees);
assert!(details.fee.unwrap_or(0) > original_details.fee.unwrap_or(0));
let tx = &psbt.global.unsigned_tx;
assert_eq!(tx.output.len(), 1);
assert_eq!(tx.output[0].value + details.fees, details.sent);
assert_eq!(tx.output[0].value + details.fee.unwrap_or(0), details.sent);
assert_eq!(details.fees, 300);
assert_eq!(details.fee.unwrap_or(0), 300);
}
#[test]
@@ -3186,7 +3192,7 @@ pub(crate) mod test {
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fees + details.received, 30_000);
assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000);
let tx = &psbt.global.unsigned_tx;
assert_eq!(tx.input.len(), 2);
@@ -3208,7 +3214,7 @@ pub(crate) mod test {
details.received
);
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(50.0), @add_signature);
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
}
#[test]
@@ -3249,7 +3255,7 @@ pub(crate) mod test {
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fees + details.received, 30_000);
assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000);
let tx = &psbt.global.unsigned_tx;
assert_eq!(tx.input.len(), 2);
@@ -3271,7 +3277,7 @@ pub(crate) mod test {
details.received
);
assert_eq!(details.fees, 6_000);
assert_eq!(details.fee.unwrap_or(0), 6_000);
}
#[test]
@@ -3321,11 +3327,11 @@ pub(crate) mod test {
builder.fee_rate(FeeRate::from_sat_per_vb(50.0));
let (psbt, details) = builder.finish().unwrap();
let original_send_all_amount = original_details.sent - original_details.fees;
let original_send_all_amount = original_details.sent - original_details.fee.unwrap_or(0);
assert_eq!(details.sent, original_details.sent + 50_000);
assert_eq!(
details.received,
75_000 - original_send_all_amount - details.fees
75_000 - original_send_all_amount - details.fee.unwrap_or(0)
);
let tx = &psbt.global.unsigned_tx;
@@ -3345,10 +3351,10 @@ pub(crate) mod test {
.find(|txout| txout.script_pubkey != addr.script_pubkey())
.unwrap()
.value,
75_000 - original_send_all_amount - details.fees
75_000 - original_send_all_amount - details.fee.unwrap_or(0)
);
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(50.0), @add_signature);
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(50.0), @add_signature);
}
#[test]
@@ -3390,10 +3396,13 @@ pub(crate) mod test {
builder.fee_rate(FeeRate::from_sat_per_vb(140.0));
let (psbt, details) = builder.finish().unwrap();
assert_eq!(original_details.received, 5_000 - original_details.fees);
assert_eq!(
original_details.received,
5_000 - original_details.fee.unwrap_or(0)
);
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fees, 30_000);
assert_eq!(details.fee.unwrap_or(0), 30_000);
assert_eq!(details.received, 0);
let tx = &psbt.global.unsigned_tx;
@@ -3408,7 +3417,7 @@ pub(crate) mod test {
45_000
);
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature);
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(140.0), @dust_change, @add_signature);
}
#[test]
@@ -3457,7 +3466,7 @@ pub(crate) mod test {
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fees + details.received, 30_000);
assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000);
let tx = &psbt.global.unsigned_tx;
assert_eq!(tx.input.len(), 2);
@@ -3479,7 +3488,7 @@ pub(crate) mod test {
details.received
);
assert_fee_rate!(psbt.extract_tx(), details.fees, FeeRate::from_sat_per_vb(5.0), @add_signature);
assert_fee_rate!(psbt.extract_tx(), details.fee.unwrap_or(0), FeeRate::from_sat_per_vb(5.0), @add_signature);
}
#[test]
@@ -3528,7 +3537,7 @@ pub(crate) mod test {
let (psbt, details) = builder.finish().unwrap();
assert_eq!(details.sent, original_details.sent + 25_000);
assert_eq!(details.fees + details.received, 30_000);
assert_eq!(details.fee.unwrap_or(0) + details.received, 30_000);
let tx = &psbt.global.unsigned_tx;
assert_eq!(tx.input.len(), 2);
@@ -3550,7 +3559,7 @@ pub(crate) mod test {
details.received
);
assert_eq!(details.fees, 250);
assert_eq!(details.fee.unwrap_or(0), 250);
}
#[test]

View File

@@ -222,7 +222,7 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
.bip32_derivation
.iter()
.filter_map(|(pk, &(fingerprint, ref path))| {
if self.matches(&(fingerprint, path.clone()), &secp).is_some() {
if self.matches(&(fingerprint, path.clone()), secp).is_some() {
Some((pk, path))
} else {
None
@@ -240,12 +240,12 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
&full_path.into_iter().cloned().collect::<Vec<ChildNumber>>()
[origin_path.len()..],
);
self.xkey.derive_priv(&secp, &deriv_path).unwrap()
self.xkey.derive_priv(secp, &deriv_path).unwrap()
}
None => self.xkey.derive_priv(&secp, &full_path).unwrap(),
None => self.xkey.derive_priv(secp, &full_path).unwrap(),
};
if &derived_key.private_key.public_key(&secp) != public_key {
if &derived_key.private_key.public_key(secp) != public_key {
Err(SignerError::InvalidKey)
} else {
derived_key.private_key.sign(psbt, Some(input_index), secp)
@@ -257,7 +257,7 @@ impl Signer for DescriptorXKey<ExtendedPrivKey> {
}
fn id(&self, secp: &SecpCtx) -> SignerId {
SignerId::from(self.root_fingerprint(&secp))
SignerId::from(self.root_fingerprint(secp))
}
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
@@ -283,7 +283,7 @@ impl Signer for PrivateKey {
return Ok(());
}
let pubkey = self.public_key(&secp);
let pubkey = self.public_key(secp);
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
return Ok(());
}
@@ -591,7 +591,7 @@ impl ComputeSighash for Segwitv0 {
.map(Script::is_v0_p2wpkh)
.unwrap_or(false)
{
p2wpkh_script_code(&psbt_input.redeem_script.as_ref().unwrap())
p2wpkh_script_code(psbt_input.redeem_script.as_ref().unwrap())
} else {
return Err(SignerError::MissingWitnessScript);
}