Compare commits
35 Commits
v1.0.0-alp
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c256280eda | ||
|
|
c01983d02a | ||
|
|
fef70d5e8f | ||
|
|
c3544c9b8c | ||
|
|
b290b29502 | ||
|
|
d77a7f2ff1 | ||
|
|
3d44ffaef2 | ||
|
|
2efa299d04 | ||
|
|
2647aff4bc | ||
|
|
c151d8fd23 | ||
|
|
2c324d3759 | ||
|
|
50c549b5ac | ||
|
|
8379839010 | ||
|
|
5489f905a4 | ||
|
|
420e929463 | ||
|
|
13ab5a835d | ||
|
|
728e26f223 | ||
|
|
dbbd514242 | ||
|
|
ae00e1ee7b | ||
|
|
adc95137ac | ||
|
|
022d5a21cf | ||
|
|
7aca88474a | ||
|
|
b3278a4c29 | ||
|
|
552f11cb5f | ||
|
|
d8f74dc5e4 | ||
|
|
8d93fad778 | ||
|
|
9bb39a3a3f | ||
|
|
9e098a5b6d | ||
|
|
c6b9ed3b76 | ||
|
|
1c15cb2f91 | ||
|
|
89a7ddca7f | ||
|
|
097d818d4c | ||
|
|
f11d663b7e | ||
|
|
4679ca1df7 | ||
|
|
64a90192d9 |
2
.github/workflows/audit.yml
vendored
2
.github/workflows/audit.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
security_audit:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rs/audit-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
2
.github/workflows/code_coverage.yml
vendored
2
.github/workflows/code_coverage.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Install lcov tools
|
||||
run: sudo apt-get install lcov -y
|
||||
- name: Install Rust toolchain
|
||||
|
||||
11
.github/workflows/cont_integration.yml
vendored
11
.github/workflows/cont_integration.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
||||
- --all-features
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -32,7 +32,6 @@ jobs:
|
||||
run: |
|
||||
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
|
||||
cargo update -p time --precise "0.3.20"
|
||||
cargo update -p jobserver --precise "0.1.26"
|
||||
cargo update -p home --precise "0.5.5"
|
||||
- name: Build
|
||||
run: cargo build ${{ matrix.features }}
|
||||
@@ -44,7 +43,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -75,7 +74,7 @@ jobs:
|
||||
CFLAGS: -I/usr/include
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
# Install a recent version of clang that supports wasm32
|
||||
- run: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - || exit 1
|
||||
- run: sudo apt-get update || exit 1
|
||||
@@ -101,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -115,7 +114,7 @@ jobs:
|
||||
clippy_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
4
.github/workflows/nightly_docs.yml
vendored
4
.github/workflows/nightly_docs.yml
vendored
@@ -8,7 +8,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2022-12-14
|
||||
- name: Set profile
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout `bitcoindevkit.org`
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ssh-key: ${{ secrets.DOCS_PUSH_SSH_KEY }}
|
||||
repository: bitcoindevkit/bitcoindevkit.org
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "1.0.0-alpha.5"
|
||||
version = "1.0.0-alpha.7"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk"
|
||||
description = "A modern, lightweight, descriptor-based wallet library"
|
||||
@@ -18,7 +18,7 @@ miniscript = { version = "10.0.0", features = ["serde"], default-features = fals
|
||||
bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.9.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", features = ["miniscript", "serde"], default-features = false }
|
||||
|
||||
# Optional dependencies
|
||||
bip39 = { version = "2.0", optional = true }
|
||||
|
||||
@@ -35,24 +35,16 @@ pub trait PsbtUtils {
|
||||
}
|
||||
|
||||
impl PsbtUtils for Psbt {
|
||||
#[allow(clippy::all)] // We want to allow `manual_map` but it is too new.
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||
let tx = &self.unsigned_tx;
|
||||
let input = self.inputs.get(input_index)?;
|
||||
|
||||
if input_index >= tx.input.len() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Some(input) = self.inputs.get(input_index) {
|
||||
if let Some(wit_utxo) = &input.witness_utxo {
|
||||
Some(wit_utxo.clone())
|
||||
} else if let Some(in_tx) = &input.non_witness_utxo {
|
||||
Some(in_tx.output[tx.input[input_index].previous_output.vout as usize].clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
match (&input.witness_utxo, &input.non_witness_utxo) {
|
||||
(Some(_), _) => input.witness_utxo.clone(),
|
||||
(_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| {
|
||||
in_tx.output[tx.input[input_index].previous_output.vout as usize].clone()
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ use core::convert::AsRef;
|
||||
use core::ops::Sub;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, TxOut};
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||
use bitcoin::{psbt, Weight};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -197,6 +197,8 @@ pub enum Utxo {
|
||||
Foreign {
|
||||
/// The location of the output.
|
||||
outpoint: OutPoint,
|
||||
/// The nSequence value to set for this input.
|
||||
sequence: Option<Sequence>,
|
||||
/// The information about the input we require to add it to a PSBT.
|
||||
// Box it to stop the type being too big.
|
||||
psbt_input: Box<psbt::Input>,
|
||||
@@ -219,6 +221,7 @@ impl Utxo {
|
||||
Utxo::Foreign {
|
||||
outpoint,
|
||||
psbt_input,
|
||||
..
|
||||
} => {
|
||||
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
return &prev_tx.output[outpoint.vout as usize];
|
||||
@@ -232,6 +235,14 @@ impl Utxo {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the sequence number if an explicit sequence number has to be set for this input.
|
||||
pub fn sequence(&self) -> Option<Sequence> {
|
||||
match self {
|
||||
Utxo::Local(_) => None,
|
||||
Utxo::Foreign { sequence, .. } => *sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
//! Wallet
|
||||
//!
|
||||
//! This module defines the [`Wallet`].
|
||||
use crate::collections::{BTreeMap, HashMap, HashSet};
|
||||
use crate::collections::{BTreeMap, HashMap};
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
string::{String, ToString},
|
||||
@@ -1347,7 +1347,7 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
Some(tx_builder::Version(x)) => x,
|
||||
None if requirements.csv.is_some() => 2,
|
||||
_ => 1,
|
||||
None => 1,
|
||||
};
|
||||
|
||||
// We use a match here instead of a unwrap_or_else as it's way more readable :)
|
||||
@@ -1400,6 +1400,7 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
};
|
||||
|
||||
// The nSequence to be by default for inputs unless an explicit sequence is specified.
|
||||
let n_sequence = match (params.rbf, requirements.csv) {
|
||||
// No RBF or CSV but there's an nLockTime, so the nSequence cannot be final
|
||||
(None, None) if lock_time != absolute::LockTime::ZERO => {
|
||||
@@ -1518,15 +1519,8 @@ impl<D> Wallet<D> {
|
||||
return Err(CreateTxError::ChangePolicyDescriptor);
|
||||
}
|
||||
|
||||
let (required_utxos, optional_utxos) = self.preselect_utxos(
|
||||
params.change_policy,
|
||||
¶ms.unspendable,
|
||||
params.utxos.clone(),
|
||||
params.drain_wallet,
|
||||
params.manually_selected_only,
|
||||
params.bumping_fee.is_some(), // we mandate confirmed transactions if we're bumping the fee
|
||||
Some(current_height.to_consensus_u32()),
|
||||
);
|
||||
let (required_utxos, optional_utxos) =
|
||||
self.preselect_utxos(¶ms, Some(current_height.to_consensus_u32()));
|
||||
|
||||
// get drain script
|
||||
let drain_script = match params.drain_to {
|
||||
@@ -1565,7 +1559,7 @@ impl<D> Wallet<D> {
|
||||
.map(|u| bitcoin::TxIn {
|
||||
previous_output: u.outpoint(),
|
||||
script_sig: ScriptBuf::default(),
|
||||
sequence: n_sequence,
|
||||
sequence: u.sequence().unwrap_or(n_sequence),
|
||||
witness: Witness::new(),
|
||||
})
|
||||
.collect();
|
||||
@@ -1745,6 +1739,7 @@ impl<D> Wallet<D> {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Foreign {
|
||||
outpoint: txin.previous_output,
|
||||
sequence: Some(txin.sequence),
|
||||
psbt_input: Box::new(psbt::Input {
|
||||
witness_utxo: Some(txout.clone()),
|
||||
non_witness_utxo: Some(prev_tx.clone()),
|
||||
@@ -2063,17 +2058,26 @@ impl<D> Wallet<D> {
|
||||
|
||||
/// Given the options returns the list of utxos that must be used to form the
|
||||
/// transaction and any further that may be used if needed.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn preselect_utxos(
|
||||
&self,
|
||||
change_policy: tx_builder::ChangeSpendPolicy,
|
||||
unspendable: &HashSet<OutPoint>,
|
||||
manually_selected: Vec<WeightedUtxo>,
|
||||
must_use_all_available: bool,
|
||||
manual_only: bool,
|
||||
must_only_use_confirmed_tx: bool,
|
||||
params: &TxParams,
|
||||
current_height: Option<u32>,
|
||||
) -> (Vec<WeightedUtxo>, Vec<WeightedUtxo>) {
|
||||
let TxParams {
|
||||
change_policy,
|
||||
unspendable,
|
||||
utxos,
|
||||
drain_wallet,
|
||||
manually_selected_only,
|
||||
bumping_fee,
|
||||
..
|
||||
} = params;
|
||||
|
||||
let manually_selected = utxos.clone();
|
||||
// we mandate confirmed transactions if we're bumping the fee
|
||||
let must_only_use_confirmed_tx = bumping_fee.is_some();
|
||||
let must_use_all_available = *drain_wallet;
|
||||
|
||||
let chain_tip = self.chain.tip().block_id();
|
||||
// must_spend <- manually selected utxos
|
||||
// may_spend <- all other available utxos
|
||||
@@ -2088,7 +2092,7 @@ impl<D> Wallet<D> {
|
||||
|
||||
// NOTE: we are intentionally ignoring `unspendable` here. i.e manual
|
||||
// selection overrides unspendable.
|
||||
if manual_only {
|
||||
if *manually_selected_only {
|
||||
return (must_spend, vec![]);
|
||||
}
|
||||
|
||||
@@ -2216,8 +2220,9 @@ impl<D> Wallet<D> {
|
||||
}
|
||||
}
|
||||
Utxo::Foreign {
|
||||
psbt_input: foreign_psbt_input,
|
||||
outpoint,
|
||||
psbt_input: foreign_psbt_input,
|
||||
..
|
||||
} => {
|
||||
let is_taproot = foreign_psbt_input
|
||||
.witness_utxo
|
||||
@@ -2290,9 +2295,6 @@ impl<D> Wallet<D> {
|
||||
) -> Result<(), MiniscriptPsbtError> {
|
||||
// We need to borrow `psbt` mutably within the loops, so we have to allocate a vec for all
|
||||
// the input utxos and outputs
|
||||
//
|
||||
// Clippy complains that the collect is not required, but that's wrong
|
||||
#[allow(clippy::needless_collect)]
|
||||
let utxos = (0..psbt.inputs.len())
|
||||
.filter_map(|i| psbt.get_utxo_for(i).map(|utxo| (true, i, utxo)))
|
||||
.chain(
|
||||
@@ -2474,7 +2476,7 @@ impl<D> Wallet<D> {
|
||||
/// This method takes in an iterator of `(tx, last_seen)` where `last_seen` is the timestamp of
|
||||
/// when the transaction was last seen in the mempool. This is used for conflict resolution
|
||||
/// when there is conflicting unconfirmed transactions. The transaction with the later
|
||||
/// `last_seen` is prioritied.
|
||||
/// `last_seen` is prioritized.
|
||||
pub fn apply_unconfirmed_txs<'t>(
|
||||
&mut self,
|
||||
unconfirmed_txs: impl IntoIterator<Item = (&'t Transaction, u64)>,
|
||||
|
||||
@@ -820,7 +820,6 @@ pub enum TapLeavesOptions {
|
||||
None,
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for SignOptions {
|
||||
fn default() -> Self {
|
||||
SignOptions {
|
||||
|
||||
@@ -190,7 +190,7 @@ impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
}
|
||||
|
||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D, Cs, Ctx> {
|
||||
impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Set a custom fee rate
|
||||
/// The fee_rate method sets the mining fee paid by the transaction as a rate on its size.
|
||||
/// This means that the total fee paid is equal to this rate * size of the transaction in virtual Bytes (vB) or Weight Unit (wu).
|
||||
@@ -389,6 +389,22 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
self.add_foreign_utxo_with_sequence(
|
||||
outpoint,
|
||||
psbt_input,
|
||||
satisfaction_weight,
|
||||
Sequence::MAX,
|
||||
)
|
||||
}
|
||||
|
||||
/// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value.
|
||||
pub fn add_foreign_utxo_with_sequence(
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
sequence: Sequence,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
if psbt_input.witness_utxo.is_none() {
|
||||
match psbt_input.non_witness_utxo.as_ref() {
|
||||
@@ -413,6 +429,7 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Foreign {
|
||||
outpoint,
|
||||
sequence: Some(sequence),
|
||||
psbt_input: Box::new(psbt_input),
|
||||
},
|
||||
});
|
||||
@@ -557,20 +574,6 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
|
||||
/// Enable signaling RBF
|
||||
///
|
||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||
@@ -617,6 +620,22 @@ impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx: TxBuilderContext> TxBuilder<'a, D,
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
|
||||
pub enum AddUtxoError {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_bitcoind_rpc"
|
||||
version = "0.4.0"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -16,7 +16,7 @@ readme = "README.md"
|
||||
# For no-std, remember to enable the bitcoin/no-std feature
|
||||
bitcoin = { version = "0.30", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.17" }
|
||||
bdk_chain = { path = "../chain", version = "0.9", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.11", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bitcoind = { version = "0.33", features = ["25_0"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.9.0"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -18,7 +18,6 @@ bitcoin = { version = "0.30.0", default-features = false }
|
||||
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
|
||||
|
||||
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
|
||||
# note versions > 0.9.1 breaks ours 1.57.0 MSRV.
|
||||
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
|
||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY};
|
||||
pub enum ChainPosition<A> {
|
||||
/// The chain data is seen as confirmed, and in anchored by `A`.
|
||||
Confirmed(A),
|
||||
/// The chain data is seen in mempool at this given timestamp.
|
||||
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
|
||||
Unconfirmed(u64),
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@ impl<A: Anchor> ChainPosition<A> {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub enum ConfirmationTime {
|
||||
/// The confirmed variant.
|
||||
/// The transaction is confirmed
|
||||
Confirmed {
|
||||
/// Confirmation height.
|
||||
height: u32,
|
||||
/// Confirmation time in unix seconds.
|
||||
time: u64,
|
||||
},
|
||||
/// The unconfirmed variant.
|
||||
/// The transaction is unconfirmed
|
||||
Unconfirmed {
|
||||
/// The last-seen timestamp in unix seconds.
|
||||
last_seen: u64,
|
||||
@@ -157,13 +157,12 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationHeightAnchor {
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
|
||||
/// The exact confirmation height of the transaction.
|
||||
///
|
||||
/// It is assumed that this value is never larger than the height of the anchor block.
|
||||
pub confirmation_height: u32,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationHeightAnchor {
|
||||
@@ -198,12 +197,12 @@ impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationTimeHeightAnchor {
|
||||
/// The confirmation height of the transaction being anchored.
|
||||
pub confirmation_height: u32,
|
||||
/// The confirmation time of the transaction being anchored.
|
||||
pub confirmation_time: u64,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
/// The confirmation height of the chain data being anchored.
|
||||
pub confirmation_height: u32,
|
||||
/// The confirmation time of the chain data being anchored.
|
||||
pub confirmation_time: u64,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationTimeHeightAnchor {
|
||||
@@ -229,12 +228,12 @@ impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
|
||||
/// A `TxOut` with as much data as we can retrieve about it
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FullTxOut<A> {
|
||||
/// The position of the transaction in `outpoint` in the overall chain.
|
||||
pub chain_position: ChainPosition<A>,
|
||||
/// The location of the `TxOut`.
|
||||
pub outpoint: OutPoint,
|
||||
/// The `TxOut`.
|
||||
pub txout: TxOut,
|
||||
/// The position of the transaction in `outpoint` in the overall chain.
|
||||
pub chain_position: ChainPosition<A>,
|
||||
/// The txid and chain position of the transaction (if any) that has spent this output.
|
||||
pub spent_by: Option<(ChainPosition<A>, Txid)>,
|
||||
/// Whether this output is on a coinbase transaction.
|
||||
@@ -299,3 +298,35 @@ impl<A: Anchor> FullTxOut<A> {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn chain_position_ord() {
|
||||
let unconf1 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(10);
|
||||
let unconf2 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(20);
|
||||
let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 9,
|
||||
anchor_block: BlockId {
|
||||
height: 20,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 12,
|
||||
anchor_block: BlockId {
|
||||
height: 15,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
assert!(unconf2 > unconf1, "higher last_seen means higher ord");
|
||||
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
|
||||
assert!(
|
||||
conf2 > conf1,
|
||||
"confirmation_height is higher then it should be higher ord"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,12 +326,17 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
self.lookahead
|
||||
}
|
||||
|
||||
/// Store lookahead scripts until `target_index`.
|
||||
/// Store lookahead scripts until `target_index` (inclusive).
|
||||
///
|
||||
/// This does not change the `lookahead` setting.
|
||||
/// This does not change the global `lookahead` setting.
|
||||
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
|
||||
let next_index = self.next_store_index(keychain);
|
||||
if let Some(temp_lookahead) = target_index.checked_sub(next_index).filter(|&v| v > 0) {
|
||||
let (next_index, _) = self.next_index(keychain);
|
||||
|
||||
let temp_lookahead = (target_index + 1)
|
||||
.checked_sub(next_index)
|
||||
.filter(|&index| index > 0);
|
||||
|
||||
if let Some(temp_lookahead) = temp_lookahead {
|
||||
self.replenish_lookahead(keychain, temp_lookahead);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ where
|
||||
/// Stages a new changeset and commits it (along with any other previously staged changes) to
|
||||
/// the persistence backend
|
||||
///
|
||||
/// Convience method for calling [`stage`] and then [`commit`].
|
||||
/// Convenience method for calling [`stage`] and then [`commit`].
|
||||
///
|
||||
/// [`stage`]: Self::stage
|
||||
/// [`commit`]: Self::commit
|
||||
|
||||
@@ -40,20 +40,23 @@
|
||||
//! # use bdk_chain::example_utils::*;
|
||||
//! # use bitcoin::Transaction;
|
||||
//! # let tx_a = tx_from_hex(RAW_TX_1);
|
||||
//! let mut graph: TxGraph = TxGraph::default();
|
||||
//! let mut another_graph: TxGraph = TxGraph::default();
|
||||
//! let mut tx_graph: TxGraph = TxGraph::default();
|
||||
//!
|
||||
//! // insert a transaction
|
||||
//! let changeset = graph.insert_tx(tx_a);
|
||||
//! let changeset = tx_graph.insert_tx(tx_a);
|
||||
//!
|
||||
//! // the resulting changeset can be applied to another tx graph
|
||||
//! another_graph.apply_changeset(changeset);
|
||||
//! // We can restore the state of the `tx_graph` by applying all
|
||||
//! // the changesets obtained by mutating the original (the order doesn't matter).
|
||||
//! let mut restored_tx_graph: TxGraph = TxGraph::default();
|
||||
//! restored_tx_graph.apply_changeset(changeset);
|
||||
//!
|
||||
//! assert_eq!(tx_graph, restored_tx_graph);
|
||||
//! ```
|
||||
//!
|
||||
//! A [`TxGraph`] can also be updated with another [`TxGraph`].
|
||||
//! A [`TxGraph`] can also be updated with another [`TxGraph`] which merges them together.
|
||||
//!
|
||||
//! ```
|
||||
//! # use bdk_chain::BlockId;
|
||||
//! # use bdk_chain::{Append, BlockId};
|
||||
//! # use bdk_chain::tx_graph::TxGraph;
|
||||
//! # use bdk_chain::example_utils::*;
|
||||
//! # use bitcoin::Transaction;
|
||||
@@ -451,6 +454,21 @@ impl<A> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// Transform the [`TxGraph`] to have [`Anchor`]s of another type.
|
||||
///
|
||||
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
|
||||
/// transform it.
|
||||
pub fn map_anchors<A2: Clone + Ord, F>(self, f: F) -> TxGraph<A2>
|
||||
where
|
||||
F: FnMut(A) -> A2,
|
||||
{
|
||||
let mut new_graph = TxGraph::<A2>::default();
|
||||
new_graph.apply_changeset(self.initial_changeset().map_anchors(f));
|
||||
new_graph
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// Construct a new [`TxGraph`] from a list of transactions.
|
||||
pub fn new(txs: impl IntoIterator<Item = Transaction>) -> Self {
|
||||
@@ -1212,11 +1230,6 @@ impl<A> Default for ChangeSet<A> {
|
||||
}
|
||||
|
||||
impl<A> ChangeSet<A> {
|
||||
/// Returns true if the [`ChangeSet`] is empty (no transactions or txouts).
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.txs.is_empty() && self.txouts.is_empty()
|
||||
}
|
||||
|
||||
/// Iterates over all outpoints contained within [`ChangeSet`].
|
||||
pub fn txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||
self.txs
|
||||
@@ -1296,6 +1309,26 @@ impl<A: Ord> Append for ChangeSet<A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Ord> ChangeSet<A> {
|
||||
/// Transform the [`ChangeSet`] to have [`Anchor`]s of another type.
|
||||
///
|
||||
/// This takes in a closure of signature `FnMut(A) -> A2` which is called for each [`Anchor`] to
|
||||
/// transform it.
|
||||
pub fn map_anchors<A2: Ord, F>(self, mut f: F) -> ChangeSet<A2>
|
||||
where
|
||||
F: FnMut(A) -> A2,
|
||||
{
|
||||
ChangeSet {
|
||||
txs: self.txs,
|
||||
txouts: self.txouts,
|
||||
anchors: BTreeSet::<(A2, Txid)>::from_iter(
|
||||
self.anchors.into_iter().map(|(a, txid)| (f(a), txid)),
|
||||
),
|
||||
last_seen: self.last_seen,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> AsRef<TxGraph<A>> for TxGraph<A> {
|
||||
fn as_ref(&self) -> &TxGraph<A> {
|
||||
self
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bdk_chain::{tx_graph::TxGraph, BlockId, SpkTxOutIndex};
|
||||
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
|
||||
use bitcoin::{
|
||||
locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction,
|
||||
TxIn, TxOut, Txid, Witness,
|
||||
@@ -49,11 +49,11 @@ impl TxOutTemplate {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn init_graph<'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, BlockId>>,
|
||||
) -> (TxGraph<BlockId>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
|
||||
) -> (TxGraph<A>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let mut graph = TxGraph::<BlockId>::default();
|
||||
let mut graph = TxGraph::<A>::default();
|
||||
let mut spk_index = SpkTxOutIndex::default();
|
||||
(0..10).for_each(|index| {
|
||||
spk_index.insert_spk(
|
||||
@@ -126,7 +126,7 @@ pub fn init_graph<'a>(
|
||||
spk_index.scan(&tx);
|
||||
let _ = graph.insert_tx(tx.clone());
|
||||
for anchor in tx_tmp.anchors.iter() {
|
||||
let _ = graph.insert_anchor(tx.txid(), *anchor);
|
||||
let _ = graph.insert_anchor(tx.txid(), anchor.clone());
|
||||
}
|
||||
if let Some(seen_at) = tx_tmp.last_seen {
|
||||
let _ = graph.insert_seen_at(tx.txid(), seen_at);
|
||||
|
||||
@@ -386,3 +386,103 @@ fn test_non_wildcard_derivations() {
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check that calling `lookahead_to_target` stores the expected spks.
|
||||
#[test]
|
||||
fn lookahead_to_target() {
|
||||
#[derive(Default)]
|
||||
struct TestCase {
|
||||
/// Global lookahead value.
|
||||
lookahead: u32,
|
||||
/// Last revealed index for external keychain.
|
||||
external_last_revealed: Option<u32>,
|
||||
/// Last revealed index for internal keychain.
|
||||
internal_last_revealed: Option<u32>,
|
||||
/// Call `lookahead_to_target(External, u32)`.
|
||||
external_target: Option<u32>,
|
||||
/// Call `lookahead_to_target(Internal, u32)`.
|
||||
internal_target: Option<u32>,
|
||||
}
|
||||
|
||||
let test_cases = &[
|
||||
TestCase {
|
||||
lookahead: 0,
|
||||
external_target: Some(100),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 10,
|
||||
internal_target: Some(99),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 100,
|
||||
internal_target: Some(9),
|
||||
external_target: Some(10),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 12,
|
||||
external_last_revealed: Some(2),
|
||||
internal_last_revealed: Some(2),
|
||||
internal_target: Some(15),
|
||||
external_target: Some(13),
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 13,
|
||||
external_last_revealed: Some(100),
|
||||
internal_last_revealed: Some(21),
|
||||
internal_target: Some(120),
|
||||
external_target: Some(130),
|
||||
},
|
||||
];
|
||||
|
||||
for t in test_cases {
|
||||
let (mut index, _, _) = init_txout_index(t.lookahead);
|
||||
|
||||
if let Some(last_revealed) = t.external_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
|
||||
}
|
||||
if let Some(last_revealed) = t.internal_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::Internal, last_revealed);
|
||||
}
|
||||
|
||||
let keychain_test_cases = [
|
||||
(
|
||||
TestKeychain::External,
|
||||
t.external_last_revealed,
|
||||
t.external_target,
|
||||
),
|
||||
(
|
||||
TestKeychain::Internal,
|
||||
t.internal_last_revealed,
|
||||
t.internal_target,
|
||||
),
|
||||
];
|
||||
for (keychain, last_revealed, target) in keychain_test_cases {
|
||||
if let Some(target) = target {
|
||||
let original_last_stored_index = match last_revealed {
|
||||
Some(last_revealed) => Some(last_revealed + t.lookahead),
|
||||
None => t.lookahead.checked_sub(1),
|
||||
};
|
||||
let exp_last_stored_index = match original_last_stored_index {
|
||||
Some(original_last_stored_index) => {
|
||||
Ord::max(target, original_last_stored_index)
|
||||
}
|
||||
None => target,
|
||||
};
|
||||
index.lookahead_to_target(&keychain, target);
|
||||
let keys = index
|
||||
.inner()
|
||||
.all_spks()
|
||||
.range((keychain.clone(), 0)..=(keychain.clone(), u32::MAX))
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let exp_keys = core::iter::repeat(keychain)
|
||||
.zip(0_u32..=exp_last_stored_index)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(keys, exp_keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ use bdk_chain::{
|
||||
use bitcoin::{
|
||||
absolute, hashes::Hash, BlockHash, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid,
|
||||
};
|
||||
use common::*;
|
||||
use core::iter;
|
||||
use rand::RngCore;
|
||||
use std::vec;
|
||||
|
||||
#[test]
|
||||
@@ -213,7 +215,8 @@ fn insert_tx_graph_doesnt_count_coinbase_as_spent() {
|
||||
};
|
||||
|
||||
let mut graph = TxGraph::<()>::default();
|
||||
let _ = graph.insert_tx(tx);
|
||||
let changeset = graph.insert_tx(tx);
|
||||
assert!(!changeset.is_empty());
|
||||
assert!(graph.outspends(OutPoint::null()).is_empty());
|
||||
assert!(graph.tx_spends(Txid::all_zeros()).next().is_none());
|
||||
}
|
||||
@@ -289,7 +292,7 @@ fn insert_tx_displaces_txouts() {
|
||||
}],
|
||||
};
|
||||
|
||||
let _ = tx_graph.insert_txout(
|
||||
let changeset = tx_graph.insert_txout(
|
||||
OutPoint {
|
||||
txid: tx.txid(),
|
||||
vout: 0,
|
||||
@@ -300,6 +303,8 @@ fn insert_tx_displaces_txouts() {
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!changeset.is_empty());
|
||||
|
||||
let _ = tx_graph.insert_txout(
|
||||
OutPoint {
|
||||
txid: tx.txid(),
|
||||
@@ -653,7 +658,8 @@ fn test_walk_ancestors() {
|
||||
]);
|
||||
|
||||
[&tx_a0, &tx_b1].iter().for_each(|&tx| {
|
||||
let _ = graph.insert_anchor(tx.txid(), tip.block_id());
|
||||
let changeset = graph.insert_anchor(tx.txid(), tip.block_id());
|
||||
assert!(!changeset.is_empty());
|
||||
});
|
||||
|
||||
let ancestors = [
|
||||
@@ -1027,10 +1033,12 @@ fn test_changeset_last_seen_append() {
|
||||
last_seen: original_ls.map(|ls| (txid, ls)).into_iter().collect(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!original.is_empty() || original_ls.is_none());
|
||||
let update = ChangeSet::<()> {
|
||||
last_seen: update_ls.map(|ls| (txid, ls)).into_iter().collect(),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!update.is_empty() || update_ls.is_none());
|
||||
|
||||
original.append(update);
|
||||
assert_eq!(
|
||||
@@ -1172,3 +1180,86 @@ fn test_missing_blocks() {
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// The `map_anchors` allow a caller to pass a function to reconstruct the [`TxGraph`] with any [`Anchor`],
|
||||
/// even though the function is non-deterministic.
|
||||
fn call_map_anchors_with_non_deterministic_anchor() {
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
/// A non-deterministic anchor
|
||||
pub struct NonDeterministicAnchor {
|
||||
pub anchor_block: BlockId,
|
||||
pub non_deterministic_field: u32,
|
||||
}
|
||||
|
||||
let template = [
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(1))],
|
||||
anchors: &[block_id!(1, "A")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
anchors: &[block_id!(2, "B")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx3",
|
||||
inputs: &[TxInTemplate::PrevTx("tx2", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
anchors: &[block_id!(3, "C"), block_id!(4, "D")],
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let (graph, _, _) = init_graph(&template);
|
||||
let new_graph = graph.clone().map_anchors(|a| NonDeterministicAnchor {
|
||||
anchor_block: a,
|
||||
// A non-deterministic value
|
||||
non_deterministic_field: rand::thread_rng().next_u32(),
|
||||
});
|
||||
|
||||
// Check all the details in new_graph reconstruct as well
|
||||
|
||||
let mut full_txs_vec: Vec<_> = graph.full_txs().collect();
|
||||
full_txs_vec.sort();
|
||||
let mut new_txs_vec: Vec<_> = new_graph.full_txs().collect();
|
||||
new_txs_vec.sort();
|
||||
let mut new_txs = new_txs_vec.iter();
|
||||
|
||||
for tx_node in full_txs_vec.iter() {
|
||||
let new_txnode = new_txs.next().unwrap();
|
||||
assert_eq!(new_txnode.txid, tx_node.txid);
|
||||
assert_eq!(new_txnode.tx, tx_node.tx);
|
||||
assert_eq!(
|
||||
new_txnode.last_seen_unconfirmed,
|
||||
tx_node.last_seen_unconfirmed
|
||||
);
|
||||
assert_eq!(new_txnode.anchors.len(), tx_node.anchors.len());
|
||||
|
||||
let mut new_anchors: Vec<_> = new_txnode.anchors.iter().map(|a| a.anchor_block).collect();
|
||||
new_anchors.sort();
|
||||
let mut old_anchors: Vec<_> = tx_node.anchors.iter().copied().collect();
|
||||
old_anchors.sort();
|
||||
assert_eq!(new_anchors, old_anchors);
|
||||
}
|
||||
assert!(new_txs.next().is_none());
|
||||
|
||||
let new_graph_anchors: Vec<_> = new_graph
|
||||
.all_anchors()
|
||||
.iter()
|
||||
.map(|i| i.0.anchor_block)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
new_graph_anchors,
|
||||
vec![
|
||||
block_id!(1, "A"),
|
||||
block_id!(2, "B"),
|
||||
block_id!(3, "C"),
|
||||
block_id!(4, "D"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_electrum"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,6 +12,6 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.9.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
|
||||
electrum-client = { version = "0.18" }
|
||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -12,7 +12,7 @@ readme = "README.md"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.9.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
|
||||
esplora-client = { version = "0.6.0", default-features = false }
|
||||
async-trait = { version = "0.1.66", optional = true }
|
||||
futures = { version = "0.3.26", optional = true }
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
use async_trait::async_trait;
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
collections::BTreeMap,
|
||||
local_chain::{self, CheckPoint},
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use esplora_client::{Error, TxStatus};
|
||||
use esplora_client::TxStatus;
|
||||
use futures::{stream::FuturesOrdered, TryStreamExt};
|
||||
|
||||
use crate::anchor_from_status;
|
||||
|
||||
/// [`esplora_client::Error`]
|
||||
type Error = Box<esplora_client::Error>;
|
||||
|
||||
/// Trait to extend the functionality of [`esplora_client::AsyncClient`].
|
||||
///
|
||||
/// Refer to [crate-level documentation] for more.
|
||||
@@ -35,7 +38,6 @@ pub trait EsploraAsyncExt {
|
||||
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
|
||||
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
|
||||
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
@@ -50,7 +52,6 @@ pub trait EsploraAsyncExt {
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn full_scan<K: Ord + Clone + Send>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<
|
||||
@@ -73,7 +74,6 @@ pub trait EsploraAsyncExt {
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: EsploraAsyncExt::full_scan
|
||||
#[allow(clippy::result_large_err)]
|
||||
async fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
@@ -204,6 +204,24 @@ impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: prevout.value,
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,17 @@ use std::thread::JoinHandle;
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::collections::BTreeMap;
|
||||
use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, Txid},
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
local_chain::{self, CheckPoint},
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use esplora_client::{Error, TxStatus};
|
||||
use esplora_client::TxStatus;
|
||||
|
||||
use crate::anchor_from_status;
|
||||
|
||||
/// [`esplora_client::Error`]
|
||||
type Error = Box<esplora_client::Error>;
|
||||
|
||||
/// Trait to extend the functionality of [`esplora_client::BlockingClient`].
|
||||
///
|
||||
/// Refer to [crate-level documentation] for more.
|
||||
@@ -33,7 +36,6 @@ pub trait EsploraExt {
|
||||
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
|
||||
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
|
||||
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
@@ -48,7 +50,6 @@ pub trait EsploraExt {
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
@@ -68,7 +69,6 @@ pub trait EsploraExt {
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: EsploraExt::full_scan
|
||||
#[allow(clippy::result_large_err)]
|
||||
fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
@@ -194,6 +194,24 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: prevout.value,
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +265,12 @@ impl EsploraExt for esplora_client::BlockingClient {
|
||||
.map(|txid| {
|
||||
std::thread::spawn({
|
||||
let client = self.clone();
|
||||
move || client.get_tx_status(&txid).map(|s| (txid, s))
|
||||
move || {
|
||||
client
|
||||
.get_tx_status(&txid)
|
||||
.map_err(Box::new)
|
||||
.map(|s| (txid, s))
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<JoinHandle<Result<(Txid, TxStatus), Error>>>>();
|
||||
|
||||
@@ -109,6 +109,28 @@ pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transactions' previous outputs.
|
||||
let fee = graph_update.calculate_fee(tx.tx).expect("Fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_sat() as u64;
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
|
||||
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
graph_update_txids.sort();
|
||||
let mut expected_txids = vec![txid1, txid2];
|
||||
|
||||
@@ -136,6 +136,28 @@ pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
1,
|
||||
)?;
|
||||
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transactions' previous outputs.
|
||||
let fee = graph_update.calculate_fee(tx.tx).expect("Fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_sat() as u64;
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
|
||||
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
graph_update_txids.sort();
|
||||
let mut expected_txids = vec![txid1, txid2];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_file_store"
|
||||
version = "0.5.0"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -11,7 +11,7 @@ authors = ["Bitcoin Dev Kit Developers"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.9.0", features = [ "serde", "miniscript" ] }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", features = [ "serde", "miniscript" ] }
|
||||
bincode = { version = "1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -110,9 +110,13 @@ enum RpcCommands {
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let start = Instant::now();
|
||||
|
||||
let (args, keymap, index, db, init_changeset) =
|
||||
example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
println!(
|
||||
"[{:>10}s] loaded initial changeset from db",
|
||||
start.elapsed().as_secs_f32()
|
||||
|
||||
@@ -53,7 +53,6 @@ pub struct Args<CS: clap::Subcommand, S: clap::Args> {
|
||||
pub command: Commands<CS, S>,
|
||||
}
|
||||
|
||||
#[allow(clippy::almost_swapped)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
|
||||
#[clap(flatten)]
|
||||
@@ -137,7 +136,6 @@ impl core::fmt::Display for CoinSelectionAlgo {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::almost_swapped)]
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum AddressCmd {
|
||||
/// Get the next unused address.
|
||||
@@ -190,7 +188,12 @@ impl core::fmt::Display for Keychain {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct CreateTxChange {
|
||||
pub index_changeset: keychain::ChangeSet<Keychain>,
|
||||
pub change_keychain: Keychain,
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||
graph: &mut KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
@@ -198,10 +201,7 @@ pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||
cs_algorithm: CoinSelectionAlgo,
|
||||
address: Address,
|
||||
value: u64,
|
||||
) -> anyhow::Result<(
|
||||
Transaction,
|
||||
Option<(keychain::ChangeSet<Keychain>, (Keychain, u32))>,
|
||||
)>
|
||||
) -> anyhow::Result<(Transaction, Option<CreateTxChange>)>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
@@ -393,7 +393,11 @@ where
|
||||
}
|
||||
|
||||
let change_info = if selection_meta.drain_value.is_some() {
|
||||
Some((changeset, (internal_keychain, change_index)))
|
||||
Some(CreateTxChange {
|
||||
index_changeset: changeset,
|
||||
change_keychain: internal_keychain,
|
||||
index: change_index,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -401,35 +405,34 @@ where
|
||||
Ok((transaction, change_info))
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
// Alias the elements of `Result` of `planned_utxos`
|
||||
pub type PlannedUtxo<K, A> = (bdk_tmp_plan::Plan<K>, FullTxOut<A>);
|
||||
|
||||
pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>(
|
||||
graph: &KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
assets: &bdk_tmp_plan::Assets<K>,
|
||||
) -> Result<Vec<(bdk_tmp_plan::Plan<K>, FullTxOut<A>)>, O::Error> {
|
||||
) -> Result<Vec<PlannedUtxo<K, A>>, O::Error> {
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
graph
|
||||
.graph()
|
||||
.try_filter_chain_unspents(chain, chain_tip, outpoints)
|
||||
.filter_map(
|
||||
#[allow(clippy::type_complexity)]
|
||||
|r| -> Option<Result<(bdk_tmp_plan::Plan<K>, FullTxOut<A>), _>> {
|
||||
let (k, i, full_txo) = match r {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(((k, i), full_txo)) => (k, i, full_txo),
|
||||
};
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&k)
|
||||
.expect("keychain must exist")
|
||||
.at_derivation_index(i)
|
||||
.expect("i can't be hardened");
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
Some(Ok((plan, full_txo)))
|
||||
},
|
||||
)
|
||||
.filter_map(|r| -> Option<Result<PlannedUtxo<K, A>, _>> {
|
||||
let (k, i, full_txo) = match r {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(((k, i), full_txo)) => (k, i, full_txo),
|
||||
};
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&k)
|
||||
.expect("keychain must exist")
|
||||
.at_derivation_index(i)
|
||||
.expect("i can't be hardened");
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
Some(Ok((plan, full_txo)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -599,7 +602,12 @@ where
|
||||
let (tx, change_info) =
|
||||
create_tx(graph, chain, keymap, coin_select, address, value)?;
|
||||
|
||||
if let Some((index_changeset, (change_keychain, index))) = change_info {
|
||||
if let Some(CreateTxChange {
|
||||
index_changeset,
|
||||
change_keychain,
|
||||
index,
|
||||
}) = change_info
|
||||
{
|
||||
// We must first persist to disk the fact that we've got a new address from the
|
||||
// change keychain so future scans will find the tx we're about to broadcast.
|
||||
// If we're unable to persist this, then we don't want to broadcast.
|
||||
@@ -648,17 +656,26 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
/// The initial state returned by [`init`].
|
||||
pub struct Init<CS: clap::Subcommand, S: clap::Args, C> {
|
||||
/// Arguments parsed by the cli.
|
||||
pub args: Args<CS, S>,
|
||||
/// Descriptor keymap.
|
||||
pub keymap: KeyMap,
|
||||
/// Keychain-txout index.
|
||||
pub index: KeychainTxOutIndex<Keychain>,
|
||||
/// Persistence backend.
|
||||
pub db: Mutex<Database<C>>,
|
||||
/// Initial changeset.
|
||||
pub init_changeset: C,
|
||||
}
|
||||
|
||||
/// Parses command line arguments and initializes all components, creating
|
||||
/// a file store with the given parameters, or loading one if it exists.
|
||||
pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
|
||||
db_magic: &[u8],
|
||||
db_default_path: &str,
|
||||
) -> anyhow::Result<(
|
||||
Args<CS, S>,
|
||||
KeyMap,
|
||||
KeychainTxOutIndex<Keychain>,
|
||||
Mutex<Database<C>>,
|
||||
C,
|
||||
)>
|
||||
) -> anyhow::Result<Init<CS, S, C>>
|
||||
where
|
||||
C: Default + Append + Serialize + DeserializeOwned,
|
||||
{
|
||||
@@ -692,11 +709,11 @@ where
|
||||
|
||||
let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
|
||||
|
||||
Ok((
|
||||
Ok(Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
Mutex::new(Database::new(db_backend)),
|
||||
db: Mutex::new(Database::new(db_backend)),
|
||||
init_changeset,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,8 +103,15 @@ type ChangeSet = (
|
||||
);
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (args, keymap, index, db, (disk_local_chain, disk_tx_graph)) =
|
||||
example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let (disk_local_chain, disk_tx_graph) = init_changeset;
|
||||
|
||||
let graph = Mutex::new({
|
||||
let mut graph = IndexedTxGraph::new(index);
|
||||
|
||||
@@ -99,8 +99,13 @@ pub struct ScanOptions {
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let (args, keymap, index, db, init_changeset) =
|
||||
example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
$ cargo run --bin wallet_rpc -- --help
|
||||
|
||||
wallet_rpc 0.1.0
|
||||
Bitcoind RPC example usign `bdk::Wallet`
|
||||
Bitcoind RPC example using `bdk::Wallet`
|
||||
|
||||
USAGE:
|
||||
wallet_rpc [OPTIONS] <DESCRIPTOR> [CHANGE_DESCRIPTOR]
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant}
|
||||
|
||||
const DB_MAGIC: &str = "bdk-rpc-wallet-example";
|
||||
|
||||
/// Bitcoind RPC example usign `bdk::Wallet`.
|
||||
/// Bitcoind RPC example using `bdk::Wallet`.
|
||||
///
|
||||
/// This syncs the chain block-by-block and prints the current balance, transaction count and UTXO
|
||||
/// count.
|
||||
|
||||
Reference in New Issue
Block a user