Compare commits

..

2 Commits

Author SHA1 Message Date
LLFourn
8de422dcfd Add SyncOptions as the second argument to Wallet::sync
The current options are awkware and it would be good if we could
introduce more in the future without breaking changes.
2022-01-27 16:52:53 +11:00
LLFourn
733300623e Remove Blockchain from wallet
Although somewhat convenient to have, coupling the Wallet with
the blockchain trait causes development friction and complexity.
What if sometimes the wallet is "offline" (no access to the blockchain)
but sometimes its online?
The only thing the Wallet needs the blockchain for is to sync.
But not all applications will even use the sync method and the sync
method doesn't require the full blockchain functionality.
So we instead pass the blockchain in when we want to sync.

- To further reduce the coupling with blockchain I removed the get_height call from `new` and just use the height of the
last sync in the database.
- I split up the blockchain trait a bit into subtraits.
2022-01-26 20:11:22 +11:00
24 changed files with 278 additions and 443 deletions

View File

@@ -89,13 +89,13 @@ jobs:
matrix: matrix:
blockchain: blockchain:
- name: electrum - name: electrum
features: test-electrum,verify features: test-electrum
- name: rpc - name: rpc
features: test-rpc features: test-rpc
- name: esplora - name: esplora
features: test-esplora,use-esplora-reqwest,verify features: test-esplora,use-esplora-reqwest
- name: esplora - name: esplora
features: test-esplora,use-esplora-ureq,verify features: test-esplora,use-esplora-ureq
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v2 uses: actions/checkout@v2

View File

@@ -18,7 +18,7 @@ jobs:
target target
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- name: Set default toolchain - name: Set default toolchain
run: rustup default nightly-2022-01-25 run: rustup default nightly
- name: Set profile - name: Set profile
run: rustup set profile minimal run: rustup set profile minimal
- name: Update toolchain - name: Update toolchain

View File

