Compare commits
2 Commits
v0.17.0
...
remove-blo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8de422dcfd | ||
|
|
733300623e |
6
.github/workflows/cont_integration.yml
vendored
6
.github/workflows/cont_integration.yml
vendored
@@ -89,13 +89,13 @@ jobs:
|
||||
matrix:
|
||||
blockchain:
|
||||
- name: electrum
|
||||
features: test-electrum,verify
|
||||
features: test-electrum
|
||||
- name: rpc
|
||||
features: test-rpc
|
||||
- name: esplora
|
||||
features: test-esplora,use-esplora-reqwest,verify
|
||||
features: test-esplora,use-esplora-reqwest
|
||||
- name: esplora
|
||||
features: test-esplora,use-esplora-ureq,verify
|
||||
features: test-esplora,use-esplora-ureq
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
2
.github/workflows/nightly_docs.yml
vendored
2
.github/workflows/nightly_docs.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
target
|
||||
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2022-01-25
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -6,41 +6,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [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
|
||||
|
||||
To decouple the `Wallet` from the `Blockchain` we've made major changes:
|
||||
|
||||
- Removed `Blockchain` from Wallet.
|
||||
- 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`.
|
||||
- Stop making a request for the block height when calling `Wallet:new`.
|
||||
- Added `SyncOptions` to capture extra (future) arguments to `Wallet::sync`.
|
||||
- Removed `max_addresses` sync parameter which determined how many addresses to cache before syncing since this can just be done with `ensure_addresses_cached`.
|
||||
|
||||
## [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]
|
||||
|
||||
- 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.
|
||||
- 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]
|
||||
|
||||
@@ -417,6 +401,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.11.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
|
||||
@@ -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.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.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
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.17.0"
|
||||
version = "0.15.1-dev"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -42,7 +42,7 @@ bitcoincore-rpc = { version = "0.14", optional = true }
|
||||
|
||||
# Platform-specific 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]
|
||||
async-trait = "0.1"
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
//!
|
||||
//! ## 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:
|
||||
//!
|
||||
//! ```no_run
|
||||
@@ -89,6 +89,9 @@ impl Blockchain for AnyBlockchain {
|
||||
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> {
|
||||
maybe_await!(impl_inner_method!(self, broadcast, tx))
|
||||
}
|
||||
@@ -98,21 +101,12 @@ impl Blockchain for AnyBlockchain {
|
||||
}
|
||||
}
|
||||
|
||||
#[maybe_async]
|
||||
impl GetHeight for AnyBlockchain {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
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 {
|
||||
fn wallet_sync<D: BatchDatabase>(
|
||||
&self,
|
||||
|
||||
@@ -67,7 +67,7 @@ mod peer;
|
||||
mod store;
|
||||
mod sync;
|
||||
|
||||
use crate::blockchain::*;
|
||||
use super::{Blockchain, Capability, ConfigurableBlockchain, GetHeight, Progress, WalletSync};
|
||||
use crate::database::{BatchDatabase, BatchOperations, DatabaseUtils};
|
||||
use crate::error::Error;
|
||||
use crate::types::{KeychainKind, LocalUtxo, TransactionDetails};
|
||||
@@ -207,6 +207,7 @@ impl CompactFiltersBlockchain {
|
||||
received: incoming,
|
||||
sent: outgoing,
|
||||
confirmation_time: BlockTime::new(height, timestamp),
|
||||
verified: height.is_some(),
|
||||
fee: Some(inputs_sum.saturating_sub(outputs_sum)),
|
||||
};
|
||||
|
||||
@@ -225,6 +226,12 @@ impl Blockchain for CompactFiltersBlockchain {
|
||||
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> {
|
||||
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 {
|
||||
#[allow(clippy::mutex_atomic)] // Mutex is easier to understand than a CAS loop.
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
|
||||
@@ -68,6 +68,10 @@ impl Blockchain for ElectrumBlockchain {
|
||||
.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> {
|
||||
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 {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
@@ -204,7 +202,6 @@ impl WalletSync for ElectrumBlockchain {
|
||||
let full_details = full_transactions
|
||||
.into_iter()
|
||||
.map(|tx| {
|
||||
let mut input_index = 0usize;
|
||||
let prev_outputs = tx
|
||||
.input
|
||||
.iter()
|
||||
@@ -219,7 +216,6 @@ impl WalletSync for ElectrumBlockchain {
|
||||
.output
|
||||
.get(input.previous_output.vout as usize)
|
||||
.ok_or_else(electrum_goof)?;
|
||||
input_index += 1;
|
||||
Ok(Some(txout.clone()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct Vin {
|
||||
// None if coinbase
|
||||
pub prevout: Option<PrevOut>,
|
||||
pub scriptsig: Script,
|
||||
#[serde(deserialize_with = "deserialize_witness", default)]
|
||||
#[serde(deserialize_with = "deserialize_witness")]
|
||||
pub witness: Vec<Vec<u8>>,
|
||||
pub sequence: u32,
|
||||
pub is_coinbase: bool,
|
||||
|
||||
@@ -91,6 +91,10 @@ impl Blockchain for EsploraBlockchain {
|
||||
.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> {
|
||||
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]
|
||||
impl WalletSync for EsploraBlockchain {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
fn wallet_setup<D: BatchDatabase, P: Progress>(
|
||||
&self,
|
||||
database: &mut D,
|
||||
_progress_update: Box<dyn Progress>,
|
||||
_progress_update: P,
|
||||
) -> Result<(), Error> {
|
||||
use crate::blockchain::script_sync::Request;
|
||||
let mut request = script_sync::start(database, self.stop_gap)?;
|
||||
@@ -193,9 +190,9 @@ impl WalletSync for EsploraBlockchain {
|
||||
.request()
|
||||
.map(|txid| {
|
||||
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)?
|
||||
}
|
||||
Request::Finish(batch_update) => break batch_update,
|
||||
|
||||
@@ -87,6 +87,10 @@ impl Blockchain for EsploraBlockchain {
|
||||
.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> {
|
||||
let _txid = self.url_client._broadcast(tx)?;
|
||||
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 {
|
||||
fn wallet_setup<D: BatchDatabase>(
|
||||
&self,
|
||||
@@ -190,9 +188,9 @@ impl WalletSync for EsploraBlockchain {
|
||||
.request()
|
||||
.map(|txid| {
|
||||
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)?
|
||||
}
|
||||
Request::Finish(batch_update) => break batch_update,
|
||||
|
||||
@@ -86,9 +86,11 @@ pub enum Capability {
|
||||
|
||||
/// Trait that defines the actions that must be supported by a blockchain backend
|
||||
#[maybe_async]
|
||||
pub trait Blockchain: WalletSync + GetHeight + GetTx {
|
||||
pub trait Blockchain: WalletSync + GetHeight {
|
||||
/// Return the set of [`Capability`] supported by this backend
|
||||
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
|
||||
fn broadcast(&self, tx: &Transaction) -> Result<(), Error>;
|
||||
/// 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>;
|
||||
}
|
||||
|
||||
#[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.
|
||||
#[maybe_async]
|
||||
pub trait WalletSync {
|
||||
@@ -235,6 +230,9 @@ impl<T: Blockchain> Blockchain for Arc<T> {
|
||||
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> {
|
||||
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]
|
||||
impl<T: GetHeight> GetHeight for Arc<T> {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
|
||||
@@ -33,7 +33,9 @@
|
||||
|
||||
use crate::bitcoin::consensus::deserialize;
|
||||
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::{BlockTime, Error, FeeRate, KeychainKind, LocalUtxo, TransactionDetails};
|
||||
use bitcoincore_rpc::json::{
|
||||
@@ -139,6 +141,10 @@ impl Blockchain for RpcBlockchain {
|
||||
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> {
|
||||
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 {
|
||||
fn get_height(&self) -> Result<u32, Error> {
|
||||
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() {
|
||||
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,
|
||||
sent,
|
||||
fee: tx_result.fee.map(|f| f.as_sat().abs() as u64),
|
||||
verified: true,
|
||||
};
|
||||
debug!(
|
||||
"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 = current_utxo
|
||||
let current_utxos: HashSet<_> = current_utxo
|
||||
.into_iter()
|
||||
.filter_map(
|
||||
|u| match db.get_path_from_script_pubkey(&u.script_pub_key) {
|
||||
Err(e) => Some(Err(e)),
|
||||
Ok(None) => None,
|
||||
Ok(Some(path)) => Some(Ok(LocalUtxo {
|
||||
outpoint: OutPoint::new(u.txid, u.vout),
|
||||
keychain: path.0,
|
||||
txout: TxOut {
|
||||
value: u.amount.as_sat(),
|
||||
script_pubkey: u.script_pub_key,
|
||||
},
|
||||
})),
|
||||
},
|
||||
)
|
||||
.collect::<Result<HashSet<_>, Error>>()?;
|
||||
.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(¤t_utxos).collect();
|
||||
for s in spent {
|
||||
|
||||
@@ -178,9 +178,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
|
||||
let mut inputs_sum: u64 = 0;
|
||||
let mut outputs_sum: u64 = 0;
|
||||
|
||||
for (txout, (_input_index, input)) in
|
||||
vout.into_iter().zip(tx.input.iter().enumerate())
|
||||
{
|
||||
for (txout, input) in vout.into_iter().zip(tx.input.iter()) {
|
||||
let txout = match txout {
|
||||
Some(txout) => txout,
|
||||
None => {
|
||||
@@ -192,19 +190,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
|
||||
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;
|
||||
if self.state.db.is_mine(&txout.script_pubkey)? {
|
||||
sent += txout.value;
|
||||
@@ -228,6 +214,7 @@ impl<'a, D: BatchDatabase> TxReq<'a, D> {
|
||||
// we're going to fill this in later
|
||||
confirmation_time: None,
|
||||
fee: Some(fee),
|
||||
verified: false,
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
@@ -515,6 +515,7 @@ macro_rules! populate_test_db {
|
||||
received: 0,
|
||||
sent: 0,
|
||||
confirmation_time,
|
||||
verified: current_height.is_some(),
|
||||
};
|
||||
|
||||
db.set_tx(&tx_details).unwrap();
|
||||
|
||||
@@ -348,6 +348,7 @@ pub mod test {
|
||||
timestamp: 123456,
|
||||
height: 1000,
|
||||
}),
|
||||
verified: true,
|
||||
};
|
||||
|
||||
tree.set_tx(&tx_details).unwrap();
|
||||
|
||||
@@ -35,11 +35,7 @@ static MIGRATIONS: &[&str] = &[
|
||||
"CREATE UNIQUE INDEX idx_indices_keychain ON last_derivation_indices(keychain);",
|
||||
"CREATE TABLE checksums (keychain TEXT, checksum BLOB);",
|
||||
"CREATE INDEX idx_checksums_keychain ON checksums(keychain);",
|
||||
"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;",
|
||||
"CREATE TABLE sync_time (id INTEGER PRIMARY KEY, height INTEGER, timestamp INTEGER);"
|
||||
];
|
||||
|
||||
/// Sqlite database stored on filesystem
|
||||
@@ -131,7 +127,7 @@ impl SqliteDatabase {
|
||||
|
||||
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! {
|
||||
":txid": txid,
|
||||
@@ -140,6 +136,7 @@ impl SqliteDatabase {
|
||||
":sent": transaction.sent,
|
||||
":fee": transaction.fee,
|
||||
":height": height,
|
||||
":verified": transaction.verified
|
||||
})?;
|
||||
|
||||
Ok(self.connection.last_insert_rowid())
|
||||
@@ -156,7 +153,7 @@ impl SqliteDatabase {
|
||||
|
||||
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! {
|
||||
":txid": txid,
|
||||
@@ -165,6 +162,7 @@ impl SqliteDatabase {
|
||||
":sent": transaction.sent,
|
||||
":fee": transaction.fee,
|
||||
":height": height,
|
||||
":verified": transaction.verified,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
@@ -369,7 +367,7 @@ impl SqliteDatabase {
|
||||
}
|
||||
|
||||
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 rows = statement.query([])?;
|
||||
while let Some(row) = rows.next()? {
|
||||
@@ -380,6 +378,7 @@ impl SqliteDatabase {
|
||||
let sent: u64 = row.get(3)?;
|
||||
let fee: Option<u64> = row.get(4)?;
|
||||
let height: Option<u32> = row.get(5)?;
|
||||
let verified: bool = row.get(6)?;
|
||||
let raw_tx: Option<Vec<u8>> = row.get(7)?;
|
||||
let tx: Option<Transaction> = match raw_tx {
|
||||
Some(raw_tx) => {
|
||||
@@ -401,6 +400,7 @@ impl SqliteDatabase {
|
||||
sent,
|
||||
fee,
|
||||
confirmation_time,
|
||||
verified,
|
||||
});
|
||||
}
|
||||
Ok(transaction_details)
|
||||
@@ -408,7 +408,7 @@ impl SqliteDatabase {
|
||||
|
||||
fn select_transaction_details(&self) -> Result<Vec<TransactionDetails>, Error> {
|
||||
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 rows = statement.query([])?;
|
||||
@@ -420,6 +420,7 @@ impl SqliteDatabase {
|
||||
let sent: u64 = row.get(3)?;
|
||||
let fee: Option<u64> = row.get(4)?;
|
||||
let height: Option<u32> = row.get(5)?;
|
||||
let verified: bool = row.get(6)?;
|
||||
|
||||
let confirmation_time = match (height, timestamp) {
|
||||
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
|
||||
@@ -433,6 +434,7 @@ impl SqliteDatabase {
|
||||
sent,
|
||||
fee,
|
||||
confirmation_time,
|
||||
verified,
|
||||
});
|
||||
}
|
||||
Ok(transaction_details)
|
||||
@@ -442,7 +444,7 @@ impl SqliteDatabase {
|
||||
&self,
|
||||
txid: &[u8],
|
||||
) -> 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 })?;
|
||||
|
||||
match rows.next()? {
|
||||
@@ -452,8 +454,9 @@ impl SqliteDatabase {
|
||||
let sent: u64 = row.get(2)?;
|
||||
let fee: Option<u64> = row.get(3)?;
|
||||
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 {
|
||||
Some(raw_tx) => {
|
||||
let tx: Transaction = deserialize(&raw_tx)?;
|
||||
@@ -474,6 +477,7 @@ impl SqliteDatabase {
|
||||
sent,
|
||||
fee,
|
||||
confirmation_time,
|
||||
verified,
|
||||
}))
|
||||
}
|
||||
None => Ok(None),
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
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::{Network, PublicKey, Script, TxOut};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
DescriptorPublicKey, DescriptorType, DescriptorXKey, InnerXKey, Wildcard,
|
||||
};
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorType, DescriptorXKey, Wildcard};
|
||||
pub use miniscript::{descriptor::KeyMap, Descriptor, Legacy, Miniscript, ScriptContext, Segwitv0};
|
||||
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
||||
|
||||
@@ -267,10 +267,41 @@ pub(crate) trait XKeyUtils {
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
||||
}
|
||||
|
||||
impl<T> XKeyUtils for DescriptorXKey<T>
|
||||
where
|
||||
T: InnerXKey,
|
||||
{
|
||||
// FIXME: `InnerXKey` was made private in rust-miniscript, so we have to implement this manually on
|
||||
// both `ExtendedPubKey` and `ExtendedPrivKey`.
|
||||
//
|
||||
// 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 {
|
||||
let full_path = match self.origin {
|
||||
Some((_, ref path)) => path
|
||||
@@ -295,7 +326,7 @@ where
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
||||
match self.origin {
|
||||
Some((fingerprint, _)) => fingerprint,
|
||||
None => self.xkey.xkey_fingerprint(secp),
|
||||
None => self.xkey.fingerprint(secp),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
//! interact with the bitcoin P2P network.
|
||||
//!
|
||||
//! ```toml
|
||||
//! bdk = "0.17.0"
|
||||
//! bdk = "0.15.0"
|
||||
//! ```
|
||||
#![cfg_attr(
|
||||
feature = "electrum",
|
||||
|
||||
@@ -90,19 +90,13 @@ impl TestClient {
|
||||
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) {
|
||||
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
|
||||
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();
|
||||
let tx = self.fund_raw_transaction(tx, None, None).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) => {
|
||||
#[cfg(test)]
|
||||
mod bdk_blockchain_tests {
|
||||
use $crate::bitcoin::{Transaction, Network};
|
||||
use $crate::bitcoin::Network;
|
||||
use $crate::testutils::blockchain_tests::TestClient;
|
||||
use $crate::blockchain::Blockchain;
|
||||
use $crate::database::MemoryDatabase;
|
||||
@@ -621,7 +615,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
#[test]
|
||||
fn test_sync_double_receive() {
|
||||
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
|
||||
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)
|
||||
});
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default()).expect("sync");
|
||||
wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(wallet.get_balance().unwrap(), 75_000, "incorrect balance");
|
||||
let target_addr = receiver_wallet.get_address($crate::wallet::AddressIndex::New).unwrap().address;
|
||||
|
||||
let tx1 = {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(target_addr.script_pubkey(), 49_000).enable_rbf();
|
||||
let (mut psbt, _details) = builder.finish().expect("building first tx");
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).expect("signing first tx");
|
||||
let (mut psbt, _details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
psbt.extract_tx()
|
||||
};
|
||||
@@ -645,17 +639,17 @@ macro_rules! bdk_blockchain_tests {
|
||||
let tx2 = {
|
||||
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));
|
||||
let (mut psbt, _details) = builder.finish().expect("building replacement tx");
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).expect("signing replacement tx");
|
||||
let (mut psbt, _details) = builder.finish().unwrap();
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
psbt.extract_tx()
|
||||
};
|
||||
|
||||
blockchain.broadcast(&tx1).expect("broadcasting first");
|
||||
blockchain.broadcast(&tx2).expect("broadcasting replacement");
|
||||
blockchain.broadcast(&tx1).unwrap();
|
||||
blockchain.broadcast(&tx2).unwrap();
|
||||
|
||||
receiver_wallet.sync(&blockchain, SyncOptions::default()).expect("syncing receiver");
|
||||
assert_eq!(receiver_wallet.get_balance().expect("balance"), 49_000, "should have received coins once and only once");
|
||||
receiver_wallet.sync(&blockchain, SyncOptions::default()).unwrap();
|
||||
assert_eq!(receiver_wallet.get_balance().unwrap(), 49_000, "should have received coins once and only once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -817,7 +811,7 @@ macro_rules! bdk_blockchain_tests {
|
||||
|
||||
let mut builder = wallet.build_fee_bump(details.txid).unwrap();
|
||||
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();
|
||||
assert!(finalized, "Cannot finalize transaction");
|
||||
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();
|
||||
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();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -15,37 +15,11 @@
|
||||
pub mod blockchain_tests;
|
||||
|
||||
use bitcoin::secp256k1::{Secp256k1, Verification};
|
||||
use bitcoin::{Address, PublicKey, Txid};
|
||||
use bitcoin::{Address, PublicKey};
|
||||
|
||||
use miniscript::descriptor::DescriptorPublicKey;
|
||||
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)]
|
||||
pub struct TestIncomingOutput {
|
||||
pub value: u64,
|
||||
@@ -63,7 +37,6 @@ impl TestIncomingOutput {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TestIncomingTx {
|
||||
pub input: Vec<TestIncomingInput>,
|
||||
pub output: Vec<TestIncomingOutput>,
|
||||
pub min_confirmations: Option<u64>,
|
||||
pub locktime: Option<i64>,
|
||||
@@ -72,14 +45,12 @@ pub struct TestIncomingTx {
|
||||
|
||||
impl TestIncomingTx {
|
||||
pub fn new(
|
||||
input: Vec<TestIncomingInput>,
|
||||
output: Vec<TestIncomingOutput>,
|
||||
min_confirmations: Option<u64>,
|
||||
locktime: Option<i64>,
|
||||
replaceable: Option<bool>,
|
||||
) -> Self {
|
||||
Self {
|
||||
input,
|
||||
output,
|
||||
min_confirmations,
|
||||
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) {
|
||||
self.output.push(output);
|
||||
}
|
||||
@@ -156,21 +123,16 @@ macro_rules! testutils {
|
||||
});
|
||||
( @e $descriptors:expr, $child:expr ) => ({ testutils!(@external $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 _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 min_confirmations = None::<u64>$(.or(Some($confirmations)))?;
|
||||
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 ) => ({
|
||||
|
||||
@@ -211,6 +211,15 @@ pub struct TransactionDetails {
|
||||
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
||||
/// transaction, unconfirmed transaction contains `None`.
|
||||
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
|
||||
|
||||
@@ -230,6 +230,7 @@ mod test {
|
||||
timestamp: 12345678,
|
||||
height: 5000,
|
||||
}),
|
||||
verified: true,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -157,29 +157,18 @@ impl fmt::Display for AddressInfo {
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// Options to a [`sync`].
|
||||
///
|
||||
/// [`sync`]: Wallet::sync
|
||||
/// Options to a [`Wallet::sync`]
|
||||
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>>,
|
||||
/// The maximum number of addresses sync on.
|
||||
pub max_addresses: Option<u32>,
|
||||
}
|
||||
|
||||
impl<D> Wallet<D>
|
||||
where
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
}
|
||||
|
||||
// Return a newly derived address for the specified `keychain`.
|
||||
fn get_new_address(&self, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
||||
let incremented_index = self.fetch_and_increment_index(keychain)?;
|
||||
// Return a newly derived address using the external descriptor
|
||||
fn get_new_address(&self) -> Result<AddressInfo, Error> {
|
||||
let incremented_index = self.fetch_and_increment_index(KeychainKind::External)?;
|
||||
|
||||
let address_result = self
|
||||
.get_descriptor_for_keychain(keychain)
|
||||
.descriptor
|
||||
.as_derived(incremented_index, &self.secp)
|
||||
.address(self.network);
|
||||
|
||||
@@ -250,14 +239,12 @@ where
|
||||
.map_err(|_| Error::ScriptDoesntHaveAddressForm)
|
||||
}
|
||||
|
||||
// Return the the last previously derived address for `keychain` if it has not been used in a
|
||||
// received transaction. Otherwise return a new address using [`Wallet::get_new_address`].
|
||||
fn get_unused_address(&self, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
||||
let current_index = self.fetch_index(keychain)?;
|
||||
// Return the the last previously derived address if it has not been used in a received
|
||||
// transaction. Otherwise return a new address using [`Wallet::get_new_address`].
|
||||
fn get_unused_address(&self) -> Result<AddressInfo, Error> {
|
||||
let current_index = self.fetch_index(KeychainKind::External)?;
|
||||
|
||||
let derived_key = self
|
||||
.get_descriptor_for_keychain(keychain)
|
||||
.as_derived(current_index, &self.secp);
|
||||
let derived_key = self.descriptor.as_derived(current_index, &self.secp);
|
||||
|
||||
let script_pubkey = derived_key.script_pubkey();
|
||||
|
||||
@@ -269,7 +256,7 @@ where
|
||||
.any(|o| o.script_pubkey == script_pubkey);
|
||||
|
||||
if found_used {
|
||||
self.get_new_address(keychain)
|
||||
self.get_new_address()
|
||||
} else {
|
||||
derived_key
|
||||
.address(self.network)
|
||||
@@ -281,21 +268,21 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
// Return derived address for the descriptor of given [`KeychainKind`] at a specific index
|
||||
fn peek_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
||||
self.get_descriptor_for_keychain(keychain)
|
||||
// Return derived address for the external descriptor at a specific index
|
||||
fn peek_address(&self, index: u32) -> Result<AddressInfo, Error> {
|
||||
self.descriptor
|
||||
.as_derived(index, &self.secp)
|
||||
.address(self.network)
|
||||
.map(|address| AddressInfo { index, address })
|
||||
.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
|
||||
fn reset_address(&self, index: u32, keychain: KeychainKind) -> Result<AddressInfo, Error> {
|
||||
self.set_index(keychain, index)?;
|
||||
fn reset_address(&self, index: u32) -> Result<AddressInfo, Error> {
|
||||
self.set_index(KeychainKind::External, index)?;
|
||||
|
||||
self.get_descriptor_for_keychain(keychain)
|
||||
self.descriptor
|
||||
.as_derived(index, &self.secp)
|
||||
.address(self.network)
|
||||
.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
|
||||
/// (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> {
|
||||
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 {
|
||||
AddressIndex::New => self.get_new_address(keychain),
|
||||
AddressIndex::LastUnused => self.get_unused_address(keychain),
|
||||
AddressIndex::Peek(index) => self.peek_address(index, keychain),
|
||||
AddressIndex::Reset(index) => self.reset_address(index, keychain),
|
||||
AddressIndex::New => self.get_new_address(),
|
||||
AddressIndex::LastUnused => self.get_unused_address(),
|
||||
AddressIndex::Peek(index) => self.peek_address(index),
|
||||
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)
|
||||
pub fn is_mine(&self, script: &Script) -> Result<bool, Error> {
|
||||
self.database.borrow().is_mine(script)
|
||||
@@ -723,10 +647,7 @@ where
|
||||
let mut drain_output = {
|
||||
let script_pubkey = match params.drain_to {
|
||||
Some(ref drain_recipient) => drain_recipient.clone(),
|
||||
None => self
|
||||
.get_internal_address(AddressIndex::New)?
|
||||
.address
|
||||
.script_pubkey(),
|
||||
None => self.get_change_address()?,
|
||||
};
|
||||
|
||||
TxOut {
|
||||
@@ -776,6 +697,7 @@ where
|
||||
received,
|
||||
sent,
|
||||
fee: Some(fee_amount),
|
||||
verified: true,
|
||||
};
|
||||
|
||||
Ok((psbt, transaction_details))
|
||||
@@ -1159,6 +1081,13 @@ where
|
||||
.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> {
|
||||
let (descriptor, keychain) = self._get_descriptor_for_keychain(keychain);
|
||||
let index = match descriptor.is_deriveable() {
|
||||
@@ -1522,10 +1451,46 @@ where
|
||||
) -> Result<(), Error> {
|
||||
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 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);
|
||||
// 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,))?;
|
||||
}
|
||||
|
||||
#[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 {
|
||||
block_time: BlockTime {
|
||||
height: maybe_await!(blockchain.get_height())?,
|
||||
@@ -3976,48 +3958,6 @@ pub(crate) mod test {
|
||||
builder.add_recipient(addr.script_pubkey(), 45_000);
|
||||
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
|
||||
|
||||
@@ -17,7 +17,7 @@ use std::fmt;
|
||||
use bitcoin::consensus::serialize;
|
||||
use bitcoin::{OutPoint, Transaction, Txid};
|
||||
|
||||
use crate::blockchain::GetTx;
|
||||
use crate::blockchain::Blockchain;
|
||||
use crate::database::Database;
|
||||
use crate::error::Error;
|
||||
|
||||
@@ -29,7 +29,7 @@ use crate::error::Error;
|
||||
/// Depending on the [capabilities](crate::blockchain::Blockchain::get_capabilities) of the
|
||||
/// [`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.
|
||||
pub fn verify_tx<D: Database, B: GetTx>(
|
||||
pub fn verify_tx<D: Database, B: Blockchain>(
|
||||
tx: &Transaction,
|
||||
database: &D,
|
||||
blockchain: &B,
|
||||
@@ -104,18 +104,43 @@ impl_error!(bitcoinconsensus::Error, Consensus, VerifyError);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::database::{BatchOperations, MemoryDatabase};
|
||||
use std::collections::HashSet;
|
||||
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::{Transaction, Txid};
|
||||
|
||||
use crate::blockchain::{Blockchain, Capability, Progress};
|
||||
use crate::database::{BatchDatabase, BatchOperations, MemoryDatabase};
|
||||
use crate::FeeRate;
|
||||
|
||||
use super::*;
|
||||
|
||||
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> {
|
||||
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]
|
||||
|
||||
Reference in New Issue
Block a user