@@ -6,41 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [v0.17.0] - [v0.16.1]
- Removed default verification from `wallet::sync`. sync-time verification is added in `script_sync` and is activated by `verify` feature flag.
- `verify` flag removed from `TransactionDetails`.
- Add `get_internal_address` to allow you to get internal addresses just as you get external addresses.
- added `ensure_addresses_cached` to `Wallet` to let offline wallets load and cache addresses in their database
### Sync API change ### Sync API change
To decouple the `Wallet` from the `Blockchain` we've made major changes: To decouple the `Wallet` from the `Blockchain` we've made major changes:
- Removed `Blockchain` from Wallet. - Removed `Blockchain` from Wallet.
- Removed `Wallet::broadcast` (just use `Blockchain::broadcast`) - Removed `Wallet::broadcast` (just use `Blockchain::broadcast`)
- Deprecated `Wallet::new_offline` (all wallets are offline now) - Depreciated `Wallet::new_offline` (all wallets are offline now)
- Changed `Wallet::sync` to take a `Blockchain`. - Changed `Wallet::sync` to take a `Blockchain`.
- Stop making a request for the block height when calling `Wallet:new`. - Stop making a request for the block height when calling `Wallet:new`.
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`. - Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
- Removed `max_addresses` sync parameter which determined how many addresses to cache before syncing since this can just be done with `ensure_addresses_cached`.
## [v0.16.1] - [v0.16.0]
- Pin tokio dependency version to ~1.14 to prevent errors due to their new MSRV 1.49.0
## [v0.16.0] - [v0.15.0]
- Disable `reqwest` default features.
- Added `reqwest-default-tls` feature: Use this to restore the TLS defaults of reqwest if you don't want to add a dependency to it in your own manifest.
- Use dust_value from rust-bitcoin
- Fixed generating WIF in the correct network format.
## [v0.15.0] - [v0.14.0] ## [v0.15.0] - [v0.14.0]
- Overhauled sync logic for electrum and esplora. - Overhauled sync logic for electrum and esplora.
- Unify ureq and reqwest esplora backends to have the same configuration parameters. This means reqwest now has a timeout parameter and ureq has a concurrency parameter. - Unify ureq and reqwest esplora backends to have the same configuration parameters. This means reqwest now has a timeout parameter and ureq has a concurrency parameter.
- Fixed esplora fee estimation. - Fixed esplora fee estimation.
- Fixed generating WIF in the correct network format.
- Disable `reqwest` default features.
- Added `reqwest-default-tls` feature: Use this to restore the TLS defaults of reqwest if you don't want to add a dependency to it in your own manifest.
## [v0.14.0] - [v0.13.0] ## [v0.14.0] - [v0.13.0]
@@ -417,6 +401,7 @@ final transaction is created by calling `finish` on the builder.
- Use `MemoryDatabase` in the compiler example - Use `MemoryDatabase` in the compiler example
- Make the REPL return JSON - Make the REPL return JSON
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.11.0...HEAD
[0.1.0-beta.1]: https://github.com/bitcoindevkit/bdk/compare/96c87ea5...0.1.0-beta.1 [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.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 [v0.3.0]: https://github.com/bitcoindevkit/bdk/compare/v0.2.0...v0.3.0
@@ -433,7 +418,3 @@ final transaction is created by calling `finish` on the builder.
[v0.13.0]: https://github.com/bitcoindevkit/bdk/compare/v0.12.0...v0.13.0 [v0.13.0]: https://github.com/bitcoindevkit/bdk/compare/v0.12.0...v0.13.0
[v0.14.0]: https://github.com/bitcoindevkit/bdk/compare/v0.13.0...v0.14.0 [v0.14.0]: https://github.com/bitcoindevkit/bdk/compare/v0.13.0...v0.14.0
[v0.15.0]: https://github.com/bitcoindevkit/bdk/compare/v0.14.0...v0.15.0 [v0.15.0]: https://github.com/bitcoindevkit/bdk/compare/v0.14.0...v0.15.0
[v0.16.0]: https://github.com/bitcoindevkit/bdk/compare/v0.15.0...v0.16.0
[v0.16.1]: https://github.com/bitcoindevkit/bdk/compare/v0.16.0...v0.16.1
[v0.17.0]: https://github.com/bitcoindevkit/bdk/compare/v0.16.1...v0.17.0
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.17.0...HEAD

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "bdk" name = "bdk"
version = "0.17.0" version = "0.15.1-dev"
edition = "2018" edition = "2018"
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"] authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
homepage = "https://bitcoindevkit.org" homepage = "https://bitcoindevkit.org"
@@ -42,7 +42,7 @@ bitcoincore-rpc = { version = "0.14", optional = true }
# Platform-specific dependencies # Platform-specific dependencies
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tokio = { version = "~1.14", features = ["rt"] } tokio = { version = "1", features = ["rt"] }
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
async-trait = "0.1" async-trait = "0.1"

View File

@@ -16,7 +16,7 @@
//! //!
//! ## Example //! ## Example
//! //!
//! When paired with the use of [`ConfigurableBlockchain`], it allows creating any //! When paired with the use of [`ConfigurableBlockchain`], it allows creating wallets with any
//! blockchain type supported using a single line of code: //! blockchain type supported using a single line of code:
//! //!
//! ```no_run //! ```no_run
@@ -89,6 +89,9 @@ impl Blockchain for AnyBlockchain {
maybe_await!(impl_inner_method!(self, get_capabilities)) maybe_await!(impl_inner_method!(self, get_capabilities))
} }
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(impl_inner_method!(self, get_tx, txid))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
maybe_await!(impl_inner_method!(self, broadcast, tx)) maybe_await!(impl_inner_method!(self, broadcast, tx))
} }
@@ -98,21 +101,12 @@ impl Blockchain for AnyBlockchain {
} }
} }
#[maybe_async]
impl GetHeight for AnyBlockchain { impl GetHeight for AnyBlockchain {
fn get_height(&self) -> Result<u32, Error> { fn get_height(&self) -> Result<u32, Error> {
maybe_await!(impl_inner_method!(self, get_height)) maybe_await!(impl_inner_method!(self, get_height))
} }
} }
#[maybe_async]
impl GetTx for AnyBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(impl_inner_method!(self, get_tx, txid))
}
}
#[maybe_async]
impl WalletSync for AnyBlockchain { impl WalletSync for AnyBlockchain {
fn wallet_sync<D: BatchDatabase>( fn wallet_sync<D: BatchDatabase>(
&self, &self,

View File

@@ -67,7 +67,7 @@ mod peer;
mod store; mod store;
mod sync; mod sync;
use crate::blockchain::*; use super::{Blockchain, Capability, ConfigurableBlockchain, GetHeight, Progress, WalletSync};
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils}; use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
use crate::error::Error; use crate::error::Error;
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails}; use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
@@ -207,6 +207,7 @@ impl CompactFiltersBlockchain {
received: incoming, received: incoming,
sent: outgoing, sent: outgoing,
confirmation_time: BlockTime::new(height, timestamp), confirmation_time: BlockTime::new(height, timestamp),
verified: height.is_some(),
fee: Some(inputs_sum.saturating_sub(outputs_sum)), fee: Some(inputs_sum.saturating_sub(outputs_sum)),
}; };
@@ -225,6 +226,12 @@ impl Blockchain for CompactFiltersBlockchain {
vec![Capability::FullHistory].into_iter().collect() vec![Capability::FullHistory].into_iter().collect()
} }
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.peers[0]
.get_mempool()
.get_tx(&Inventory::Transaction(*txid)))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
self.peers[0].broadcast_tx(tx.clone())?; self.peers[0].broadcast_tx(tx.clone())?;
@@ -243,14 +250,6 @@ impl GetHeight for CompactFiltersBlockchain {
} }
} }
impl GetTx for CompactFiltersBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.peers[0]
.get_mempool()
.get_tx(&Inventory::Transaction(*txid)))
}
}
impl WalletSync for CompactFiltersBlockchain { impl WalletSync for CompactFiltersBlockchain {
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop. #[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
fn wallet_setup<D: BatchDatabase>( fn wallet_setup<D: BatchDatabase>(

View File

@@ -68,6 +68,10 @@ impl Blockchain for ElectrumBlockchain {
.collect() .collect()
} }
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.client.transaction_get(txid).map(Option::Some)?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.transaction_broadcast(tx).map(|_| ())?) Ok(self.client.transaction_broadcast(tx).map(|_| ())?)
} }
@@ -90,12 +94,6 @@ impl GetHeight for ElectrumBlockchain {
} }
} }
impl GetTx for ElectrumBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.client.transaction_get(txid).map(Option::Some)?)
}
}
impl WalletSync for ElectrumBlockchain { impl WalletSync for ElectrumBlockchain {
fn wallet_setup<D: BatchDatabase>( fn wallet_setup<D: BatchDatabase>(
&self, &self,
@@ -204,7 +202,6 @@ impl WalletSync for ElectrumBlockchain {
let full_details = full_transactions let full_details = full_transactions
.into_iter() .into_iter()
.map(|tx| { .map(|tx| {
let mut input_index = 0usize;
let prev_outputs = tx let prev_outputs = tx
.input .input
.iter() .iter()
@@ -219,7 +216,6 @@ impl WalletSync for ElectrumBlockchain {
.output .output
.get(input.previous_output.vout as usize) .get(input.previous_output.vout as usize)
.ok_or_else(electrum_goof)?; .ok_or_else(electrum_goof)?;
input_index += 1;
Ok(Some(txout.clone())) Ok(Some(txout.clone()))
}) })
.collect::<Result<Vec<_>, Error>>()?; .collect::<Result<Vec<_>, Error>>()?;

View File

@@ -17,7 +17,7 @@ pub struct Vin {
// None if coinbase // None if coinbase
pub prevout: Option<PrevOut>, pub prevout: Option<PrevOut>,
pub scriptsig: Script, pub scriptsig: Script,
#[serde(deserialize_with = "deserialize_witness", default)] #[serde(deserialize_with = "deserialize_witness")]
pub witness: Vec<Vec<u8>>, pub witness: Vec<Vec<u8>>,
pub sequence: u32, pub sequence: u32,
pub is_coinbase: bool, pub is_coinbase: bool,

View File

@@ -91,6 +91,10 @@ impl Blockchain for EsploraBlockchain {
.collect() .collect()
} }
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(await_or_block!(self.url_client._get_tx(txid))?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(await_or_block!(self.url_client._broadcast(tx))?) Ok(await_or_block!(self.url_client._broadcast(tx))?)
} }
@@ -108,19 +112,12 @@ impl GetHeight for EsploraBlockchain {
} }
} }
#[maybe_async]
impl GetTx for EsploraBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(await_or_block!(self.url_client._get_tx(txid))?)
}
}
#[maybe_async] #[maybe_async]
impl WalletSync for EsploraBlockchain { impl WalletSync for EsploraBlockchain {
fn wallet_setup<D: BatchDatabase>( fn wallet_setup<D: BatchDatabase, P: Progress>(
&self, &self,
database: &mut D, database: &mut D,
_progress_update: Box<dyn Progress>, _progress_update: P,
) -> Result<(), Error> { ) -> Result<(), Error> {
use crate::blockchain::script_sync::Request; use crate::blockchain::script_sync::Request;
let mut request = script_sync::start(database, self.stop_gap)?; let mut request = script_sync::start(database, self.stop_gap)?;
@@ -193,9 +190,9 @@ impl WalletSync for EsploraBlockchain {
.request() .request()
.map(|txid| { .map(|txid| {
let tx = tx_index.get(txid).expect("must be in index"); let tx = tx_index.get(txid).expect("must be in index");
Ok((tx.previous_outputs(), tx.to_tx())) (tx.previous_outputs(), tx.to_tx())
}) })
.collect::<Result<_, Error>>()?; .collect();
tx_req.satisfy(full_txs)? tx_req.satisfy(full_txs)?
} }
Request::Finish(batch_update) => break batch_update, Request::Finish(batch_update) => break batch_update,

View File

@@ -87,6 +87,10 @@ impl Blockchain for EsploraBlockchain {
.collect() .collect()
} }
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.url_client._get_tx(txid)?)
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
let _txid = self.url_client._broadcast(tx)?; let _txid = self.url_client._broadcast(tx)?;
Ok(()) Ok(())
@@ -104,12 +108,6 @@ impl GetHeight for EsploraBlockchain {
} }
} }
impl GetTx for EsploraBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(self.url_client._get_tx(txid)?)
}
}
impl WalletSync for EsploraBlockchain { impl WalletSync for EsploraBlockchain {
fn wallet_setup<D: BatchDatabase>( fn wallet_setup<D: BatchDatabase>(
&self, &self,
@@ -190,9 +188,9 @@ impl WalletSync for EsploraBlockchain {
.request() .request()
.map(|txid| { .map(|txid| {
let tx = tx_index.get(txid).expect("must be in index"); let tx = tx_index.get(txid).expect("must be in index");
Ok((tx.previous_outputs(), tx.to_tx())) (tx.previous_outputs(), tx.to_tx())
}) })
.collect::<Result<_, Error>>()?; .collect();
tx_req.satisfy(full_txs)? tx_req.satisfy(full_txs)?
} }
Request::Finish(batch_update) => break batch_update, Request::Finish(batch_update) => break batch_update,

View File

@@ -86,9 +86,11 @@ pub enum Capability {
/// Trait that defines the actions that must be supported by a blockchain backend /// Trait that defines the actions that must be supported by a blockchain backend
#[maybe_async] #[maybe_async]
pub trait Blockchain: WalletSync + GetHeight + GetTx { pub trait Blockchain: WalletSync + GetHeight {
/// Return the set of [`Capability`] supported by this backend /// Return the set of [`Capability`] supported by this backend
fn get_capabilities(&self) -> HashSet<Capability>; fn get_capabilities(&self) -> HashSet<Capability>;
/// Fetch a transaction from the blockchain given its txid
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
/// Broadcast a transaction /// Broadcast a transaction
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>; fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
/// Estimate the fee rate required to confirm a transaction in a given `target` of blocks /// Estimate the fee rate required to confirm a transaction in a given `target` of blocks
@@ -102,13 +104,6 @@ pub trait GetHeight {
fn get_height(&self) -> Result<u32, Error>; fn get_height(&self) -> Result<u32, Error>;
} }
#[maybe_async]
/// Trait for getting a transaction by txid
pub trait GetTx {
/// Fetch a transaction given its txid
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error>;
}
/// Trait for blockchains that can sync by updating the database directly. /// Trait for blockchains that can sync by updating the database directly.
#[maybe_async] #[maybe_async]
pub trait WalletSync { pub trait WalletSync {
@@ -235,6 +230,9 @@ impl<T: Blockchain> Blockchain for Arc<T> {
maybe_await!(self.deref().get_capabilities()) maybe_await!(self.deref().get_capabilities())
} }
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(self.deref().get_tx(txid))
}
fn broadcast(&self, tx: &Transaction) -> Result<(), Error> { fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
maybe_await!(self.deref().broadcast(tx)) maybe_await!(self.deref().broadcast(tx))
} }
@@ -244,13 +242,6 @@ impl<T: Blockchain> Blockchain for Arc<T> {
} }
} }
#[maybe_async]
impl<T: GetTx> GetTx for Arc<T> {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
maybe_await!(self.deref().get_tx(txid))
}
}
#[maybe_async] #[maybe_async]
impl<T: GetHeight> GetHeight for Arc<T> { impl<T: GetHeight> GetHeight for Arc<T> {
fn get_height(&self) -> Result<u32, Error> { fn get_height(&self) -> Result<u32, Error> {

View File

@@ -33,7 +33,9 @@
use crate::bitcoin::consensus::deserialize; use crate::bitcoin::consensus::deserialize;
use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid}; use crate::bitcoin::{Address, Network, OutPoint, Transaction, TxOut, Txid};
use crate::blockchain::*; use crate::blockchain::{
Blockchain, Capability, ConfigurableBlockchain, GetHeight, Progress, WalletSync,
};
use crate::database::{BatchDatabase, DatabaseUtils}; use crate::database::{BatchDatabase, DatabaseUtils};
use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails}; use crate::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
use bitcoincore_rpc::json::{ use bitcoincore_rpc::json::{
@@ -139,6 +141,10 @@ impl Blockchain for RpcBlockchain {
self.capabilities.clone() self.capabilities.clone()
} }
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> { fn broadcast(&self, tx: &Transaction) -> Result<(), Error> {
Ok(self.client.send_raw_transaction(tx).map(|_| ())?) Ok(self.client.send_raw_transaction(tx).map(|_| ())?)
} }
@@ -155,12 +161,6 @@ impl Blockchain for RpcBlockchain {
} }
} }
impl GetTx for RpcBlockchain {
fn get_tx(&self, txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(Some(self.client.get_raw_transaction(txid, None)?))
}
}
impl GetHeight for RpcBlockchain { impl GetHeight for RpcBlockchain {
fn get_height(&self) -> Result<u32, Error> { fn get_height(&self) -> Result<u32, Error> {
Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?) Ok(self.client.get_blockchain_info().map(|i| i.blocks as u32)?)
@@ -286,9 +286,7 @@ impl WalletSync for RpcBlockchain {
for input in tx.input.iter() { for input in tx.input.iter() {
if let Some(previous_output) = db.get_previous_output(&input.previous_output)? { if let Some(previous_output) = db.get_previous_output(&input.previous_output)? {
if db.is_mine(&previous_output.script_pubkey)? { sent += previous_output.value;
sent += previous_output.value;
}
} }
} }
@@ -302,6 +300,7 @@ impl WalletSync for RpcBlockchain {
received, received,
sent, sent,
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64), fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
verified: true,
}; };
debug!( debug!(
"saving tx: {} tx_result.fee:{:?} td.fees:{:?}", "saving tx: {} tx_result.fee:{:?} td.fees:{:?}",
@@ -318,24 +317,22 @@ impl WalletSync for RpcBlockchain {
} }
} }
// Filter out trasactions that are for script pubkeys that aren't in this wallet. let current_utxos: HashSet<_> = current_utxo
let current_utxos = current_utxo
.into_iter() .into_iter()
.filter_map( .map(|u| {
|u| match db.get_path_from_script_pubkey(&u.script_pub_key) { Ok(LocalUtxo {
Err(e) => Some(Err(e)), outpoint: OutPoint::new(u.txid, u.vout),
Ok(None) => None, keychain: db
Ok(Some(path)) => Some(Ok(LocalUtxo { .get_path_from_script_pubkey(&u.script_pub_key)?
outpoint: OutPoint::new(u.txid, u.vout), .ok_or(Error::TransactionNotFound)?
keychain: path.0, .0,
txout: TxOut { txout: TxOut {
value: u.amount.as_sat(), value: u.amount.as_sat(),
script_pubkey: u.script_pub_key, script_pubkey: u.script_pub_key,
}, },
})), })
}, })
) .collect::<Result<_, Error>>()?;
.collect::<Result<HashSet<_>, Error>>()?;
let spent: HashSet<_> = known_utxos.difference(&current_utxos).collect(); let spent: HashSet<_> = known_utxos.difference(&current_utxos).collect();
for s in spent { for s in spent {

View File

@@ -178,9 +178,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
let mut inputs_sum: u64 = 0; let mut inputs_sum: u64 = 0;
let mut outputs_sum: u64 = 0; let mut outputs_sum: u64 = 0;
for (txout, (_input_index, input)) in for (txout, input) in vout.into_iter().zip(tx.input.iter()) {
vout.into_iter().zip(tx.input.iter().enumerate())
{
let txout = match txout { let txout = match txout {
Some(txout) => txout, Some(txout) => txout,
None => { None => {
@@ -192,19 +190,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
continue; continue;
} }
}; };
// Verify this input if requested via feature flag
#[cfg(feature = "verify")]
{
use crate::wallet::verify::VerifyError;
let serialized_tx = bitcoin::consensus::serialize(&tx);
bitcoinconsensus::verify(
txout.script_pubkey.to_bytes().as_ref(),
txout.value,
&serialized_tx,
_input_index,
)
.map_err(VerifyError::from)?;
}
inputs_sum += txout.value; inputs_sum += txout.value;
if self.state.db.is_mine(&txout.script_pubkey)? { if self.state.db.is_mine(&txout.script_pubkey)? {
sent += txout.value; sent += txout.value;
@@ -228,6 +214,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
// we're going to fill this in later // we're going to fill this in later
confirmation_time: None, confirmation_time: None,
fee: Some(fee), fee: Some(fee),
verified: false,
}) })
}) })
.collect::<Result<Vec<_>, _>>()?; .collect::<Result<Vec<_>, _>>()?;

View File

@@ -515,6 +515,7 @@ macro_rules! populate_test_db {
received: 0, received: 0,
sent: 0, sent: 0,
confirmation_time, confirmation_time,
verified: current_height.is_some(),
}; };
db.set_tx(&tx_details).unwrap(); db.set_tx(&tx_details).unwrap();

View File

@@ -348,6 +348,7 @@ pub mod test {
timestamp: 123456, timestamp: 123456,
height: 1000, height: 1000,
}), }),
verified: true,
}; };
tree.set_tx(&tx_details).unwrap(); tree.set_tx(&tx_details).unwrap();

View File

@@ -35,11 +35,7 @@ static MIGRATIONS: &[&str] = &[
"CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);", "CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);",
"CREATE TABLE checksums (keychain TEXT, checksum BLOB);", "CREATE TABLE checksums (keychain TEXT, checksum BLOB);",
"CREATE INDEX idx_checksums_keychain ON checksums(keychain);", "CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);", "CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);"
"ALTER TABLE transaction_details RENAME TO transaction_details_old;",
"CREATE TABLE transaction_details (txid BLOB, timestamp INTEGER, received INTEGER, sent INTEGER, fee INTEGER, height INTEGER);",
"INSERT INTO transaction_details SELECT txid, timestamp, received, sent, fee, height FROM transaction_details_old;",
"DROP TABLE transaction_details_old;",
]; ];
/// Sqlite database stored on filesystem /// Sqlite database stored on filesystem
@@ -131,7 +127,7 @@ impl SqliteDatabase {
let txid: &[u8] = &transaction.txid; let txid: &[u8] = &transaction.txid;
let mut statement = self.connection.prepare_cached("INSERT INTO transaction_details (txid, timestamp, received, sent, fee, height) VALUES (:txid, :timestamp, :received, :sent, :fee, :height)")?; let mut statement = self.connection.prepare_cached("INSERT INTO transaction_details (txid, timestamp, received, sent, fee, height, verified) VALUES (:txid, :timestamp, :received, :sent, :fee, :height, :verified)")?;
statement.execute(named_params! { statement.execute(named_params! {
":txid": txid, ":txid": txid,
@@ -140,6 +136,7 @@ impl SqliteDatabase {
":sent": transaction.sent, ":sent": transaction.sent,
":fee": transaction.fee, ":fee": transaction.fee,
":height": height, ":height": height,
":verified": transaction.verified
})?; })?;
Ok(self.connection.last_insert_rowid()) Ok(self.connection.last_insert_rowid())
@@ -156,7 +153,7 @@ impl SqliteDatabase {
let txid: &[u8] = &transaction.txid; let txid: &[u8] = &transaction.txid;
let mut statement = self.connection.prepare_cached("UPDATE transaction_details SET timestamp=:timestamp, received=:received, sent=:sent, fee=:fee, height=:height WHERE txid=:txid")?; let mut statement = self.connection.prepare_cached("UPDATE transaction_details SET timestamp=:timestamp, received=:received, sent=:sent, fee=:fee, height=:height, verified=:verified WHERE txid=:txid")?;
statement.execute(named_params! { statement.execute(named_params! {
":txid": txid, ":txid": txid,
@@ -165,6 +162,7 @@ impl SqliteDatabase {
":sent": transaction.sent, ":sent": transaction.sent,
":fee": transaction.fee, ":fee": transaction.fee,
":height": height, ":height": height,
":verified": transaction.verified,
})?; })?;
Ok(()) Ok(())
@@ -369,7 +367,7 @@ impl SqliteDatabase {
} }
fn select_transaction_details_with_raw(&self) -> Result<Vec<TransactionDetails>, Error> { fn select_transaction_details_with_raw(&self) -> Result<Vec<TransactionDetails>, Error> {
let mut statement = self.connection.prepare_cached("SELECT transaction_details.txid, transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid = transactions.txid")?; let mut statement = self.connection.prepare_cached("SELECT transaction_details.txid, transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transaction_details.verified, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid = transactions.txid")?;
let mut transaction_details: Vec<TransactionDetails> = vec![]; let mut transaction_details: Vec<TransactionDetails> = vec![];
let mut rows = statement.query([])?; let mut rows = statement.query([])?;
while let Some(row) = rows.next()? { while let Some(row) = rows.next()? {
@@ -380,6 +378,7 @@ impl SqliteDatabase {
let sent: u64 = row.get(3)?; let sent: u64 = row.get(3)?;
let fee: Option<u64> = row.get(4)?; let fee: Option<u64> = row.get(4)?;
let height: Option<u32> = row.get(5)?; let height: Option<u32> = row.get(5)?;
let verified: bool = row.get(6)?;
let raw_tx: Option<Vec<u8>> = row.get(7)?; let raw_tx: Option<Vec<u8>> = row.get(7)?;
let tx: Option<Transaction> = match raw_tx { let tx: Option<Transaction> = match raw_tx {
Some(raw_tx) => { Some(raw_tx) => {
@@ -401,6 +400,7 @@ impl SqliteDatabase {
sent, sent,
fee, fee,
confirmation_time, confirmation_time,
verified,
}); });
} }
Ok(transaction_details) Ok(transaction_details)
@@ -408,7 +408,7 @@ impl SqliteDatabase {
fn select_transaction_details(&self) -> Result<Vec<TransactionDetails>, Error> { fn select_transaction_details(&self) -> Result<Vec<TransactionDetails>, Error> {
let mut statement = self.connection.prepare_cached( let mut statement = self.connection.prepare_cached(
"SELECT txid, timestamp, received, sent, fee, height FROM transaction_details", "SELECT txid, timestamp, received, sent, fee, height, verified FROM transaction_details",
)?; )?;
let mut transaction_details: Vec<TransactionDetails> = vec![]; let mut transaction_details: Vec<TransactionDetails> = vec![];
let mut rows = statement.query([])?; let mut rows = statement.query([])?;
@@ -420,6 +420,7 @@ impl SqliteDatabase {
let sent: u64 = row.get(3)?; let sent: u64 = row.get(3)?;
let fee: Option<u64> = row.get(4)?; let fee: Option<u64> = row.get(4)?;
let height: Option<u32> = row.get(5)?; let height: Option<u32> = row.get(5)?;
let verified: bool = row.get(6)?;
let confirmation_time = match (height, timestamp) { let confirmation_time = match (height, timestamp) {
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }), (Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
@@ -433,6 +434,7 @@ impl SqliteDatabase {
sent, sent,
fee, fee,
confirmation_time, confirmation_time,
verified,
}); });
} }
Ok(transaction_details) Ok(transaction_details)
@@ -442,7 +444,7 @@ impl SqliteDatabase {
&self, &self,
txid: &[u8], txid: &[u8],
) -> Result<Option<TransactionDetails>, Error> { ) -> Result<Option<TransactionDetails>, Error> {
let mut statement = self.connection.prepare_cached("SELECT transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid=transactions.txid AND transaction_details.txid=:txid")?; let mut statement = self.connection.prepare_cached("SELECT transaction_details.timestamp, transaction_details.received, transaction_details.sent, transaction_details.fee, transaction_details.height, transaction_details.verified, transactions.raw_tx FROM transaction_details, transactions WHERE transaction_details.txid=transactions.txid AND transaction_details.txid=:txid")?;
let mut rows = statement.query(named_params! { ":txid": txid })?; let mut rows = statement.query(named_params! { ":txid": txid })?;
match rows.next()? { match rows.next()? {
@@ -452,8 +454,9 @@ impl SqliteDatabase {
let sent: u64 = row.get(2)?; let sent: u64 = row.get(2)?;
let fee: Option<u64> = row.get(3)?; let fee: Option<u64> = row.get(3)?;
let height: Option<u32> = row.get(4)?; let height: Option<u32> = row.get(4)?;
let verified: bool = row.get(5)?;
let raw_tx: Option<Vec<u8>> = row.get(5)?; let raw_tx: Option<Vec<u8>> = row.get(6)?;
let tx: Option<Transaction> = match raw_tx { let tx: Option<Transaction> = match raw_tx {
Some(raw_tx) => { Some(raw_tx) => {
let tx: Transaction = deserialize(&raw_tx)?; let tx: Transaction = deserialize(&raw_tx)?;
@@ -474,6 +477,7 @@ impl SqliteDatabase {
sent, sent,
fee, fee,
confirmation_time, confirmation_time,
verified,
})) }))
} }
None => Ok(None), None => Ok(None),

View File

@@ -17,13 +17,13 @@
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::ops::Deref; use std::ops::Deref;
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource}; use bitcoin::util::bip32::{
ChildNumber, DerivationPath, ExtendedPrivKey, ExtendedPubKey, Fingerprint, KeySource,
};
use bitcoin::util::psbt; use bitcoin::util::psbt;
use bitcoin::{Network, PublicKey, Script, TxOut}; use bitcoin::{Network, PublicKey, Script, TxOut};
use miniscript::descriptor::{ use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey, Wildcard};
DescriptorPublicKey, DescriptorType, DescriptorXKey, InnerXKey, Wildcard,
};
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0}; pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk}; use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
@@ -267,10 +267,41 @@ pub(crate) trait XKeyUtils {
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint; fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
} }
impl<T> XKeyUtils for DescriptorXKey<T> // FIXME: `InnerXKey` was made private in rust-miniscript, so we have to implement this manually on
where // both `ExtendedPubKey` and `ExtendedPrivKey`.
T: InnerXKey, //
{ // Revert back to using the trait once https://github.com/rust-bitcoin/rust-miniscript/pull/230 is
// released
impl XKeyUtils for DescriptorXKey<ExtendedPubKey> {
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
let full_path = match self.origin {
Some((_, ref path)) => path
.into_iter()
.chain(self.derivation_path.into_iter())
.cloned()
.collect(),
None => self.derivation_path.clone(),
};
if self.wildcard != Wildcard::None {
full_path
.into_iter()
.chain(append.iter())
.cloned()
.collect()
} else {
full_path
}
}
fn root_fingerprint(&self, _: &SecpCtx) -> Fingerprint {
match self.origin {
Some((fingerprint, _)) => fingerprint,
None => self.xkey.fingerprint(),
}
}
}
impl XKeyUtils for DescriptorXKey<ExtendedPrivKey> {
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath { fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
let full_path = match self.origin { let full_path = match self.origin {
Some((_, ref path)) => path Some((_, ref path)) => path
@@ -295,7 +326,7 @@ where
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint { fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
match self.origin { match self.origin {
Some((fingerprint, _)) => fingerprint, Some((fingerprint, _)) => fingerprint,
None => self.xkey.xkey_fingerprint(secp), None => self.xkey.fingerprint(secp),
} }
} }
} }

View File

@@ -44,7 +44,7 @@
//! interact with the bitcoin P2P network. //! interact with the bitcoin P2P network.
//! //!
//! ```toml //! ```toml
//! bdk = "0.17.0" //! bdk = "0.15.0"
//! ``` //! ```
#![cfg_attr( #![cfg_attr(
feature = "electrum", feature = "electrum",

View File

@@ -90,19 +90,13 @@ impl TestClient {
map.insert(out.to_address.clone(), Amount::from_sat(out.value)); map.insert(out.to_address.clone(), Amount::from_sat(out.value));
} }
let input: Vec<_> = meta_tx
.input
.into_iter()
.map(|x| x.into_raw_tx_input())
.collect();
if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) { if self.get_balance(None, None).unwrap() < Amount::from_sat(required_balance) {
panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap()); panic!("Insufficient funds in bitcoind. Please generate a few blocks with: `bitcoin-cli generatetoaddress 10 {}`", self.get_new_address(None, None).unwrap());
} }
// FIXME: core can't create a tx with two outputs to the same address // FIXME: core can't create a tx with two outputs to the same address
let tx = self let tx = self
.create_raw_transaction_hex(&input, &map, meta_tx.locktime, meta_tx.replaceable) .create_raw_transaction_hex(&[], &map, meta_tx.locktime, meta_tx.replaceable)
.unwrap(); .unwrap();
let tx = self.fund_raw_transaction(tx, None, None).unwrap(); let tx = self.fund_raw_transaction(tx, None, None).unwrap();
let mut tx: Transaction = deserialize(&tx.hex).unwrap(); let mut tx: Transaction = deserialize(&tx.hex).unwrap();
@@ -359,7 +353,7 @@ macro_rules! bdk_blockchain_tests {
fn $_fn_name:ident ( $( $test_client:ident : &TestClient )? $(,)? ) -> $blockchain:ty $block:block) => { fn $_fn_name:ident ( $( $test_client:ident : &TestClient )? $(,)? ) -> $blockchain:ty $block:block) => {
#[cfg(test)] #[cfg(test)]
mod bdk_blockchain_tests { mod bdk_blockchain_tests {
use $crate::bitcoin::{Transaction, Network}; use $crate::bitcoin::Network;
use $crate::testutils::blockchain_tests::TestClient; use $crate::testutils::blockchain_tests::TestClient;
use $crate::blockchain::Blockchain; use $crate::blockchain::Blockchain;
use $crate::database::MemoryDatabase; use $crate::database::MemoryDatabase;
@@ -621,7 +615,7 @@ macro_rules! bdk_blockchain_tests {
#[test] #[test]
fn test_sync_double_receive() { fn test_sync_double_receive() {
let (wallet, blockchain, descriptors, mut test_client) = init_single_sig(); let (wallet, blockchain, descriptors, mut test_client) = init_single_sig();
let receiver_wallet = get_wallet_from_descriptors(&("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None)); let receiver_wallet = get_wallet_from_descriptors(&("wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None));
// need to sync so rpc can start watching // need to sync so rpc can start watching
receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap(); receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
@@ -629,15 +623,15 @@ macro_rules! bdk_blockchain_tests {
@tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1) @tx ( (@external descriptors, 0) => 50_000, (@external descriptors, 1) => 25_000 ) (@confirmations 1)
}); });
wallet.sync(&blockchain, SyncOptions::default()).expect("sync"); wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance"); assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address; let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
let tx1 = { let tx1 = {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf(); builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf();
let (mut psbt, _details) = builder.finish().expect("building first tx"); let (mut psbt, _details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).expect("signing first tx"); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
psbt.extract_tx() psbt.extract_tx()
}; };
@@ -645,17 +639,17 @@ macro_rules! bdk_blockchain_tests {
let tx2 = { let tx2 = {
let mut builder = wallet.build_tx(); let mut builder = wallet.build_tx();
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf().fee_rate(FeeRate::from_sat_per_vb(5.0)); builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf().fee_rate(FeeRate::from_sat_per_vb(5.0));
let (mut psbt, _details) = builder.finish().expect("building replacement tx"); let (mut psbt, _details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut psbt, Default::default()).expect("signing replacement tx"); let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
psbt.extract_tx() psbt.extract_tx()
}; };
blockchain.broadcast(&tx1).expect("broadcasting first"); blockchain.broadcast(&tx1).unwrap();
blockchain.broadcast(&tx2).expect("broadcasting replacement"); blockchain.broadcast(&tx2).unwrap();
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver"); receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once"); assert_eq!(receiver_wallet.get_balance().unwrap(), 49_000, "should have received coins once and only once");
} }
#[test] #[test]
@@ -817,7 +811,7 @@ macro_rules! bdk_blockchain_tests {
let mut builder = wallet.build_fee_bump(details.txid).unwrap(); let mut builder = wallet.build_fee_bump(details.txid).unwrap();
builder.fee_rate(FeeRate::from_sat_per_vb(2.1)); builder.fee_rate(FeeRate::from_sat_per_vb(2.1));
let (mut new_psbt, new_details) = builder.finish().expect("fee bump tx"); let (mut new_psbt, new_details) = builder.finish().unwrap();
let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap(); let finalized = wallet.sign(&mut new_psbt, Default::default()).unwrap();
assert!(finalized, "Cannot finalize transaction"); assert!(finalized, "Cannot finalize transaction");
blockchain.broadcast(&new_psbt.extract_tx()).unwrap(); blockchain.broadcast(&new_psbt.extract_tx()).unwrap();
@@ -1081,79 +1075,6 @@ macro_rules! bdk_blockchain_tests {
let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap(); let taproot_balance = taproot_wallet_client.get_balance(None, None).unwrap();
assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance"); assert_eq!(taproot_balance.as_sat(), 25_000, "node has incorrect taproot wallet balance");
} }
#[test]
fn test_tx_chain() {
use bitcoincore_rpc::RpcApi;
use bitcoin::consensus::encode::deserialize;
use $crate::wallet::AddressIndex;
// Here we want to test that we set correctly the send and receive
// fields in the transaction object. For doing so, we create two
// different txs, the second one spending from the first:
// 1.
// Core (#1) -> Core (#2)
// -> Us (#3)
// 2.
// Core (#2) -> Us (#4)
let (wallet, blockchain, _, mut test_client) = init_single_sig();
let bdk_address = wallet.get_address(AddressIndex::New).unwrap().address;
let core_address = test_client.get_new_address(None, None).unwrap();
let tx = testutils! {
@tx ( (@addr bdk_address.clone()) => 50_000, (@addr core_address.clone()) => 40_000 )
};
// Tx one: from Core #1 to Core #2 and Us #3.
let txid_1 = test_client.receive(tx);
let tx_1: Transaction = deserialize(&test_client.get_transaction(&txid_1, None).unwrap().hex).unwrap();
let vout_1 = tx_1.output.into_iter().position(|o| o.script_pubkey == core_address.script_pubkey()).unwrap() as u32;
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let tx_1 = wallet.list_transactions(false).unwrap().into_iter().find(|tx| tx.txid == txid_1).unwrap();
assert_eq!(tx_1.received, 50_000);
assert_eq!(tx_1.sent, 0);
// Tx two: from Core #2 to Us #4.
let tx = testutils! {
@tx ( (@addr bdk_address) => 10_000 ) ( @inputs (txid_1,vout_1))
};
let txid_2 = test_client.receive(tx);
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let tx_2 = wallet.list_transactions(false).unwrap().into_iter().find(|tx| tx.txid == txid_2).unwrap();
assert_eq!(tx_2.received, 10_000);
assert_eq!(tx_2.sent, 0);
}
#[test]
fn test_send_receive_pkh() {
let descriptors = ("pkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)".to_string(), None);
let mut test_client = TestClient::default();
let blockchain = get_blockchain(&test_client);
let wallet = get_wallet_from_descriptors(&descriptors);
#[cfg(feature = "test-rpc")]
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
let _ = test_client.receive(testutils! {
@tx ( (@external descriptors, 0) => 50_000 )
});
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
assert_eq!(wallet.get_balance().unwrap(), 50_000);
let tx = {
let mut builder = wallet.build_tx();
builder.add_recipient(test_client.get_node_address(None).script_pubkey(), 25_000);
let (mut psbt, _details) = builder.finish().unwrap();
wallet.sign(&mut psbt, Default::default()).unwrap();
psbt.extract_tx()
};
blockchain.broadcast(&tx).unwrap();
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
}
} }
}; };

View File

@@ -15,37 +15,11 @@
pub mod blockchain_tests; pub mod blockchain_tests;
use bitcoin::secp256k1::{Secp256k1, Verification}; use bitcoin::secp256k1::{Secp256k1, Verification};
use bitcoin::{Address, PublicKey, Txid}; use bitcoin::{Address, PublicKey};
use miniscript::descriptor::DescriptorPublicKey; use miniscript::descriptor::DescriptorPublicKey;
use miniscript::{Descriptor, MiniscriptKey, TranslatePk}; use miniscript::{Descriptor, MiniscriptKey, TranslatePk};
#[derive(Clone, Debug)]
pub struct TestIncomingInput {
pub txid: Txid,
pub vout: u32,
pub sequence: Option<u32>,
}
impl TestIncomingInput {
pub fn new(txid: Txid, vout: u32, sequence: Option<u32>) -> Self {
Self {
txid,
vout,
sequence,
}
}
#[cfg(feature = "test-blockchains")]
pub fn into_raw_tx_input(self) -> bitcoincore_rpc::json::CreateRawTransactionInput {
bitcoincore_rpc::json::CreateRawTransactionInput {
txid: self.txid,
vout: self.vout,
sequence: self.sequence,
}
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TestIncomingOutput { pub struct TestIncomingOutput {
pub value: u64, pub value: u64,
@@ -63,7 +37,6 @@ impl TestIncomingOutput {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct TestIncomingTx { pub struct TestIncomingTx {
pub input: Vec<TestIncomingInput>,
pub output: Vec<TestIncomingOutput>, pub output: Vec<TestIncomingOutput>,
pub min_confirmations: Option<u64>, pub min_confirmations: Option<u64>,
pub locktime: Option<i64>, pub locktime: Option<i64>,
@@ -72,14 +45,12 @@ pub struct TestIncomingTx {
impl TestIncomingTx { impl TestIncomingTx {
pub fn new( pub fn new(
input: Vec<TestIncomingInput>,
output: Vec<TestIncomingOutput>, output: Vec<TestIncomingOutput>,
min_confirmations: Option<u64>, min_confirmations: Option<u64>,
locktime: Option<i64>, locktime: Option<i64>,
replaceable: Option<bool>, replaceable: Option<bool>,
) -> Self { ) -> Self {
Self { Self {
input,
output, output,
min_confirmations, min_confirmations,
locktime, locktime,
@@ -87,10 +58,6 @@ impl TestIncomingTx {
} }
} }
pub fn add_input(&mut self, input: TestIncomingInput) {
self.input.push(input);
}
pub fn add_output(&mut self, output: TestIncomingOutput) { pub fn add_output(&mut self, output: TestIncomingOutput) {
self.output.push(output); self.output.push(output);
} }
@@ -156,21 +123,16 @@ macro_rules! testutils {
}); });
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) }); ( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $descriptors, $child) });
( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) }); ( @i $descriptors:expr, $child:expr ) => ({ testutils!(@internal $descriptors, $child) });
( @addr $addr:expr ) => ({ $addr });
( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @inputs $( ($txid:expr, $vout:expr) ),+ ) )? $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({ ( @tx ( $( ( $( $addr:tt )* ) => $amount:expr ),+ ) $( ( @locktime $locktime:expr ) )? $( ( @confirmations $confirmations:expr ) )? $( ( @replaceable $replaceable:expr ) )? ) => ({
let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+]; let outs = vec![$( $crate::testutils::TestIncomingOutput::new($amount, testutils!( $($addr)* ))),+];
let _ins: Vec<$crate::testutils::TestIncomingInput> = vec![];
$(
let _ins = vec![$( $crate::testutils::TestIncomingInput { txid: $txid, vout: $vout, sequence: None }),+];
)?
let locktime = None::<i64>$(.or(Some($locktime)))?; let locktime = None::<i64>$(.or(Some($locktime)))?;
let min_confirmations = None::<u64>$(.or(Some($confirmations)))?; let min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
let replaceable = None::<bool>$(.or(Some($replaceable)))?; let replaceable = None::<bool>$(.or(Some($replaceable)))?;
$crate::testutils::TestIncomingTx::new(_ins, outs, min_confirmations, locktime, replaceable) $crate::testutils::TestIncomingTx::new(outs, min_confirmations, locktime, replaceable)
}); });
( @literal $key:expr ) => ({ ( @literal $key:expr ) => ({

View File

@@ -211,6 +211,15 @@ pub struct TransactionDetails {
/// If the transaction is confirmed, contains height and timestamp of the block containing the /// If the transaction is confirmed, contains height and timestamp of the block containing the
/// transaction, unconfirmed transaction contains `None`. /// transaction, unconfirmed transaction contains `None`.
pub confirmation_time: Option<BlockTime>, pub confirmation_time: Option<BlockTime>,
/// Whether the tx has been verified against the consensus rules
///
/// Confirmed txs are considered "verified" by default, while unconfirmed txs are checked to
/// ensure an unstrusted [`Blockchain`](crate::blockchain::Blockchain) backend can't trick the
/// wallet into using an invalid tx as an RBF template.
///
/// The check is only performed when the `verify` feature is enabled.
#[serde(default = "bool::default")] // default to `false` if not specified
pub verified: bool,
} }
/// Block height and timestamp of a block /// Block height and timestamp of a block

View File

@@ -230,6 +230,7 @@ mod test {
timestamp: 12345678, timestamp: 12345678,
height: 5000, height: 5000,
}), }),
verified: true,
}) })
.unwrap(); .unwrap();

View File

@@ -157,29 +157,18 @@ impl fmt::Display for AddressInfo {
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
/// Options to a [`sync`]. /// Options to a [`Wallet::sync`]
///
/// [`sync`]: Wallet::sync
pub struct SyncOptions { pub struct SyncOptions {
/// The progress tracker which may be informed when progress is made. /// The progress tracker which may be informated when progress is made.
pub progress: Option<Box<dyn Progress>>, pub progress: Option<Box<dyn Progress>>,
/// The maximum number of addresses sync on.
pub max_addresses: Option<u32>,
} }
impl<D> Wallet<D> impl<D> Wallet<D>
where where
D: BatchDatabase, D: BatchDatabase,
{ {
#[deprecated = "Just use Wallet::new -- all wallets are offline now!"]
/// Create a new "offline" wallet
pub fn new_offline<E: IntoWalletDescriptor>(
descriptor: E,
change_descriptor: Option<E>,
network: Network,
database: D,
) -> Result<Self, Error> {
Self::new(descriptor, change_descriptor, network, database)
}
/// Create a wallet. /// Create a wallet.
/// ///
/// The only way this can fail is if the descriptors passed in do not match the checksums in `database`. /// The only way this can fail is if the descriptors passed in do not match the checksums in `database`.
@@ -233,12 +222,12 @@ where
self.network self.network
} }
// Return a newly derived address for the specified `keychain`. // Return a newly derived address using the external descriptor
fn get_new_address(&self, keychain: KeychainKind) -> Result<AddressInfo, Error> { fn get_new_address(&self) -> Result<AddressInfo, Error> {
let incremented_index = self.fetch_and_increment_index(keychain)?; let incremented_index = self.fetch_and_increment_index(KeychainKind::External)?;
let address_result = self let address_result = self
.get_descriptor_for_keychain(keychain) .descriptor
.as_derived(incremented_index, &self.secp) .as_derived(incremented_index, &self.secp)
.address(self.network); .address(self.network);
@@ -250,14 +239,12 @@ where
.map_err(|_| Error::ScriptDoesntHaveAddressForm) .map_err(|_| Error::ScriptDoesntHaveAddressForm)
} }
// Return the the last previously derived address for `keychain` if it has not been used in a // Return the the last previously derived address if it has not been used in a received
// received transaction. Otherwise return a new address using [`Wallet::get_new_address`]. // transaction. Otherwise return a new address using [`Wallet::get_new_address`].
fn get_unused_address(&self, keychain: KeychainKind) -> Result<AddressInfo, Error> { fn get_unused_address(&self) -> Result<AddressInfo, Error> {
let current_index = self.fetch_index(keychain)?; let current_index = self.fetch_index(KeychainKind::External)?;
let derived_key = self let derived_key = self.descriptor.as_derived(current_index, &self.secp);
.get_descriptor_for_keychain(keychain)
.as_derived(current_index, &self.secp);
let script_pubkey = derived_key.script_pubkey(); let script_pubkey = derived_key.script_pubkey();
@@ -269,7 +256,7 @@ where
.any(|o| o.script_pubkey == script_pubkey); .any(|o| o.script_pubkey == script_pubkey);
if found_used { if found_used {
self.get_new_address(keychain) self.get_new_address()
} else { } else {
derived_key derived_key
.address(self.network) .address(self.network)
@@ -281,21 +268,21 @@ where
} }
} }
// Return derived address for the descriptor of given [`KeychainKind`] at a specific index // Return derived address for the external descriptor at a specific index
fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> { fn peek_address(&self, index: u32) -> Result<AddressInfo, Error> {
self.get_descriptor_for_keychain(keychain) self.descriptor
.as_derived(index, &self.secp) .as_derived(index, &self.secp)
.address(self.network) .address(self.network)
.map(|address| AddressInfo { index, address }) .map(|address| AddressInfo { index, address })
.map_err(|_| Error::ScriptDoesntHaveAddressForm) .map_err(|_| Error::ScriptDoesntHaveAddressForm)
} }
// Return derived address for `keychain` at a specific index and reset current // Return derived address for the external descriptor at a specific index and reset current
// address index // address index
fn reset_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> { fn reset_address(&self, index: u32) -> Result<AddressInfo, Error> {
self.set_index(keychain, index)?; self.set_index(KeychainKind::External, index)?;
self.get_descriptor_for_keychain(keychain) self.descriptor
.as_derived(index, &self.secp) .as_derived(index, &self.secp)
.address(self.network) .address(self.network)
.map(|address| AddressInfo { index, address }) .map(|address| AddressInfo { index, address })
@@ -306,77 +293,14 @@ where
/// available address index selection strategies. If none of the keys in the descriptor are derivable /// 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`]. /// (ie. 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> { pub fn get_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
self._get_address(address_index, KeychainKind::External)
}
/// Return a derived address using the internal (change) descriptor.
///
/// 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
/// be returned for any [`AddressIndex`].
pub fn get_internal_address(&self, address_index: AddressIndex) -> Result<AddressInfo, Error> {
self._get_address(address_index, KeychainKind::Internal)
}
fn _get_address(
&self,
address_index: AddressIndex,
keychain: KeychainKind,
) -> Result<AddressInfo, Error> {
match address_index { match address_index {
AddressIndex::New => self.get_new_address(keychain), AddressIndex::New => self.get_new_address(),
AddressIndex::LastUnused => self.get_unused_address(keychain), AddressIndex::LastUnused => self.get_unused_address(),
AddressIndex::Peek(index) => self.peek_address(index, keychain), AddressIndex::Peek(index) => self.peek_address(index),
AddressIndex::Reset(index) => self.reset_address(index, keychain), AddressIndex::Reset(index) => self.reset_address(index),
} }
} }
/// Ensures that there are at least `max_addresses` addresses cached in the database if the
/// descriptor is derivable, or 1 address if it is not.
/// Will return `Ok(true)` if there are new addresses generated (either external or internal),
/// and `Ok(false)` if all the required addresses are already cached. This function is useful to
/// explicitly cache addresses in a wallet to do things like check [`Wallet::is_mine`] on
/// transaction output scripts.
pub fn ensure_addresses_cached(&self, max_addresses: u32) -> Result<bool, Error> {
let mut new_addresses_cached = false;
let max_address = match self.descriptor.is_deriveable() {
false => 0,
true => max_addresses,
};
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");
new_addresses_cached = true;
self.cache_addresses(KeychainKind::External, 0, max_address)?;
}
if let Some(change_descriptor) = &self.change_descriptor {
let max_address = match change_descriptor.is_deriveable() {
false => 0,
true => max_addresses,
};
if self
.database
.borrow()
.get_script_pubkey_from_path(KeychainKind::Internal, max_address.saturating_sub(1))?
.is_none()
{
debug!("caching internal addresses");
new_addresses_cached = true;
self.cache_addresses(KeychainKind::Internal, 0, max_address)?;
}
}
Ok(new_addresses_cached)
}
/// Return whether or not a `script` is part of this wallet (either internal or external) /// Return whether or not a `script` is part of this wallet (either internal or external)
pub fn is_mine(&self, script: &Script) -> Result<bool, Error> { pub fn is_mine(&self, script: &Script) -> Result<bool, Error> {
self.database.borrow().is_mine(script) self.database.borrow().is_mine(script)
@@ -723,10 +647,7 @@ where
let mut drain_output = { let mut drain_output = {
let script_pubkey = match params.drain_to { let script_pubkey = match params.drain_to {
Some(ref drain_recipient) => drain_recipient.clone(), Some(ref drain_recipient) => drain_recipient.clone(),
None => self None => self.get_change_address()?,
.get_internal_address(AddressIndex::New)?
.address
.script_pubkey(),
}; };
TxOut { TxOut {
@@ -776,6 +697,7 @@ where
received, received,
sent, sent,
fee: Some(fee_amount), fee: Some(fee_amount),
verified: true,
}; };
Ok((psbt, transaction_details)) Ok((psbt, transaction_details))
@@ -1159,6 +1081,13 @@ where
.map(|(desc, child)| desc.as_derived(child, &self.secp))) .map(|(desc, child)| desc.as_derived(child, &self.secp)))
} }
fn get_change_address(&self) -> Result<Script, Error> {
let (desc, keychain) = self._get_descriptor_for_keychain(KeychainKind::Internal);
let index = self.fetch_and_increment_index(keychain)?;
Ok(desc.as_derived(index, &self.secp).script_pubkey())
}
fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> { fn fetch_and_increment_index(&self, keychain: KeychainKind) -> Result<u32, Error> {
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain); let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
let index = match descriptor.is_deriveable() { let index = match descriptor.is_deriveable() {
@@ -1522,10 +1451,46 @@ where
) -> Result<(), Error> { ) -> Result<(), Error> {
debug!("Begin sync..."); debug!("Begin sync...");
let SyncOptions { progress } = sync_opts; let mut run_setup = false;
let SyncOptions {
max_addresses,
progress,
} = sync_opts;
let progress = progress.unwrap_or_else(|| Box::new(NoopProgress)); let progress = progress.unwrap_or_else(|| Box::new(NoopProgress));
let run_setup = self.ensure_addresses_cached(CACHE_ADDR_BATCH_SIZE)?; let max_address = match self.descriptor.is_deriveable() {
false => 0,
true => max_addresses.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)?;
}
if let Some(change_descriptor) = &self.change_descriptor {
let max_address = match change_descriptor.is_deriveable() {
false => 0,
true => max_addresses.unwrap_or(CACHE_ADDR_BATCH_SIZE),
};
if self
.database
.borrow()
.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); debug!("run_setup: {}", run_setup);
// TODO: what if i generate an address first and cache some addresses? // TODO: what if i generate an address first and cache some addresses?
@@ -1538,6 +1503,23 @@ where
maybe_await!(blockchain.wallet_sync(self.database.borrow_mut().deref_mut(), progress,))?; maybe_await!(blockchain.wallet_sync(self.database.borrow_mut().deref_mut(), progress,))?;
} }
#[cfg(feature = "verify")]
{
debug!("Verifying transactions...");
for mut tx in self.database.borrow().iter_txs(true)? {
if !tx.verified {
verify::verify_tx(
tx.transaction.as_ref().ok_or(Error::TransactionNotFound)?,
self.database.borrow().deref(),
blockchain,
)?;
tx.verified = true;
self.database.borrow_mut().set_tx(&tx)?;
}
}
}
let sync_time = SyncTime { let sync_time = SyncTime {
block_time: BlockTime { block_time: BlockTime {
height: maybe_await!(blockchain.get_height())?, height: maybe_await!(blockchain.get_height())?,
@@ -3976,48 +3958,6 @@ pub(crate) mod test {
builder.add_recipient(addr.script_pubkey(), 45_000); builder.add_recipient(addr.script_pubkey(), 45_000);
builder.finish().unwrap(); builder.finish().unwrap();
} }
#[test]
fn test_get_address() {
use crate::descriptor::template::Bip84;
let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
let wallet = Wallet::new(
Bip84(key, KeychainKind::External),
Some(Bip84(key, KeychainKind::Internal)),
Network::Regtest,
MemoryDatabase::default(),
)
.unwrap();
assert_eq!(
wallet.get_address(AddressIndex::New).unwrap().address,
Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap()
);
assert_eq!(
wallet
.get_internal_address(AddressIndex::New)
.unwrap()
.address,
Address::from_str("bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa").unwrap()
);
let wallet = Wallet::new(
Bip84(key, KeychainKind::External),
None,
Network::Regtest,
MemoryDatabase::default(),
)
.unwrap();
assert_eq!(
wallet
.get_internal_address(AddressIndex::New)
.unwrap()
.address,
Address::from_str("bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s").unwrap(),
"when there's no internal descriptor it should just use external"
);
}
} }
/// Deterministically generate a unique name given the descriptors defining the wallet /// Deterministically generate a unique name given the descriptors defining the wallet

View File

@@ -17,7 +17,7 @@ use std::fmt;
use bitcoin::consensus::serialize; use bitcoin::consensus::serialize;
use bitcoin::{OutPoint, Transaction, Txid}; use bitcoin::{OutPoint, Transaction, Txid};
use crate::blockchain::GetTx; use crate::blockchain::Blockchain;
use crate::database::Database; use crate::database::Database;
use crate::error::Error; use crate::error::Error;
@@ -29,7 +29,7 @@ use crate::error::Error;
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the /// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
/// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or /// [`Blockchain`] backend, the method could fail when called with old "historical" transactions or
/// with unconfirmed transactions that have been evicted from the backend's memory. /// with unconfirmed transactions that have been evicted from the backend's memory.
pub fn verify_tx<D: Database, B: GetTx>( pub fn verify_tx<D: Database, B: Blockchain>(
tx: &Transaction, tx: &Transaction,
database: &D, database: &D,
blockchain: &B, blockchain: &B,
@@ -104,18 +104,43 @@ impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use std::collections::HashSet;
use crate::database::{BatchOperations, MemoryDatabase};
use bitcoin::consensus::encode::deserialize; use bitcoin::consensus::encode::deserialize;
use bitcoin::hashes::hex::FromHex; use bitcoin::hashes::hex::FromHex;
use bitcoin::{Transaction, Txid}; use bitcoin::{Transaction, Txid};
use crate::blockchain::{Blockchain, Capability, Progress};
use crate::database::{BatchDatabase, BatchOperations, MemoryDatabase};
use crate::FeeRate;
use super::*;
struct DummyBlockchain; struct DummyBlockchain;
impl GetTx for DummyBlockchain { impl Blockchain for DummyBlockchain {
fn get_capabilities(&self) -> HashSet<Capability> {
Default::default()
}
fn setup<D: BatchDatabase, P: 'static + Progress>(
&self,
_database: &mut D,
_progress_update: P,
) -> Result<(), Error> {
Ok(())
}
fn get_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> { fn get_tx(&self, _txid: &Txid) -> Result<Option<Transaction>, Error> {
Ok(None) Ok(None)
} }
fn broadcast(&self, _tx: &Transaction) -> Result<(), Error> {
Ok(())
}
fn get_height(&self) -> Result<u32, Error> {
Ok(42)
}
fn estimate_fee(&self, _target: usize) -> Result<FeeRate, Error> {
Ok(FeeRate::default_min_relay_fee())
}
} }
#[test] #[test]