Compare commits
62 Commits
v1.0.0-alp
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
785371e0a1 | ||
|
|
18626c66ac | ||
|
|
8bf8c7d080 | ||
|
|
a8efeaa0fb | ||
|
|
82141a8201 | ||
|
|
28d75304e1 | ||
|
|
17a9850cba | ||
|
|
53bea0d902 | ||
|
|
5478bb1ebb | ||
|
|
79262185d5 | ||
|
|
7c07b9de02 | ||
|
|
0c8ee1dfe2 | ||
|
|
64eb576348 | ||
|
|
8875c92ec1 | ||
|
|
2cf07d686b | ||
|
|
93f9b83e27 | ||
|
|
892b97d441 | ||
|
|
3aed4cf179 | ||
|
|
af4ee0fa4b | ||
|
|
22d02ed3d1 | ||
|
|
eb73f0659e | ||
|
|
6b43001951 | ||
|
|
d99b3ef4b4 | ||
|
|
1a62488abf | ||
|
|
e761adf481 | ||
|
|
d7f4ab71e2 | ||
|
|
1a39821b88 | ||
|
|
3f9ed95e2e | ||
|
|
8714e9d806 | ||
|
|
43f093d918 | ||
|
|
962305f415 | ||
|
|
db8fbd729d | ||
|
|
e7ec5a8733 | ||
|
|
139eec7da0 | ||
|
|
3bee563c81 | ||
|
|
e5cb7b2066 | ||
|
|
c3fc1dd123 | ||
|
|
a112b4d97c | ||
|
|
af75817d4b | ||
|
|
6204d2c766 | ||
|
|
496601b8b1 | ||
|
|
c4057297a9 | ||
|
|
b34790c6b6 | ||
|
|
2ce4bb4dfc | ||
|
|
36f58870cb | ||
|
|
bbc19c3536 | ||
|
|
22368ab7b0 | ||
|
|
d75d9f94ce | ||
|
|
8f5b172e59 | ||
|
|
46c6f18cc3 | ||
|
|
cf7aca84d1 | ||
|
|
5c7cc30978 | ||
|
|
438cd4682d | ||
|
|
275e069cf4 | ||
|
|
55a17293a4 | ||
|
|
f2a2dae84c | ||
|
|
324eeb3eb4 | ||
|
|
6dab68d35b | ||
|
|
e406675f43 | ||
|
|
4bddb0de62 | ||
|
|
996605f2bf | ||
|
|
45c0cae0a4 |
4
.github/workflows/cont_integration.yml
vendored
4
.github/workflows/cont_integration.yml
vendored
@@ -35,6 +35,8 @@ jobs:
|
||||
cargo update -p home --precise "0.5.5"
|
||||
cargo update -p proptest --precise "1.2.0"
|
||||
cargo update -p url --precise "2.5.0"
|
||||
cargo update -p cc --precise "1.0.105"
|
||||
cargo update -p tokio --precise "1.38.1"
|
||||
- name: Build
|
||||
run: cargo build ${{ matrix.features }}
|
||||
- name: Test
|
||||
@@ -92,7 +94,7 @@ jobs:
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Check bdk wallet
|
||||
working-directory: ./crates/wallet
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown
|
||||
- name: Check esplora
|
||||
working-directory: ./crates/esplora
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features miniscript/no-std,bdk_chain/hashbrown,async
|
||||
|
||||
@@ -4,7 +4,6 @@ members = [
|
||||
"crates/wallet",
|
||||
"crates/chain",
|
||||
"crates/file_store",
|
||||
"crates/sqlite",
|
||||
"crates/electrum",
|
||||
"crates/esplora",
|
||||
"crates/bitcoind_rpc",
|
||||
|
||||
@@ -73,6 +73,8 @@ cargo update -p time --precise "0.3.20"
|
||||
cargo update -p home --precise "0.5.5"
|
||||
cargo update -p proptest --precise "1.2.0"
|
||||
cargo update -p url --precise "2.5.0"
|
||||
cargo update -p cc --precise "1.0.105"
|
||||
cargo update -p tokio --precise "1.38.1"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_bitcoind_rpc"
|
||||
version = "0.12.0"
|
||||
version = "0.13.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -15,7 +15,7 @@ readme = "README.md"
|
||||
[dependencies]
|
||||
bitcoin = { version = "0.32.0", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.19.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.16", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.17", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
|
||||
@@ -3,9 +3,9 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
use bdk_bitcoind_rpc::Emitter;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Address, Amount, Txid},
|
||||
keychain::Balance,
|
||||
local_chain::{CheckPoint, LocalChain},
|
||||
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
||||
spk_txout::SpkTxOutIndex,
|
||||
Balance, BlockId, IndexedTxGraph, Merge,
|
||||
};
|
||||
use bdk_testenv::{anyhow, TestEnv};
|
||||
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
|
||||
@@ -48,7 +48,7 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
|
||||
assert_eq!(
|
||||
local_chain.apply_update(emission.checkpoint,)?,
|
||||
BTreeMap::from([(height, Some(hash))]),
|
||||
[(height, Some(hash))].into(),
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
}
|
||||
@@ -94,11 +94,13 @@ pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
assert_eq!(
|
||||
local_chain.apply_update(emission.checkpoint,)?,
|
||||
if exp_height == exp_hashes.len() - reorged_blocks.len() {
|
||||
core::iter::once((height, Some(hash)))
|
||||
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
|
||||
.collect::<bdk_chain::local_chain::ChangeSet>()
|
||||
bdk_chain::local_chain::ChangeSet {
|
||||
blocks: core::iter::once((height, Some(hash)))
|
||||
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
|
||||
.collect(),
|
||||
}
|
||||
} else {
|
||||
BTreeMap::from([(height, Some(hash))])
|
||||
[(height, Some(hash))].into()
|
||||
},
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
@@ -194,7 +196,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
let indexed_additions = indexed_tx_graph.batch_insert_unconfirmed(mempool_txs);
|
||||
assert_eq!(
|
||||
indexed_additions
|
||||
.graph
|
||||
.tx_graph
|
||||
.txs
|
||||
.iter()
|
||||
.map(|tx| tx.compute_txid())
|
||||
@@ -202,7 +204,7 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
exp_txids,
|
||||
"changeset should have the 3 mempool transactions",
|
||||
);
|
||||
assert!(indexed_additions.graph.anchors.is_empty());
|
||||
assert!(indexed_additions.tx_graph.anchors.is_empty());
|
||||
}
|
||||
|
||||
// mine a block that confirms the 3 txs
|
||||
@@ -225,9 +227,9 @@ fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(emission.checkpoint)?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
assert!(indexed_additions.graph.txs.is_empty());
|
||||
assert!(indexed_additions.graph.txouts.is_empty());
|
||||
assert_eq!(indexed_additions.graph.anchors, exp_anchors);
|
||||
assert!(indexed_additions.tx_graph.txs.is_empty());
|
||||
assert!(indexed_additions.tx_graph.txouts.is_empty());
|
||||
assert_eq!(indexed_additions.tx_graph.anchors, exp_anchors);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -392,7 +394,6 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT * (ADDITIONAL_COUNT - reorg_count) as u64,
|
||||
trusted_pending: SEND_AMOUNT * reorg_count as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.16.0"
|
||||
version = "0.17.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -20,6 +20,10 @@ serde_crate = { package = "serde", version = "1", optional = true, features = ["
|
||||
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
|
||||
miniscript = { version = "12.0.0", optional = true, default-features = false }
|
||||
|
||||
# Feature dependencies
|
||||
rusqlite_crate = { package = "rusqlite", version = "0.31.0", features = ["bundled"], optional = true }
|
||||
serde_json = {version = "1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
proptest = "1.2.0"
|
||||
@@ -28,3 +32,4 @@ proptest = "1.2.0"
|
||||
default = ["std", "miniscript"]
|
||||
std = ["bitcoin/std", "miniscript?/std"]
|
||||
serde = ["serde_crate", "bitcoin/serde", "miniscript?/serde"]
|
||||
rusqlite = ["std", "rusqlite_crate", "serde", "serde_json"]
|
||||
|
||||
@@ -1,20 +1,4 @@
|
||||
//! Module for keychain related structures.
|
||||
//!
|
||||
//! A keychain here is a set of application-defined indexes for a miniscript descriptor where we can
|
||||
//! derive script pubkeys at a particular derivation index. The application's index is simply
|
||||
//! anything that implements `Ord`.
|
||||
//!
|
||||
//! [`KeychainTxOutIndex`] indexes script pubkeys of keychains and scans in relevant outpoints (that
|
||||
//! has a `txout` containing an indexed script pubkey). Internally, this uses [`SpkTxOutIndex`], but
|
||||
//! also maintains "revealed" and "lookahead" index counts per keychain.
|
||||
//!
|
||||
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod txout_index;
|
||||
use bitcoin::{Amount, ScriptBuf};
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use txout_index::*;
|
||||
use bitcoin::Amount;
|
||||
|
||||
/// Balance, differentiated into various categories.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
@@ -49,11 +33,6 @@ impl Balance {
|
||||
}
|
||||
}
|
||||
|
||||
/// A tuple of keychain index and `T` representing the indexed value.
|
||||
pub type Indexed<T> = (u32, T);
|
||||
/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them.
|
||||
pub type KeychainIndexed<K, T> = ((K, u32), T);
|
||||
|
||||
impl core::fmt::Display for Balance {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
@@ -74,11 +74,11 @@ impl ConfirmationTime {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChainPosition<ConfirmationTimeHeightAnchor>> for ConfirmationTime {
|
||||
fn from(observed_as: ChainPosition<ConfirmationTimeHeightAnchor>) -> Self {
|
||||
impl From<ChainPosition<ConfirmationBlockTime>> for ConfirmationTime {
|
||||
fn from(observed_as: ChainPosition<ConfirmationBlockTime>) -> Self {
|
||||
match observed_as {
|
||||
ChainPosition::Confirmed(a) => Self::Confirmed {
|
||||
height: a.confirmation_height,
|
||||
height: a.block_id.height,
|
||||
time: a.confirmation_time,
|
||||
},
|
||||
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
|
||||
@@ -145,9 +145,7 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
|
||||
///
|
||||
/// Note that the confirmation block and the anchor block can be different here.
|
||||
/// An [`Anchor`] implementation that also records the exact confirmation time of the transaction.
|
||||
///
|
||||
/// Refer to [`Anchor`] for more details.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
@@ -156,70 +154,27 @@ impl From<(&u32, &BlockHash)> for BlockId {
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationHeightAnchor {
|
||||
/// 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,
|
||||
pub struct ConfirmationBlockTime {
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationHeightAnchor {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
self.anchor_block
|
||||
}
|
||||
|
||||
fn confirmation_height_upper_bound(&self) -> u32 {
|
||||
self.confirmation_height
|
||||
}
|
||||
}
|
||||
|
||||
impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
|
||||
fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
|
||||
Self {
|
||||
anchor_block: block_id,
|
||||
confirmation_height: block_id.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
|
||||
/// transaction.
|
||||
///
|
||||
/// Note that the confirmation block and the anchor block can be different here.
|
||||
///
|
||||
/// Refer to [`Anchor`] for more details.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationTimeHeightAnchor {
|
||||
/// The confirmation height of the transaction being anchored.
|
||||
pub confirmation_height: u32,
|
||||
pub block_id: BlockId,
|
||||
/// The confirmation time of the transaction being anchored.
|
||||
pub confirmation_time: u64,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationTimeHeightAnchor {
|
||||
impl Anchor for ConfirmationBlockTime {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
self.anchor_block
|
||||
self.block_id
|
||||
}
|
||||
|
||||
fn confirmation_height_upper_bound(&self) -> u32 {
|
||||
self.confirmation_height
|
||||
self.block_id.height
|
||||
}
|
||||
}
|
||||
|
||||
impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
|
||||
impl AnchorFromBlockPosition for ConfirmationBlockTime {
|
||||
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
|
||||
Self {
|
||||
anchor_block: block_id,
|
||||
confirmation_height: block_id.height,
|
||||
block_id,
|
||||
confirmation_time: block.header.time as _,
|
||||
}
|
||||
}
|
||||
@@ -305,19 +260,19 @@ mod test {
|
||||
|
||||
#[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,
|
||||
let unconf1 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(10);
|
||||
let unconf2 = ChainPosition::<ConfirmationBlockTime>::Unconfirmed(20);
|
||||
let conf1 = ChainPosition::Confirmed(ConfirmationBlockTime {
|
||||
confirmation_time: 20,
|
||||
block_id: BlockId {
|
||||
height: 9,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 12,
|
||||
anchor_block: BlockId {
|
||||
height: 15,
|
||||
let conf2 = ChainPosition::Confirmed(ConfirmationBlockTime {
|
||||
confirmation_time: 15,
|
||||
block_id: BlockId {
|
||||
height: 12,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/// A changeset containing [`crate`] structures typically persisted together.
|
||||
#[cfg(feature = "miniscript")]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(crate::serde::Deserialize, crate::serde::Serialize),
|
||||
serde(
|
||||
crate = "crate::serde",
|
||||
bound(
|
||||
deserialize = "A: Ord + crate::serde::Deserialize<'de>, K: Ord + crate::serde::Deserialize<'de>",
|
||||
serialize = "A: Ord + crate::serde::Serialize, K: Ord + crate::serde::Serialize",
|
||||
),
|
||||
)
|
||||
)]
|
||||
pub struct CombinedChangeSet<K, A> {
|
||||
/// Changes to the [`LocalChain`](crate::local_chain::LocalChain).
|
||||
pub chain: crate::local_chain::ChangeSet,
|
||||
/// Changes to [`IndexedTxGraph`](crate::indexed_tx_graph::IndexedTxGraph).
|
||||
pub indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,
|
||||
/// Stores the network type of the transaction data.
|
||||
pub network: Option<bitcoin::Network>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K, A> core::default::Default for CombinedChangeSet<K, A> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
chain: core::default::Default::default(),
|
||||
indexed_tx_graph: core::default::Default::default(),
|
||||
network: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K: Ord, A: crate::Anchor> crate::Append for CombinedChangeSet<K, A> {
|
||||
fn append(&mut self, other: Self) {
|
||||
crate::Append::append(&mut self.chain, other.chain);
|
||||
crate::Append::append(&mut self.indexed_tx_graph, other.indexed_tx_graph);
|
||||
if other.network.is_some() {
|
||||
debug_assert!(
|
||||
self.network.is_none() || self.network == other.network,
|
||||
"network type must either be just introduced or remain the same"
|
||||
);
|
||||
self.network = other.network;
|
||||
}
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.chain.is_empty() && self.indexed_tx_graph.is_empty() && self.network.is_none()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K, A> From<crate::local_chain::ChangeSet> for CombinedChangeSet<K, A> {
|
||||
fn from(chain: crate::local_chain::ChangeSet) -> Self {
|
||||
Self {
|
||||
chain,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K, A> From<crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>>
|
||||
for CombinedChangeSet<K, A>
|
||||
{
|
||||
fn from(
|
||||
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet<A, crate::keychain::ChangeSet<K>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
indexed_tx_graph,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<K, A> From<crate::keychain::ChangeSet<K>> for CombinedChangeSet<K, A> {
|
||||
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
|
||||
Self {
|
||||
indexed_tx_graph: crate::indexed_tx_graph::ChangeSet {
|
||||
indexer,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,8 @@
|
||||
use crate::{
|
||||
alloc::{string::ToString, vec::Vec},
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
};
|
||||
use crate::miniscript::{Descriptor, DescriptorPublicKey};
|
||||
use bitcoin::hashes::{hash_newtype, sha256, Hash};
|
||||
|
||||
hash_newtype! {
|
||||
/// Represents the ID of a descriptor, defined as the sha256 hash of
|
||||
/// the descriptor string, checksum excluded.
|
||||
/// Represents the unique ID of a descriptor.
|
||||
///
|
||||
/// This is useful for having a fixed-length unique representation of a descriptor,
|
||||
/// in particular, we use it to persist application state changes related to the
|
||||
@@ -21,8 +17,8 @@ pub trait DescriptorExt {
|
||||
/// Panics if the descriptor wildcard is hardened.
|
||||
fn dust_value(&self) -> u64;
|
||||
|
||||
/// Returns the descriptor id, calculated as the sha256 of the descriptor, checksum not
|
||||
/// included.
|
||||
/// Returns the descriptor ID, calculated as the sha256 hash of the spk derived from the
|
||||
/// descriptor at index 0.
|
||||
fn descriptor_id(&self) -> DescriptorId;
|
||||
}
|
||||
|
||||
@@ -36,9 +32,7 @@ impl DescriptorExt for Descriptor<DescriptorPublicKey> {
|
||||
}
|
||||
|
||||
fn descriptor_id(&self) -> DescriptorId {
|
||||
let desc = self.to_string();
|
||||
let desc_without_checksum = desc.split('#').next().expect("Must be here");
|
||||
let descriptor_bytes = <Vec<u8>>::from(desc_without_checksum.as_bytes());
|
||||
DescriptorId(sha256::Hash::hash(&descriptor_bytes))
|
||||
let spk = self.at_derivation_index(0).unwrap().script_pubkey();
|
||||
DescriptorId(sha256::Hash::hash(spk.as_bytes()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the
|
||||
//! [`IndexedTxGraph`] documentation for more.
|
||||
use core::fmt::Debug;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
|
||||
|
||||
use crate::{
|
||||
tx_graph::{self, TxGraph},
|
||||
Anchor, AnchorFromBlockPosition, Append, BlockId,
|
||||
Anchor, AnchorFromBlockPosition, BlockId, Indexer, Merge,
|
||||
};
|
||||
|
||||
/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
|
||||
@@ -47,27 +49,30 @@ impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
|
||||
pub fn apply_changeset(&mut self, changeset: ChangeSet<A, I::ChangeSet>) {
|
||||
self.index.apply_changeset(changeset.indexer);
|
||||
|
||||
for tx in &changeset.graph.txs {
|
||||
for tx in &changeset.tx_graph.txs {
|
||||
self.index.index_tx(tx);
|
||||
}
|
||||
for (&outpoint, txout) in &changeset.graph.txouts {
|
||||
for (&outpoint, txout) in &changeset.tx_graph.txouts {
|
||||
self.index.index_txout(outpoint, txout);
|
||||
}
|
||||
|
||||
self.graph.apply_changeset(changeset.graph);
|
||||
self.graph.apply_changeset(changeset.tx_graph);
|
||||
}
|
||||
|
||||
/// Determines the [`ChangeSet`] between `self` and an empty [`IndexedTxGraph`].
|
||||
pub fn initial_changeset(&self) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.initial_changeset();
|
||||
let indexer = self.index.initial_changeset();
|
||||
ChangeSet { graph, indexer }
|
||||
ChangeSet {
|
||||
tx_graph: graph,
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
|
||||
where
|
||||
I::ChangeSet: Default + Append,
|
||||
I::ChangeSet: Default + Merge,
|
||||
{
|
||||
fn index_tx_graph_changeset(
|
||||
&mut self,
|
||||
@@ -75,10 +80,10 @@ where
|
||||
) -> I::ChangeSet {
|
||||
let mut changeset = I::ChangeSet::default();
|
||||
for added_tx in &tx_graph_changeset.txs {
|
||||
changeset.append(self.index.index_tx(added_tx));
|
||||
changeset.merge(self.index.index_tx(added_tx));
|
||||
}
|
||||
for (&added_outpoint, added_txout) in &tx_graph_changeset.txouts {
|
||||
changeset.append(self.index.index_txout(added_outpoint, added_txout));
|
||||
changeset.merge(self.index.index_txout(added_outpoint, added_txout));
|
||||
}
|
||||
changeset
|
||||
}
|
||||
@@ -89,21 +94,30 @@ where
|
||||
pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.apply_update(update);
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
ChangeSet {
|
||||
tx_graph: graph,
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a floating `txout` of given `outpoint`.
|
||||
pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.insert_txout(outpoint, txout);
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
ChangeSet {
|
||||
tx_graph: graph,
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert and index a transaction into the graph.
|
||||
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.insert_tx(tx);
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
ChangeSet {
|
||||
tx_graph: graph,
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert an `anchor` for a given transaction.
|
||||
@@ -137,21 +151,24 @@ where
|
||||
|
||||
let mut indexer = I::ChangeSet::default();
|
||||
for (tx, _) in &txs {
|
||||
indexer.append(self.index.index_tx(tx));
|
||||
indexer.merge(self.index.index_tx(tx));
|
||||
}
|
||||
|
||||
let mut graph = tx_graph::ChangeSet::default();
|
||||
for (tx, anchors) in txs {
|
||||
if self.index.is_tx_relevant(tx) {
|
||||
let txid = tx.compute_txid();
|
||||
graph.append(self.graph.insert_tx(tx.clone()));
|
||||
graph.merge(self.graph.insert_tx(tx.clone()));
|
||||
for anchor in anchors {
|
||||
graph.append(self.graph.insert_anchor(txid, anchor));
|
||||
graph.merge(self.graph.insert_anchor(txid, anchor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChangeSet { graph, indexer }
|
||||
ChangeSet {
|
||||
tx_graph: graph,
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch insert unconfirmed transactions, filtering out those that are irrelevant.
|
||||
@@ -176,7 +193,7 @@ where
|
||||
|
||||
let mut indexer = I::ChangeSet::default();
|
||||
for (tx, _) in &txs {
|
||||
indexer.append(self.index.index_tx(tx));
|
||||
indexer.merge(self.index.index_tx(tx));
|
||||
}
|
||||
|
||||
let graph = self.graph.batch_insert_unconfirmed(
|
||||
@@ -185,7 +202,10 @@ where
|
||||
.map(|(tx, seen_at)| (tx.clone(), seen_at)),
|
||||
);
|
||||
|
||||
ChangeSet { graph, indexer }
|
||||
ChangeSet {
|
||||
tx_graph: graph,
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
|
||||
/// Batch insert unconfirmed transactions.
|
||||
@@ -203,14 +223,17 @@ where
|
||||
) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.batch_insert_unconfirmed(txs);
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
ChangeSet {
|
||||
tx_graph: graph,
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Methods are available if the anchor (`A`) implements [`AnchorFromBlockPosition`].
|
||||
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
|
||||
where
|
||||
I::ChangeSet: Default + Append,
|
||||
I::ChangeSet: Default + Merge,
|
||||
A: AnchorFromBlockPosition,
|
||||
{
|
||||
/// Batch insert all transactions of the given `block` of `height`, filtering out those that are
|
||||
@@ -232,14 +255,14 @@ where
|
||||
};
|
||||
let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
|
||||
for (tx_pos, tx) in block.txdata.iter().enumerate() {
|
||||
changeset.indexer.append(self.index.index_tx(tx));
|
||||
changeset.indexer.merge(self.index.index_tx(tx));
|
||||
if self.index.is_tx_relevant(tx) {
|
||||
let txid = tx.compute_txid();
|
||||
let anchor = A::from_block_position(block, block_id, tx_pos);
|
||||
changeset.graph.append(self.graph.insert_tx(tx.clone()));
|
||||
changeset.tx_graph.merge(self.graph.insert_tx(tx.clone()));
|
||||
changeset
|
||||
.graph
|
||||
.append(self.graph.insert_anchor(txid, anchor));
|
||||
.tx_graph
|
||||
.merge(self.graph.insert_anchor(txid, anchor));
|
||||
}
|
||||
}
|
||||
changeset
|
||||
@@ -261,11 +284,20 @@ where
|
||||
let mut graph = tx_graph::ChangeSet::default();
|
||||
for (tx_pos, tx) in block.txdata.iter().enumerate() {
|
||||
let anchor = A::from_block_position(&block, block_id, tx_pos);
|
||||
graph.append(self.graph.insert_anchor(tx.compute_txid(), anchor));
|
||||
graph.append(self.graph.insert_tx(tx.clone()));
|
||||
graph.merge(self.graph.insert_anchor(tx.compute_txid(), anchor));
|
||||
graph.merge(self.graph.insert_tx(tx.clone()));
|
||||
}
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
ChangeSet {
|
||||
tx_graph: graph,
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
|
||||
fn as_ref(&self) -> &TxGraph<A> {
|
||||
&self.graph
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +317,7 @@ where
|
||||
#[must_use]
|
||||
pub struct ChangeSet<A, IA> {
|
||||
/// [`TxGraph`] changeset.
|
||||
pub graph: tx_graph::ChangeSet<A>,
|
||||
pub tx_graph: tx_graph::ChangeSet<A>,
|
||||
/// [`Indexer`] changeset.
|
||||
pub indexer: IA,
|
||||
}
|
||||
@@ -293,68 +325,38 @@ pub struct ChangeSet<A, IA> {
|
||||
impl<A, IA: Default> Default for ChangeSet<A, IA> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
tx_graph: Default::default(),
|
||||
indexer: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, IA: Append> Append for ChangeSet<A, IA> {
|
||||
fn append(&mut self, other: Self) {
|
||||
self.graph.append(other.graph);
|
||||
self.indexer.append(other.indexer);
|
||||
impl<A: Anchor, IA: Merge> Merge for ChangeSet<A, IA> {
|
||||
fn merge(&mut self, other: Self) {
|
||||
self.tx_graph.merge(other.tx_graph);
|
||||
self.indexer.merge(other.indexer);
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.graph.is_empty() && self.indexer.is_empty()
|
||||
self.tx_graph.is_empty() && self.indexer.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
|
||||
fn from(graph: tx_graph::ChangeSet<A>) -> Self {
|
||||
Self {
|
||||
graph,
|
||||
tx_graph: graph,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl<A, K> From<crate::keychain::ChangeSet<K>> for ChangeSet<A, crate::keychain::ChangeSet<K>> {
|
||||
fn from(indexer: crate::keychain::ChangeSet<K>) -> Self {
|
||||
impl<A> From<crate::keychain_txout::ChangeSet> for ChangeSet<A, crate::keychain_txout::ChangeSet> {
|
||||
fn from(indexer: crate::keychain_txout::ChangeSet) -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
tx_graph: Default::default(),
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Utilities for indexing transaction data.
|
||||
///
|
||||
/// Types which implement this trait can be used to construct an [`IndexedTxGraph`].
|
||||
/// This trait's methods should rarely be called directly.
|
||||
pub trait Indexer {
|
||||
/// The resultant "changeset" when new transaction data is indexed.
|
||||
type ChangeSet;
|
||||
|
||||
/// Scan and index the given `outpoint` and `txout`.
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet;
|
||||
|
||||
/// Scans a transaction for relevant outpoints, which are stored and indexed internally.
|
||||
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet;
|
||||
|
||||
/// Apply changeset to itself.
|
||||
fn apply_changeset(&mut self, changeset: Self::ChangeSet);
|
||||
|
||||
/// Determines the [`ChangeSet`] between `self` and an empty [`Indexer`].
|
||||
fn initial_changeset(&self) -> Self::ChangeSet;
|
||||
|
||||
/// Determines whether the transaction should be included in the index.
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
|
||||
}
|
||||
|
||||
impl<A, I> AsRef<TxGraph<A>> for IndexedTxGraph<A, I> {
|
||||
fn as_ref(&self) -> &TxGraph<A> {
|
||||
&self.graph
|
||||
}
|
||||
}
|
||||
|
||||
33
crates/chain/src/indexer.rs
Normal file
33
crates/chain/src/indexer.rs
Normal file
@@ -0,0 +1,33 @@
|
||||
//! [`Indexer`] provides utilities for indexing transaction data.
|
||||
|
||||
use bitcoin::{OutPoint, Transaction, TxOut};
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub mod keychain_txout;
|
||||
pub mod spk_txout;
|
||||
|
||||
/// Utilities for indexing transaction data.
|
||||
///
|
||||
/// Types which implement this trait can be used to construct an [`IndexedTxGraph`].
|
||||
/// This trait's methods should rarely be called directly.
|
||||
///
|
||||
/// [`IndexedTxGraph`]: crate::IndexedTxGraph
|
||||
pub trait Indexer {
|
||||
/// The resultant "changeset" when new transaction data is indexed.
|
||||
type ChangeSet;
|
||||
|
||||
/// Scan and index the given `outpoint` and `txout`.
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet;
|
||||
|
||||
/// Scans a transaction for relevant outpoints, which are stored and indexed internally.
|
||||
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet;
|
||||
|
||||
/// Apply changeset to itself.
|
||||
fn apply_changeset(&mut self, changeset: Self::ChangeSet);
|
||||
|
||||
/// Determines the [`ChangeSet`](Indexer::ChangeSet) between `self` and an empty [`Indexer`].
|
||||
fn initial_changeset(&self) -> Self::ChangeSet;
|
||||
|
||||
/// Determines whether the transaction should be included in the index.
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
//! [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains and
|
||||
//! indexes [`TxOut`]s with them.
|
||||
|
||||
use crate::{
|
||||
collections::*,
|
||||
indexed_tx_graph::Indexer,
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
spk_iter::BIP32_MAX_INDEX,
|
||||
DescriptorExt, DescriptorId, SpkIterator, SpkTxOutIndex,
|
||||
spk_txout::SpkTxOutIndex,
|
||||
DescriptorExt, DescriptorId, Indexed, Indexer, KeychainIndexed, SpkIterator,
|
||||
};
|
||||
use alloc::{borrow::ToOwned, vec::Vec};
|
||||
use bitcoin::{Amount, OutPoint, Script, SignedAmount, Transaction, TxOut, Txid};
|
||||
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
|
||||
use core::{
|
||||
fmt::Debug,
|
||||
ops::{Bound, RangeBounds},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::Append;
|
||||
use crate::Merge;
|
||||
|
||||
/// The default lookahead for a [`KeychainTxOutIndex`]
|
||||
pub const DEFAULT_LOOKAHEAD: u32 = 25;
|
||||
@@ -73,7 +75,7 @@ pub const DEFAULT_LOOKAHEAD: u32 = 25;
|
||||
/// ## Synopsis
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_chain::keychain::KeychainTxOutIndex;
|
||||
/// use bdk_chain::indexer::keychain_txout::KeychainTxOutIndex;
|
||||
/// # use bdk_chain::{ miniscript::{Descriptor, DescriptorPublicKey} };
|
||||
/// # use core::str::FromStr;
|
||||
///
|
||||
@@ -97,8 +99,8 @@ pub const DEFAULT_LOOKAHEAD: u32 = 25;
|
||||
/// let _ = txout_index.insert_descriptor(MyKeychain::Internal, internal_descriptor)?;
|
||||
/// let _ = txout_index.insert_descriptor(MyKeychain::MyAppUser { user_id: 42 }, descriptor_42)?;
|
||||
///
|
||||
/// let new_spk_for_user = txout_index.reveal_next_spk(&MyKeychain::MyAppUser{ user_id: 42 });
|
||||
/// # Ok::<_, bdk_chain::keychain::InsertDescriptorError<_>>(())
|
||||
/// let new_spk_for_user = txout_index.reveal_next_spk(MyKeychain::MyAppUser{ user_id: 42 });
|
||||
/// # Ok::<_, bdk_chain::indexer::keychain_txout::InsertDescriptorError<_>>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`Ord`]: core::cmp::Ord
|
||||
@@ -134,7 +136,7 @@ impl<K> Default for KeychainTxOutIndex<K> {
|
||||
}
|
||||
|
||||
impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
|
||||
type ChangeSet = ChangeSet<K>;
|
||||
type ChangeSet = ChangeSet;
|
||||
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
|
||||
let mut changeset = ChangeSet::default();
|
||||
@@ -153,20 +155,16 @@ impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
|
||||
}
|
||||
|
||||
fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet {
|
||||
let mut changeset = ChangeSet::<K>::default();
|
||||
let mut changeset = ChangeSet::default();
|
||||
let txid = tx.compute_txid();
|
||||
for (op, txout) in tx.output.iter().enumerate() {
|
||||
changeset.append(self.index_txout(OutPoint::new(txid, op as u32), txout));
|
||||
changeset.merge(self.index_txout(OutPoint::new(txid, op as u32), txout));
|
||||
}
|
||||
changeset
|
||||
}
|
||||
|
||||
fn initial_changeset(&self) -> Self::ChangeSet {
|
||||
ChangeSet {
|
||||
keychains_added: self
|
||||
.keychains()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect(),
|
||||
last_revealed: self.last_revealed.clone().into_iter().collect(),
|
||||
}
|
||||
}
|
||||
@@ -253,14 +251,14 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Return the script that exists under the given `keychain`'s `index`.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::spk_at_index`] internally.
|
||||
pub fn spk_at_index(&self, keychain: K, index: u32) -> Option<&Script> {
|
||||
pub fn spk_at_index(&self, keychain: K, index: u32) -> Option<ScriptBuf> {
|
||||
self.inner.spk_at_index(&(keychain.clone(), index))
|
||||
}
|
||||
|
||||
/// Returns the keychain and keychain index associated with the spk.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::index_of_spk`] internally.
|
||||
pub fn index_of_spk(&self, script: &Script) -> Option<&(K, u32)> {
|
||||
pub fn index_of_spk(&self, script: ScriptBuf) -> Option<&(K, u32)> {
|
||||
self.inner.index_of_spk(script)
|
||||
}
|
||||
|
||||
@@ -337,11 +335,11 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Return all keychains and their corresponding descriptors.
|
||||
pub fn keychains(
|
||||
&self,
|
||||
) -> impl DoubleEndedIterator<Item = (&K, &Descriptor<DescriptorPublicKey>)> + ExactSizeIterator + '_
|
||||
) -> impl DoubleEndedIterator<Item = (K, &Descriptor<DescriptorPublicKey>)> + ExactSizeIterator + '_
|
||||
{
|
||||
self.keychain_to_descriptor_id
|
||||
.iter()
|
||||
.map(|(k, did)| (k, self.descriptors.get(did).expect("invariant")))
|
||||
.map(|(k, did)| (k.clone(), self.descriptors.get(did).expect("invariant")))
|
||||
}
|
||||
|
||||
/// Insert a descriptor with a keychain associated to it.
|
||||
@@ -352,12 +350,18 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
///
|
||||
/// keychain <-> descriptor is a one-to-one mapping that cannot be changed. Attempting to do so
|
||||
/// will return a [`InsertDescriptorError<K>`].
|
||||
///
|
||||
/// [`KeychainTxOutIndex`] will prevent you from inserting two descriptors which derive the same
|
||||
/// script pubkey at index 0, but it's up to you to ensure that descriptors don't collide at
|
||||
/// other indices. If they do nothing catastrophic happens at the `KeychainTxOutIndex` level
|
||||
/// (one keychain just becomes the defacto owner of that spk arbitrarily) but this may have
|
||||
/// subtle implications up the application stack like one UTXO being missing from one keychain
|
||||
/// because it has been assigned to another which produces the same script pubkey.
|
||||
pub fn insert_descriptor(
|
||||
&mut self,
|
||||
keychain: K,
|
||||
descriptor: Descriptor<DescriptorPublicKey>,
|
||||
) -> Result<ChangeSet<K>, InsertDescriptorError<K>> {
|
||||
let mut changeset = ChangeSet::<K>::default();
|
||||
) -> Result<bool, InsertDescriptorError<K>> {
|
||||
let did = descriptor.descriptor_id();
|
||||
if !self.keychain_to_descriptor_id.contains_key(&keychain)
|
||||
&& !self.descriptor_id_to_keychain.contains_key(&did)
|
||||
@@ -366,39 +370,37 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
self.keychain_to_descriptor_id.insert(keychain.clone(), did);
|
||||
self.descriptor_id_to_keychain.insert(did, keychain.clone());
|
||||
self.replenish_inner_index(did, &keychain, self.lookahead);
|
||||
changeset
|
||||
.keychains_added
|
||||
.insert(keychain.clone(), descriptor);
|
||||
} else {
|
||||
if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) {
|
||||
let descriptor = self.descriptors.get(existing_desc_id).expect("invariant");
|
||||
if *existing_desc_id != did {
|
||||
return Err(InsertDescriptorError::KeychainAlreadyAssigned {
|
||||
existing_assignment: descriptor.clone(),
|
||||
keychain,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Some(existing_keychain) = self.descriptor_id_to_keychain.get(&did) {
|
||||
let descriptor = self.descriptors.get(&did).expect("invariant").clone();
|
||||
|
||||
if *existing_keychain != keychain {
|
||||
return Err(InsertDescriptorError::DescriptorAlreadyAssigned {
|
||||
existing_assignment: existing_keychain.clone(),
|
||||
descriptor,
|
||||
});
|
||||
}
|
||||
if let Some(existing_desc_id) = self.keychain_to_descriptor_id.get(&keychain) {
|
||||
let descriptor = self.descriptors.get(existing_desc_id).expect("invariant");
|
||||
if *existing_desc_id != did {
|
||||
return Err(InsertDescriptorError::KeychainAlreadyAssigned {
|
||||
existing_assignment: descriptor.clone(),
|
||||
keychain,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
if let Some(existing_keychain) = self.descriptor_id_to_keychain.get(&did) {
|
||||
let descriptor = self.descriptors.get(&did).expect("invariant").clone();
|
||||
|
||||
if *existing_keychain != keychain {
|
||||
return Err(InsertDescriptorError::DescriptorAlreadyAssigned {
|
||||
existing_assignment: existing_keychain.clone(),
|
||||
descriptor,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// Gets the descriptor associated with the keychain. Returns `None` if the keychain doesn't
|
||||
/// have a descriptor associated with it.
|
||||
pub fn get_descriptor(&self, keychain: &K) -> Option<&Descriptor<DescriptorPublicKey>> {
|
||||
let did = self.keychain_to_descriptor_id.get(keychain)?;
|
||||
pub fn get_descriptor(&self, keychain: K) -> Option<&Descriptor<DescriptorPublicKey>> {
|
||||
let did = self.keychain_to_descriptor_id.get(&keychain)?;
|
||||
self.descriptors.get(did)
|
||||
}
|
||||
|
||||
@@ -414,8 +416,8 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Store lookahead scripts until `target_index` (inclusive).
|
||||
///
|
||||
/// This does not change the global `lookahead` setting.
|
||||
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
|
||||
if let Some((next_index, _)) = self.next_index(keychain) {
|
||||
pub fn lookahead_to_target(&mut self, keychain: K, target_index: u32) {
|
||||
if let Some((next_index, _)) = self.next_index(keychain.clone()) {
|
||||
let temp_lookahead = (target_index + 1)
|
||||
.checked_sub(next_index)
|
||||
.filter(|&index| index > 0);
|
||||
@@ -432,9 +434,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
}
|
||||
}
|
||||
|
||||
fn replenish_inner_index_keychain(&mut self, keychain: &K, lookahead: u32) {
|
||||
if let Some(did) = self.keychain_to_descriptor_id.get(keychain) {
|
||||
self.replenish_inner_index(*did, keychain, lookahead);
|
||||
fn replenish_inner_index_keychain(&mut self, keychain: K, lookahead: u32) {
|
||||
if let Some(did) = self.keychain_to_descriptor_id.get(&keychain) {
|
||||
self.replenish_inner_index(*did, &keychain, lookahead);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,7 +464,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// keychain doesn't exist
|
||||
pub fn unbounded_spk_iter(
|
||||
&self,
|
||||
keychain: &K,
|
||||
keychain: K,
|
||||
) -> Option<SpkIterator<Descriptor<DescriptorPublicKey>>> {
|
||||
let descriptor = self.get_descriptor(keychain)?.clone();
|
||||
Some(SpkIterator::new(descriptor))
|
||||
@@ -487,7 +489,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
pub fn revealed_spks(
|
||||
&self,
|
||||
range: impl RangeBounds<K>,
|
||||
) -> impl Iterator<Item = KeychainIndexed<K, &Script>> {
|
||||
) -> impl Iterator<Item = KeychainIndexed<K, ScriptBuf>> + '_ {
|
||||
let start = range.start_bound();
|
||||
let end = range.end_bound();
|
||||
let mut iter_last_revealed = self
|
||||
@@ -514,7 +516,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
let (current_keychain, last_revealed) = current_keychain?;
|
||||
|
||||
if current_keychain == keychain && Some(*index) <= last_revealed {
|
||||
break Some(((keychain.clone(), *index), spk.as_script()));
|
||||
break Some(((keychain.clone(), *index), spk.clone()));
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -523,27 +525,27 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
///
|
||||
/// This is a double ended iterator so you can easily reverse it to get an iterator where
|
||||
/// the script pubkeys that were most recently revealed are first.
|
||||
pub fn revealed_keychain_spks<'a>(
|
||||
&'a self,
|
||||
keychain: &'a K,
|
||||
) -> impl DoubleEndedIterator<Item = Indexed<&Script>> + 'a {
|
||||
pub fn revealed_keychain_spks(
|
||||
&self,
|
||||
keychain: K,
|
||||
) -> impl DoubleEndedIterator<Item = Indexed<ScriptBuf>> + '_ {
|
||||
let end = self
|
||||
.last_revealed_index(keychain)
|
||||
.last_revealed_index(keychain.clone())
|
||||
.map(|v| v + 1)
|
||||
.unwrap_or(0);
|
||||
self.inner
|
||||
.all_spks()
|
||||
.range((keychain.clone(), 0)..(keychain.clone(), end))
|
||||
.map(|((_, index), spk)| (*index, spk.as_script()))
|
||||
.map(|((_, index), spk)| (*index, spk.clone()))
|
||||
}
|
||||
|
||||
/// Iterate over revealed, but unused, spks of all keychains.
|
||||
pub fn unused_spks(
|
||||
&self,
|
||||
) -> impl DoubleEndedIterator<Item = KeychainIndexed<K, &Script>> + Clone {
|
||||
) -> impl DoubleEndedIterator<Item = KeychainIndexed<K, ScriptBuf>> + Clone + '_ {
|
||||
self.keychain_to_descriptor_id.keys().flat_map(|keychain| {
|
||||
self.unused_keychain_spks(keychain)
|
||||
.map(|(i, spk)| ((keychain.clone(), i), spk))
|
||||
self.unused_keychain_spks(keychain.clone())
|
||||
.map(|(i, spk)| ((keychain.clone(), i), spk.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -551,9 +553,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Returns an empty iterator if the provided keychain doesn't exist.
|
||||
pub fn unused_keychain_spks(
|
||||
&self,
|
||||
keychain: &K,
|
||||
) -> impl DoubleEndedIterator<Item = Indexed<&Script>> + Clone {
|
||||
let end = match self.keychain_to_descriptor_id.get(keychain) {
|
||||
keychain: K,
|
||||
) -> impl DoubleEndedIterator<Item = Indexed<ScriptBuf>> + Clone + '_ {
|
||||
let end = match self.keychain_to_descriptor_id.get(&keychain) {
|
||||
Some(did) => self.last_revealed.get(did).map(|v| *v + 1).unwrap_or(0),
|
||||
None => 0,
|
||||
};
|
||||
@@ -575,8 +577,8 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Not checking the second field of the tuple may result in address reuse.
|
||||
///
|
||||
/// Returns None if the provided `keychain` doesn't exist.
|
||||
pub fn next_index(&self, keychain: &K) -> Option<(u32, bool)> {
|
||||
let did = self.keychain_to_descriptor_id.get(keychain)?;
|
||||
pub fn next_index(&self, keychain: K) -> Option<(u32, bool)> {
|
||||
let did = self.keychain_to_descriptor_id.get(&keychain)?;
|
||||
let last_index = self.last_revealed.get(did).cloned();
|
||||
let descriptor = self.descriptors.get(did).expect("invariant");
|
||||
|
||||
@@ -613,18 +615,18 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
|
||||
/// Get the last derivation index revealed for `keychain`. Returns None if the keychain doesn't
|
||||
/// exist, or if the keychain doesn't have any revealed scripts.
|
||||
pub fn last_revealed_index(&self, keychain: &K) -> Option<u32> {
|
||||
let descriptor_id = self.keychain_to_descriptor_id.get(keychain)?;
|
||||
pub fn last_revealed_index(&self, keychain: K) -> Option<u32> {
|
||||
let descriptor_id = self.keychain_to_descriptor_id.get(&keychain)?;
|
||||
self.last_revealed.get(descriptor_id).cloned()
|
||||
}
|
||||
|
||||
/// Convenience method to call [`Self::reveal_to_target`] on multiple keychains.
|
||||
pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap<K, u32>) -> ChangeSet<K> {
|
||||
pub fn reveal_to_target_multi(&mut self, keychains: &BTreeMap<K, u32>) -> ChangeSet {
|
||||
let mut changeset = ChangeSet::default();
|
||||
|
||||
for (keychain, &index) in keychains {
|
||||
if let Some((_, new_changeset)) = self.reveal_to_target(keychain, index) {
|
||||
changeset.append(new_changeset);
|
||||
if let Some((_, new_changeset)) = self.reveal_to_target(keychain.clone(), index) {
|
||||
changeset.merge(new_changeset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,19 +648,19 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
#[must_use]
|
||||
pub fn reveal_to_target(
|
||||
&mut self,
|
||||
keychain: &K,
|
||||
keychain: K,
|
||||
target_index: u32,
|
||||
) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet<K>)> {
|
||||
) -> Option<(Vec<Indexed<ScriptBuf>>, ChangeSet)> {
|
||||
let mut changeset = ChangeSet::default();
|
||||
let mut spks: Vec<Indexed<ScriptBuf>> = vec![];
|
||||
while let Some((i, new)) = self.next_index(keychain) {
|
||||
while let Some((i, new)) = self.next_index(keychain.clone()) {
|
||||
if !new || i > target_index {
|
||||
break;
|
||||
}
|
||||
match self.reveal_next_spk(keychain) {
|
||||
match self.reveal_next_spk(keychain.clone()) {
|
||||
Some(((i, spk), change)) => {
|
||||
spks.push((i, spk));
|
||||
changeset.append(change);
|
||||
changeset.merge(change);
|
||||
}
|
||||
None => break,
|
||||
}
|
||||
@@ -679,21 +681,21 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// 1. The descriptor has no wildcard and already has one script revealed.
|
||||
/// 2. The descriptor has already revealed scripts up to the numeric bound.
|
||||
/// 3. There is no descriptor associated with the given keychain.
|
||||
pub fn reveal_next_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet<K>)> {
|
||||
let (next_index, new) = self.next_index(keychain)?;
|
||||
pub fn reveal_next_spk(&mut self, keychain: K) -> Option<(Indexed<ScriptBuf>, ChangeSet)> {
|
||||
let (next_index, new) = self.next_index(keychain.clone())?;
|
||||
let mut changeset = ChangeSet::default();
|
||||
|
||||
if new {
|
||||
let did = self.keychain_to_descriptor_id.get(keychain)?;
|
||||
let did = self.keychain_to_descriptor_id.get(&keychain)?;
|
||||
self.last_revealed.insert(*did, next_index);
|
||||
changeset.last_revealed.insert(*did, next_index);
|
||||
self.replenish_inner_index(*did, keychain, self.lookahead);
|
||||
self.replenish_inner_index(*did, &keychain, self.lookahead);
|
||||
}
|
||||
let script = self
|
||||
.inner
|
||||
.spk_at_index(&(keychain.clone(), next_index))
|
||||
.expect("we just inserted it");
|
||||
Some(((next_index, script.into()), changeset))
|
||||
Some(((next_index, script), changeset))
|
||||
}
|
||||
|
||||
/// Gets the next unused script pubkey in the keychain. I.e., the script pubkey with the lowest
|
||||
@@ -709,9 +711,9 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// could be revealed (see [`reveal_next_spk`] for when this happens).
|
||||
///
|
||||
/// [`reveal_next_spk`]: Self::reveal_next_spk
|
||||
pub fn next_unused_spk(&mut self, keychain: &K) -> Option<(Indexed<ScriptBuf>, ChangeSet<K>)> {
|
||||
pub fn next_unused_spk(&mut self, keychain: K) -> Option<(Indexed<ScriptBuf>, ChangeSet)> {
|
||||
let next_unused = self
|
||||
.unused_keychain_spks(keychain)
|
||||
.unused_keychain_spks(keychain.clone())
|
||||
.next()
|
||||
.map(|(i, spk)| ((i, spk.to_owned()), ChangeSet::default()));
|
||||
|
||||
@@ -720,11 +722,11 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
|
||||
/// Iterate over all [`OutPoint`]s that have `TxOut`s with script pubkeys derived from
|
||||
/// `keychain`.
|
||||
pub fn keychain_outpoints<'a>(
|
||||
&'a self,
|
||||
keychain: &'a K,
|
||||
) -> impl DoubleEndedIterator<Item = Indexed<OutPoint>> + 'a {
|
||||
self.keychain_outpoints_in_range(keychain..=keychain)
|
||||
pub fn keychain_outpoints(
|
||||
&self,
|
||||
keychain: K,
|
||||
) -> impl DoubleEndedIterator<Item = Indexed<OutPoint>> + '_ {
|
||||
self.keychain_outpoints_in_range(keychain.clone()..=keychain)
|
||||
.map(|((_, i), op)| (i, op))
|
||||
}
|
||||
|
||||
@@ -755,7 +757,7 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
|
||||
/// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has
|
||||
/// found a [`TxOut`] with it's script pubkey.
|
||||
pub fn last_used_index(&self, keychain: &K) -> Option<u32> {
|
||||
pub fn last_used_index(&self, keychain: K) -> Option<u32> {
|
||||
self.keychain_outpoints(keychain).last().map(|(i, _)| i)
|
||||
}
|
||||
|
||||
@@ -765,33 +767,18 @@ impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
self.keychain_to_descriptor_id
|
||||
.iter()
|
||||
.filter_map(|(keychain, _)| {
|
||||
self.last_used_index(keychain)
|
||||
self.last_used_index(keychain.clone())
|
||||
.map(|index| (keychain.clone(), index))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Applies the `ChangeSet<K>` to the [`KeychainTxOutIndex<K>`]
|
||||
///
|
||||
/// Keychains added by the `keychains_added` field of `ChangeSet<K>` respect the one-to-one
|
||||
/// keychain <-> descriptor invariant by silently ignoring attempts to violate it (but will
|
||||
/// panic if `debug_assertions` are enabled).
|
||||
pub fn apply_changeset(&mut self, changeset: ChangeSet<K>) {
|
||||
let ChangeSet {
|
||||
keychains_added,
|
||||
last_revealed,
|
||||
} = changeset;
|
||||
for (keychain, descriptor) in keychains_added {
|
||||
let _ignore_invariant_violation = self.insert_descriptor(keychain, descriptor);
|
||||
}
|
||||
|
||||
for (&desc_id, &index) in &last_revealed {
|
||||
pub fn apply_changeset(&mut self, changeset: ChangeSet) {
|
||||
for (&desc_id, &index) in &changeset.last_revealed {
|
||||
let v = self.last_revealed.entry(desc_id).or_default();
|
||||
*v = index.max(*v);
|
||||
}
|
||||
|
||||
for did in last_revealed.keys() {
|
||||
self.replenish_inner_index_did(*did, self.lookahead);
|
||||
self.replenish_inner_index_did(desc_id, self.lookahead);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -848,54 +835,28 @@ impl<K: core::fmt::Debug> std::error::Error for InsertDescriptorError<K> {}
|
||||
///
|
||||
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`].
|
||||
///
|
||||
/// The `last_revealed` field is monotone in that [`append`] will never decrease it.
|
||||
/// The `last_revealed` field is monotone in that [`merge`] will never decrease it.
|
||||
/// `keychains_added` is *not* monotone, once it is set any attempt to change it is subject to the
|
||||
/// same *one-to-one* keychain <-> descriptor mapping invariant as [`KeychainTxOutIndex`] itself.
|
||||
///
|
||||
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
|
||||
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
|
||||
/// [`append`]: Self::append
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
/// [`KeychainTxOutIndex`]: crate::keychain_txout::KeychainTxOutIndex
|
||||
/// [`apply_changeset`]: crate::keychain_txout::KeychainTxOutIndex::apply_changeset
|
||||
/// [`merge`]: Self::merge
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(
|
||||
crate = "serde_crate",
|
||||
bound(
|
||||
deserialize = "K: Ord + serde::Deserialize<'de>",
|
||||
serialize = "K: Ord + serde::Serialize"
|
||||
)
|
||||
)
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
#[must_use]
|
||||
pub struct ChangeSet<K> {
|
||||
/// Contains the keychains that have been added and their respective descriptor
|
||||
pub keychains_added: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
|
||||
pub struct ChangeSet {
|
||||
/// Contains for each descriptor_id the last revealed index of derivation
|
||||
pub last_revealed: BTreeMap<DescriptorId, u32>,
|
||||
}
|
||||
|
||||
impl<K: Ord> Append for ChangeSet<K> {
|
||||
/// Merge another [`ChangeSet<K>`] into self.
|
||||
///
|
||||
/// For the `keychains_added` field this method respects the invariants of
|
||||
/// [`insert_descriptor`]. `last_revealed` always becomes the larger of the two.
|
||||
///
|
||||
/// [`insert_descriptor`]: KeychainTxOutIndex::insert_descriptor
|
||||
fn append(&mut self, other: Self) {
|
||||
for (new_keychain, new_descriptor) in other.keychains_added {
|
||||
// enforce 1-to-1 invariance
|
||||
if !self.keychains_added.contains_key(&new_keychain)
|
||||
// FIXME: very inefficient
|
||||
&& self
|
||||
.keychains_added
|
||||
.values()
|
||||
.all(|descriptor| descriptor != &new_descriptor)
|
||||
{
|
||||
self.keychains_added.insert(new_keychain, new_descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
impl Merge for ChangeSet {
|
||||
/// Merge another [`ChangeSet`] into self.
|
||||
fn merge(&mut self, other: Self) {
|
||||
// for `last_revealed`, entries of `other` will take precedence ONLY if it is greater than
|
||||
// what was originally in `self`.
|
||||
for (desc_id, index) in other.last_revealed {
|
||||
@@ -915,25 +876,6 @@ impl<K: Ord> Append for ChangeSet<K> {
|
||||
|
||||
/// Returns whether the changeset are empty.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.last_revealed.is_empty() && self.keychains_added.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Default for ChangeSet<K> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
last_revealed: BTreeMap::default(),
|
||||
keychains_added: BTreeMap::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
/// The keychain doesn't exist. Most likley hasn't been inserted with [`KeychainTxOutIndex::insert_descriptor`].
|
||||
pub struct NoSuchKeychain<K>(K);
|
||||
|
||||
impl<K: Debug> core::fmt::Display for NoSuchKeychain<K> {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(f, "no such keychain {:?} exists", &self.0)
|
||||
self.last_revealed.is_empty()
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
//! [`SpkTxOutIndex`] is an index storing [`TxOut`]s that have a script pubkey that matches those in a list.
|
||||
|
||||
use core::ops::RangeBounds;
|
||||
|
||||
use crate::{
|
||||
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
|
||||
indexed_tx_graph::Indexer,
|
||||
Indexer,
|
||||
};
|
||||
use bitcoin::{Amount, OutPoint, Script, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
|
||||
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
|
||||
|
||||
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
|
||||
///
|
||||
@@ -82,7 +84,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
/// Typically, this is used in two situations:
|
||||
///
|
||||
/// 1. After loading transaction data from the disk, you may scan over all the txouts to restore all
|
||||
/// your txouts.
|
||||
/// your txouts.
|
||||
/// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state.
|
||||
pub fn scan(&mut self, tx: &Transaction) -> BTreeSet<I> {
|
||||
let mut scanned_indices = BTreeSet::new();
|
||||
@@ -174,8 +176,8 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
/// Returns the script that has been inserted at the `index`.
|
||||
///
|
||||
/// If that index hasn't been inserted yet, it will return `None`.
|
||||
pub fn spk_at_index(&self, index: &I) -> Option<&Script> {
|
||||
self.spks.get(index).map(|s| s.as_script())
|
||||
pub fn spk_at_index(&self, index: &I) -> Option<ScriptBuf> {
|
||||
self.spks.get(index).cloned()
|
||||
}
|
||||
|
||||
/// The script pubkeys that are being tracked by the index.
|
||||
@@ -206,7 +208,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bdk_chain::SpkTxOutIndex;
|
||||
/// # use bdk_chain::spk_txout::SpkTxOutIndex;
|
||||
///
|
||||
/// // imagine our spks are indexed like (keychain, derivation_index).
|
||||
/// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();
|
||||
@@ -215,7 +217,10 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
/// let unused_change_spks =
|
||||
/// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
|
||||
/// ```
|
||||
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)> + Clone
|
||||
pub fn unused_spks<R>(
|
||||
&self,
|
||||
range: R,
|
||||
) -> impl DoubleEndedIterator<Item = (&I, ScriptBuf)> + Clone + '_
|
||||
where
|
||||
R: RangeBounds<I>,
|
||||
{
|
||||
@@ -266,8 +271,8 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
}
|
||||
|
||||
/// Returns the index associated with the script pubkey.
|
||||
pub fn index_of_spk(&self, script: &Script) -> Option<&I> {
|
||||
self.spk_indices.get(script)
|
||||
pub fn index_of_spk(&self, script: ScriptBuf) -> Option<&I> {
|
||||
self.spk_indices.get(script.as_script())
|
||||
}
|
||||
|
||||
/// Computes the total value transfer effect `tx` has on the script pubkeys in `range`. Value is
|
||||
@@ -291,7 +296,7 @@ impl<I: Clone + Ord + core::fmt::Debug> SpkTxOutIndex<I> {
|
||||
}
|
||||
}
|
||||
for txout in &tx.output {
|
||||
if let Some(index) = self.index_of_spk(&txout.script_pubkey) {
|
||||
if let Some(index) = self.index_of_spk(txout.script_pubkey.clone()) {
|
||||
if range.contains(index) {
|
||||
received += txout.value;
|
||||
}
|
||||
@@ -21,14 +21,15 @@
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub use bitcoin;
|
||||
mod spk_txout_index;
|
||||
pub use spk_txout_index::*;
|
||||
mod balance;
|
||||
pub use balance::*;
|
||||
mod chain_data;
|
||||
pub use chain_data::*;
|
||||
pub mod indexed_tx_graph;
|
||||
pub use indexed_tx_graph::IndexedTxGraph;
|
||||
pub mod keychain;
|
||||
pub use keychain::{Indexed, KeychainIndexed};
|
||||
pub mod indexer;
|
||||
pub use indexer::spk_txout;
|
||||
pub use indexer::Indexer;
|
||||
pub mod local_chain;
|
||||
mod tx_data_traits;
|
||||
pub mod tx_graph;
|
||||
@@ -36,6 +37,8 @@ pub use tx_data_traits::*;
|
||||
pub use tx_graph::TxGraph;
|
||||
mod chain_oracle;
|
||||
pub use chain_oracle::*;
|
||||
mod persist;
|
||||
pub use persist::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod example_utils;
|
||||
@@ -49,15 +52,18 @@ pub use descriptor_ext::{DescriptorExt, DescriptorId};
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod spk_iter;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use indexer::keychain_txout;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use spk_iter::*;
|
||||
mod changeset;
|
||||
pub use changeset::*;
|
||||
#[cfg(feature = "rusqlite")]
|
||||
pub mod rusqlite_impl;
|
||||
pub mod spk_client;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(feature = "rusqlite")]
|
||||
pub extern crate rusqlite_crate as rusqlite;
|
||||
#[cfg(feature = "serde")]
|
||||
pub extern crate serde_crate as serde;
|
||||
|
||||
@@ -98,3 +104,25 @@ pub mod collections {
|
||||
|
||||
/// How many confirmations are needed f or a coinbase output to be spent.
|
||||
pub const COINBASE_MATURITY: u32 = 100;
|
||||
|
||||
/// A tuple of keychain index and `T` representing the indexed value.
|
||||
pub type Indexed<T> = (u32, T);
|
||||
/// A tuple of keychain `K`, derivation index (`u32`) and a `T` associated with them.
|
||||
pub type KeychainIndexed<K, T> = ((K, u32), T);
|
||||
|
||||
/// A wrapper that we use to impl remote traits for types in our crate or dependency crates.
|
||||
pub struct Impl<T>(pub T);
|
||||
|
||||
impl<T> From<T> for Impl<T> {
|
||||
fn from(value: T) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> core::ops::Deref for Impl<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,11 @@ use core::convert::Infallible;
|
||||
use core::ops::RangeBounds;
|
||||
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::{BlockId, ChainOracle};
|
||||
use crate::{BlockId, ChainOracle, Merge};
|
||||
use alloc::sync::Arc;
|
||||
use bitcoin::block::Header;
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
/// The [`ChangeSet`] represents changes to [`LocalChain`].
|
||||
///
|
||||
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
|
||||
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
|
||||
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
|
||||
|
||||
/// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a
|
||||
/// transaction anchor.
|
||||
///
|
||||
@@ -216,7 +210,7 @@ impl CheckPoint {
|
||||
|
||||
/// Apply `changeset` to the checkpoint.
|
||||
fn apply_changeset(mut self, changeset: &ChangeSet) -> Result<CheckPoint, MissingGenesisError> {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
if let Some(start_height) = changeset.blocks.keys().next().cloned() {
|
||||
// changes after point of agreement
|
||||
let mut extension = BTreeMap::default();
|
||||
// point of agreement
|
||||
@@ -231,7 +225,7 @@ impl CheckPoint {
|
||||
}
|
||||
}
|
||||
|
||||
for (&height, &hash) in changeset {
|
||||
for (&height, &hash) in &changeset.blocks {
|
||||
match hash {
|
||||
Some(hash) => {
|
||||
extension.insert(height, hash);
|
||||
@@ -331,7 +325,7 @@ impl LocalChain {
|
||||
|
||||
/// Construct a [`LocalChain`] from an initial `changeset`.
|
||||
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
|
||||
let genesis_entry = changeset.get(&0).copied().flatten();
|
||||
let genesis_entry = changeset.blocks.get(&0).copied().flatten();
|
||||
let genesis_hash = match genesis_entry {
|
||||
Some(hash) => hash,
|
||||
None => return Err(MissingGenesisError),
|
||||
@@ -521,12 +515,14 @@ impl LocalChain {
|
||||
}
|
||||
|
||||
let mut changeset = ChangeSet::default();
|
||||
changeset.insert(block_id.height, Some(block_id.hash));
|
||||
changeset
|
||||
.blocks
|
||||
.insert(block_id.height, Some(block_id.hash));
|
||||
self.apply_changeset(&changeset)
|
||||
.map_err(|_| AlterCheckPointError {
|
||||
height: 0,
|
||||
original_hash: self.genesis_hash(),
|
||||
update_hash: changeset.get(&0).cloned().flatten(),
|
||||
update_hash: changeset.blocks.get(&0).cloned().flatten(),
|
||||
})?;
|
||||
Ok(changeset)
|
||||
}
|
||||
@@ -548,7 +544,7 @@ impl LocalChain {
|
||||
if cp_id.height < block_id.height {
|
||||
break;
|
||||
}
|
||||
changeset.insert(cp_id.height, None);
|
||||
changeset.blocks.insert(cp_id.height, None);
|
||||
if cp_id == block_id {
|
||||
remove_from = Some(cp);
|
||||
}
|
||||
@@ -569,13 +565,16 @@ impl LocalChain {
|
||||
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
||||
/// recover the current chain.
|
||||
pub fn initial_changeset(&self) -> ChangeSet {
|
||||
self.tip
|
||||
.iter()
|
||||
.map(|cp| {
|
||||
let block_id = cp.block_id();
|
||||
(block_id.height, Some(block_id.hash))
|
||||
})
|
||||
.collect()
|
||||
ChangeSet {
|
||||
blocks: self
|
||||
.tip
|
||||
.iter()
|
||||
.map(|cp| {
|
||||
let block_id = cp.block_id();
|
||||
(block_id.height, Some(block_id.hash))
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over checkpoints in descending height order.
|
||||
@@ -587,7 +586,7 @@ impl LocalChain {
|
||||
|
||||
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
||||
let mut curr_cp = self.tip.clone();
|
||||
for (height, exp_hash) in changeset.iter().rev() {
|
||||
for (height, exp_hash) in changeset.blocks.iter().rev() {
|
||||
match curr_cp.get(*height) {
|
||||
Some(query_cp) => {
|
||||
if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
|
||||
@@ -630,6 +629,58 @@ impl LocalChain {
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`ChangeSet`] represents changes to [`LocalChain`].
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ChangeSet {
|
||||
/// Changes to the [`LocalChain`] blocks.
|
||||
///
|
||||
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
|
||||
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
|
||||
pub blocks: BTreeMap<u32, Option<BlockHash>>,
|
||||
}
|
||||
|
||||
impl Merge for ChangeSet {
|
||||
fn merge(&mut self, other: Self) {
|
||||
Merge::merge(&mut self.blocks, other.blocks)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.blocks.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: IntoIterator<Item = (u32, Option<BlockHash>)>> From<B> for ChangeSet {
|
||||
fn from(blocks: B) -> Self {
|
||||
Self {
|
||||
blocks: blocks.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(u32, Option<BlockHash>)> for ChangeSet {
|
||||
fn from_iter<T: IntoIterator<Item = (u32, Option<BlockHash>)>>(iter: T) -> Self {
|
||||
Self {
|
||||
blocks: iter.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(u32, BlockHash)> for ChangeSet {
|
||||
fn from_iter<T: IntoIterator<Item = (u32, BlockHash)>>(iter: T) -> Self {
|
||||
Self {
|
||||
blocks: iter
|
||||
.into_iter()
|
||||
.map(|(height, hash)| (height, Some(hash)))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MissingGenesisError;
|
||||
@@ -761,7 +812,7 @@ fn merge_chains(
|
||||
match (curr_orig.as_ref(), curr_update.as_ref()) {
|
||||
// Update block that doesn't exist in the original chain
|
||||
(o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => {
|
||||
changeset.insert(u.height(), Some(u.hash()));
|
||||
changeset.blocks.insert(u.height(), Some(u.hash()));
|
||||
prev_update = curr_update.take();
|
||||
}
|
||||
// Original block that isn't in the update
|
||||
@@ -813,9 +864,9 @@ fn merge_chains(
|
||||
} else {
|
||||
// We have an invalidation height so we set the height to the updated hash and
|
||||
// also purge all the original chain block hashes above this block.
|
||||
changeset.insert(u.height(), Some(u.hash()));
|
||||
changeset.blocks.insert(u.height(), Some(u.hash()));
|
||||
for invalidated_height in potentially_invalidated_heights.drain(..) {
|
||||
changeset.insert(invalidated_height, None);
|
||||
changeset.blocks.insert(invalidated_height, None);
|
||||
}
|
||||
prev_orig_was_invalidated = true;
|
||||
}
|
||||
|
||||
169
crates/chain/src/persist.rs
Normal file
169
crates/chain/src/persist.rs
Normal file
@@ -0,0 +1,169 @@
|
||||
use core::{
|
||||
future::Future,
|
||||
ops::{Deref, DerefMut},
|
||||
pin::Pin,
|
||||
};
|
||||
|
||||
use alloc::boxed::Box;
|
||||
|
||||
use crate::Merge;
|
||||
|
||||
/// Represents a type that contains staged changes.
|
||||
pub trait Staged {
|
||||
/// Type for staged changes.
|
||||
type ChangeSet: Merge;
|
||||
|
||||
/// Get mutable reference of staged changes.
|
||||
fn staged(&mut self) -> &mut Self::ChangeSet;
|
||||
}
|
||||
|
||||
/// Trait that persists the type with `Db`.
|
||||
///
|
||||
/// Methods of this trait should not be called directly.
|
||||
pub trait PersistWith<Db>: Staged + Sized {
|
||||
/// Parameters for [`PersistWith::create`].
|
||||
type CreateParams;
|
||||
/// Parameters for [`PersistWith::load`].
|
||||
type LoadParams;
|
||||
/// Error type of [`PersistWith::create`].
|
||||
type CreateError;
|
||||
/// Error type of [`PersistWith::load`].
|
||||
type LoadError;
|
||||
/// Error type of [`PersistWith::persist`].
|
||||
type PersistError;
|
||||
|
||||
/// Initialize the `Db` and create `Self`.
|
||||
fn create(db: &mut Db, params: Self::CreateParams) -> Result<Self, Self::CreateError>;
|
||||
|
||||
/// Initialize the `Db` and load a previously-persisted `Self`.
|
||||
fn load(db: &mut Db, params: Self::LoadParams) -> Result<Option<Self>, Self::LoadError>;
|
||||
|
||||
/// Persist changes to the `Db`.
|
||||
fn persist(
|
||||
db: &mut Db,
|
||||
changeset: &<Self as Staged>::ChangeSet,
|
||||
) -> Result<(), Self::PersistError>;
|
||||
}
|
||||
|
||||
type FutureResult<'a, T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'a>>;
|
||||
|
||||
/// Trait that persists the type with an async `Db`.
|
||||
pub trait PersistAsyncWith<Db>: Staged + Sized {
|
||||
/// Parameters for [`PersistAsyncWith::create`].
|
||||
type CreateParams;
|
||||
/// Parameters for [`PersistAsyncWith::load`].
|
||||
type LoadParams;
|
||||
/// Error type of [`PersistAsyncWith::create`].
|
||||
type CreateError;
|
||||
/// Error type of [`PersistAsyncWith::load`].
|
||||
type LoadError;
|
||||
/// Error type of [`PersistAsyncWith::persist`].
|
||||
type PersistError;
|
||||
|
||||
/// Initialize the `Db` and create `Self`.
|
||||
fn create(db: &mut Db, params: Self::CreateParams) -> FutureResult<Self, Self::CreateError>;
|
||||
|
||||
/// Initialize the `Db` and load a previously-persisted `Self`.
|
||||
fn load(db: &mut Db, params: Self::LoadParams) -> FutureResult<Option<Self>, Self::LoadError>;
|
||||
|
||||
/// Persist changes to the `Db`.
|
||||
fn persist<'a>(
|
||||
db: &'a mut Db,
|
||||
changeset: &'a <Self as Staged>::ChangeSet,
|
||||
) -> FutureResult<'a, (), Self::PersistError>;
|
||||
}
|
||||
|
||||
/// Represents a persisted `T`.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Persisted<T> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T> Persisted<T> {
|
||||
/// Create a new persisted `T`.
|
||||
pub fn create<Db>(db: &mut Db, params: T::CreateParams) -> Result<Self, T::CreateError>
|
||||
where
|
||||
T: PersistWith<Db>,
|
||||
{
|
||||
T::create(db, params).map(|inner| Self { inner })
|
||||
}
|
||||
|
||||
/// Create a new persisted `T` with async `Db`.
|
||||
pub async fn create_async<Db>(
|
||||
db: &mut Db,
|
||||
params: T::CreateParams,
|
||||
) -> Result<Self, T::CreateError>
|
||||
where
|
||||
T: PersistAsyncWith<Db>,
|
||||
{
|
||||
T::create(db, params).await.map(|inner| Self { inner })
|
||||
}
|
||||
|
||||
/// Construct a persisted `T` from `Db`.
|
||||
pub fn load<Db>(db: &mut Db, params: T::LoadParams) -> Result<Option<Self>, T::LoadError>
|
||||
where
|
||||
T: PersistWith<Db>,
|
||||
{
|
||||
Ok(T::load(db, params)?.map(|inner| Self { inner }))
|
||||
}
|
||||
|
||||
/// Construct a persisted `T` from an async `Db`.
|
||||
pub async fn load_async<Db>(
|
||||
db: &mut Db,
|
||||
params: T::LoadParams,
|
||||
) -> Result<Option<Self>, T::LoadError>
|
||||
where
|
||||
T: PersistAsyncWith<Db>,
|
||||
{
|
||||
Ok(T::load(db, params).await?.map(|inner| Self { inner }))
|
||||
}
|
||||
|
||||
/// Persist staged changes of `T` into `Db`.
|
||||
///
|
||||
/// If the database errors, the staged changes will not be cleared.
|
||||
pub fn persist<Db>(&mut self, db: &mut Db) -> Result<bool, T::PersistError>
|
||||
where
|
||||
T: PersistWith<Db>,
|
||||
{
|
||||
let stage = T::staged(&mut self.inner);
|
||||
if stage.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
T::persist(db, &*stage)?;
|
||||
stage.take();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Persist staged changes of `T` into an async `Db`.
|
||||
///
|
||||
/// If the database errors, the staged changes will not be cleared.
|
||||
pub async fn persist_async<'a, Db>(
|
||||
&'a mut self,
|
||||
db: &'a mut Db,
|
||||
) -> Result<bool, T::PersistError>
|
||||
where
|
||||
T: PersistAsyncWith<Db>,
|
||||
{
|
||||
let stage = T::staged(&mut self.inner);
|
||||
if stage.is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
T::persist(db, &*stage).await?;
|
||||
stage.take();
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Persisted<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DerefMut for Persisted<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
530
crates/chain/src/rusqlite_impl.rs
Normal file
530
crates/chain/src/rusqlite_impl.rs
Normal file
@@ -0,0 +1,530 @@
|
||||
//! Module for stuff
|
||||
|
||||
use crate::*;
|
||||
use core::str::FromStr;
|
||||
|
||||
use alloc::{borrow::ToOwned, boxed::Box, string::ToString, sync::Arc, vec::Vec};
|
||||
use bitcoin::consensus::{Decodable, Encodable};
|
||||
use rusqlite;
|
||||
use rusqlite::named_params;
|
||||
use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSql, ToSqlOutput, ValueRef};
|
||||
use rusqlite::OptionalExtension;
|
||||
use rusqlite::Transaction;
|
||||
|
||||
/// Table name for schemas.
|
||||
pub const SCHEMAS_TABLE_NAME: &str = "bdk_schemas";
|
||||
|
||||
/// Initialize the schema table.
|
||||
fn init_schemas_table(db_tx: &Transaction) -> rusqlite::Result<()> {
|
||||
let sql = format!("CREATE TABLE IF NOT EXISTS {}( name TEXT PRIMARY KEY NOT NULL, version INTEGER NOT NULL ) STRICT", SCHEMAS_TABLE_NAME);
|
||||
db_tx.execute(&sql, ())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get schema version of `schema_name`.
|
||||
fn schema_version(db_tx: &Transaction, schema_name: &str) -> rusqlite::Result<Option<u32>> {
|
||||
let sql = format!(
|
||||
"SELECT version FROM {} WHERE name=:name",
|
||||
SCHEMAS_TABLE_NAME
|
||||
);
|
||||
db_tx
|
||||
.query_row(&sql, named_params! { ":name": schema_name }, |row| {
|
||||
row.get::<_, u32>("version")
|
||||
})
|
||||
.optional()
|
||||
}
|
||||
|
||||
/// Set the `schema_version` of `schema_name`.
|
||||
fn set_schema_version(
|
||||
db_tx: &Transaction,
|
||||
schema_name: &str,
|
||||
schema_version: u32,
|
||||
) -> rusqlite::Result<()> {
|
||||
let sql = format!(
|
||||
"REPLACE INTO {}(name, version) VALUES(:name, :version)",
|
||||
SCHEMAS_TABLE_NAME,
|
||||
);
|
||||
db_tx.execute(
|
||||
&sql,
|
||||
named_params! { ":name": schema_name, ":version": schema_version },
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs logic that initializes/migrates the table schemas.
|
||||
pub fn migrate_schema(
|
||||
db_tx: &Transaction,
|
||||
schema_name: &str,
|
||||
versioned_scripts: &[&[&str]],
|
||||
) -> rusqlite::Result<()> {
|
||||
init_schemas_table(db_tx)?;
|
||||
let current_version = schema_version(db_tx, schema_name)?;
|
||||
let exec_from = current_version.map_or(0_usize, |v| v as usize + 1);
|
||||
let scripts_to_exec = versioned_scripts.iter().enumerate().skip(exec_from);
|
||||
for (version, &script) in scripts_to_exec {
|
||||
set_schema_version(db_tx, schema_name, version as u32)?;
|
||||
for statement in script {
|
||||
db_tx.execute(statement, ())?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl FromSql for Impl<bitcoin::Txid> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
bitcoin::Txid::from_str(value.as_str()?)
|
||||
.map(Self)
|
||||
.map_err(from_sql_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Impl<bitcoin::Txid> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
Ok(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Impl<bitcoin::BlockHash> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
bitcoin::BlockHash::from_str(value.as_str()?)
|
||||
.map(Self)
|
||||
.map_err(from_sql_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Impl<bitcoin::BlockHash> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
Ok(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl FromSql for Impl<DescriptorId> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
DescriptorId::from_str(value.as_str()?)
|
||||
.map(Self)
|
||||
.map_err(from_sql_error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl ToSql for Impl<DescriptorId> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
Ok(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Impl<bitcoin::Transaction> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
bitcoin::Transaction::consensus_decode_from_finite_reader(&mut value.as_bytes()?)
|
||||
.map(Self)
|
||||
.map_err(from_sql_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Impl<bitcoin::Transaction> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
let mut bytes = Vec::<u8>::new();
|
||||
self.consensus_encode(&mut bytes).map_err(to_sql_error)?;
|
||||
Ok(bytes.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Impl<bitcoin::ScriptBuf> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
Ok(bitcoin::Script::from_bytes(value.as_bytes()?)
|
||||
.to_owned()
|
||||
.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Impl<bitcoin::ScriptBuf> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
Ok(self.as_bytes().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Impl<bitcoin::Amount> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
Ok(bitcoin::Amount::from_sat(value.as_i64()?.try_into().map_err(from_sql_error)?).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Impl<bitcoin::Amount> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
let amount: i64 = self.to_sat().try_into().map_err(to_sql_error)?;
|
||||
Ok(amount.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor + serde_crate::de::DeserializeOwned> FromSql for Impl<A> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
serde_json::from_str(value.as_str()?)
|
||||
.map(Impl)
|
||||
.map_err(from_sql_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor + serde_crate::Serialize> ToSql for Impl<A> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
serde_json::to_string(&self.0)
|
||||
.map(Into::into)
|
||||
.map_err(to_sql_error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl FromSql for Impl<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
miniscript::Descriptor::from_str(value.as_str()?)
|
||||
.map(Self)
|
||||
.map_err(from_sql_error)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl ToSql for Impl<miniscript::Descriptor<miniscript::DescriptorPublicKey>> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
Ok(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromSql for Impl<bitcoin::Network> {
|
||||
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
|
||||
bitcoin::Network::from_str(value.as_str()?)
|
||||
.map(Self)
|
||||
.map_err(from_sql_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToSql for Impl<bitcoin::Network> {
|
||||
fn to_sql(&self) -> rusqlite::Result<ToSqlOutput<'_>> {
|
||||
Ok(self.to_string().into())
|
||||
}
|
||||
}
|
||||
|
||||
fn from_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> FromSqlError {
|
||||
FromSqlError::Other(Box::new(err))
|
||||
}
|
||||
|
||||
fn to_sql_error<E: std::error::Error + Send + Sync + 'static>(err: E) -> rusqlite::Error {
|
||||
rusqlite::Error::ToSqlConversionFailure(Box::new(err))
|
||||
}
|
||||
|
||||
impl<A> tx_graph::ChangeSet<A>
|
||||
where
|
||||
A: Anchor + Clone + Ord + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
/// Schema name for [`tx_graph::ChangeSet`].
|
||||
pub const SCHEMA_NAME: &'static str = "bdk_txgraph";
|
||||
/// Name of table that stores full transactions and `last_seen` timestamps.
|
||||
pub const TXS_TABLE_NAME: &'static str = "bdk_txs";
|
||||
/// Name of table that stores floating txouts.
|
||||
pub const TXOUTS_TABLE_NAME: &'static str = "bdk_txouts";
|
||||
/// Name of table that stores [`Anchor`]s.
|
||||
pub const ANCHORS_TABLE_NAME: &'static str = "bdk_anchors";
|
||||
|
||||
/// Initialize sqlite tables.
|
||||
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
|
||||
let schema_v0: &[&str] = &[
|
||||
// full transactions
|
||||
&format!(
|
||||
"CREATE TABLE {} ( \
|
||||
txid TEXT PRIMARY KEY NOT NULL, \
|
||||
raw_tx BLOB, \
|
||||
last_seen INTEGER \
|
||||
) STRICT",
|
||||
Self::TXS_TABLE_NAME,
|
||||
),
|
||||
// floating txouts
|
||||
&format!(
|
||||
"CREATE TABLE {} ( \
|
||||
txid TEXT NOT NULL, \
|
||||
vout INTEGER NOT NULL, \
|
||||
value INTEGER NOT NULL, \
|
||||
script BLOB NOT NULL, \
|
||||
PRIMARY KEY (txid, vout) \
|
||||
) STRICT",
|
||||
Self::TXOUTS_TABLE_NAME,
|
||||
),
|
||||
// anchors
|
||||
&format!(
|
||||
"CREATE TABLE {} ( \
|
||||
txid TEXT NOT NULL REFERENCES {} (txid), \
|
||||
block_height INTEGER NOT NULL, \
|
||||
block_hash TEXT NOT NULL, \
|
||||
anchor BLOB NOT NULL, \
|
||||
PRIMARY KEY (txid, block_height, block_hash) \
|
||||
) STRICT",
|
||||
Self::ANCHORS_TABLE_NAME,
|
||||
Self::TXS_TABLE_NAME,
|
||||
),
|
||||
];
|
||||
migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
|
||||
}
|
||||
|
||||
/// Construct a [`TxGraph`] from an sqlite database.
|
||||
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
|
||||
Self::init_sqlite_tables(db_tx)?;
|
||||
|
||||
let mut changeset = Self::default();
|
||||
|
||||
let mut statement = db_tx.prepare(&format!(
|
||||
"SELECT txid, raw_tx, last_seen FROM {}",
|
||||
Self::TXS_TABLE_NAME,
|
||||
))?;
|
||||
let row_iter = statement.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, Impl<bitcoin::Txid>>("txid")?,
|
||||
row.get::<_, Option<Impl<bitcoin::Transaction>>>("raw_tx")?,
|
||||
row.get::<_, Option<u64>>("last_seen")?,
|
||||
))
|
||||
})?;
|
||||
for row in row_iter {
|
||||
let (Impl(txid), tx, last_seen) = row?;
|
||||
if let Some(Impl(tx)) = tx {
|
||||
changeset.txs.insert(Arc::new(tx));
|
||||
}
|
||||
if let Some(last_seen) = last_seen {
|
||||
changeset.last_seen.insert(txid, last_seen);
|
||||
}
|
||||
}
|
||||
|
||||
let mut statement = db_tx.prepare(&format!(
|
||||
"SELECT txid, vout, value, script FROM {}",
|
||||
Self::TXOUTS_TABLE_NAME,
|
||||
))?;
|
||||
let row_iter = statement.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, Impl<bitcoin::Txid>>("txid")?,
|
||||
row.get::<_, u32>("vout")?,
|
||||
row.get::<_, Impl<bitcoin::Amount>>("value")?,
|
||||
row.get::<_, Impl<bitcoin::ScriptBuf>>("script")?,
|
||||
))
|
||||
})?;
|
||||
for row in row_iter {
|
||||
let (Impl(txid), vout, Impl(value), Impl(script_pubkey)) = row?;
|
||||
changeset.txouts.insert(
|
||||
bitcoin::OutPoint { txid, vout },
|
||||
bitcoin::TxOut {
|
||||
value,
|
||||
script_pubkey,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let mut statement = db_tx.prepare(&format!(
|
||||
"SELECT json(anchor), txid FROM {}",
|
||||
Self::ANCHORS_TABLE_NAME,
|
||||
))?;
|
||||
let row_iter = statement.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, Impl<A>>("json(anchor)")?,
|
||||
row.get::<_, Impl<bitcoin::Txid>>("txid")?,
|
||||
))
|
||||
})?;
|
||||
for row in row_iter {
|
||||
let (Impl(anchor), Impl(txid)) = row?;
|
||||
changeset.anchors.insert((anchor, txid));
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Persist `changeset` to the sqlite database.
|
||||
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
|
||||
Self::init_sqlite_tables(db_tx)?;
|
||||
|
||||
let mut statement = db_tx.prepare_cached(&format!(
|
||||
"INSERT INTO {}(txid, raw_tx) VALUES(:txid, :raw_tx) ON CONFLICT(txid) DO UPDATE SET raw_tx=:raw_tx",
|
||||
Self::TXS_TABLE_NAME,
|
||||
))?;
|
||||
for tx in &self.txs {
|
||||
statement.execute(named_params! {
|
||||
":txid": Impl(tx.compute_txid()),
|
||||
":raw_tx": Impl(tx.as_ref().clone()),
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut statement = db_tx
|
||||
.prepare_cached(&format!(
|
||||
"INSERT INTO {}(txid, last_seen) VALUES(:txid, :last_seen) ON CONFLICT(txid) DO UPDATE SET last_seen=:last_seen",
|
||||
Self::TXS_TABLE_NAME,
|
||||
))?;
|
||||
for (&txid, &last_seen) in &self.last_seen {
|
||||
statement.execute(named_params! {
|
||||
":txid": Impl(txid),
|
||||
":last_seen": Some(last_seen),
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut statement = db_tx.prepare_cached(&format!(
|
||||
"REPLACE INTO {}(txid, vout, value, script) VALUES(:txid, :vout, :value, :script)",
|
||||
Self::TXOUTS_TABLE_NAME,
|
||||
))?;
|
||||
for (op, txo) in &self.txouts {
|
||||
statement.execute(named_params! {
|
||||
":txid": Impl(op.txid),
|
||||
":vout": op.vout,
|
||||
":value": Impl(txo.value),
|
||||
":script": Impl(txo.script_pubkey.clone()),
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut statement = db_tx.prepare_cached(&format!(
|
||||
"REPLACE INTO {}(txid, block_height, block_hash, anchor) VALUES(:txid, :block_height, :block_hash, jsonb(:anchor))",
|
||||
Self::ANCHORS_TABLE_NAME,
|
||||
))?;
|
||||
for (anchor, txid) in &self.anchors {
|
||||
let anchor_block = anchor.anchor_block();
|
||||
statement.execute(named_params! {
|
||||
":txid": Impl(*txid),
|
||||
":block_height": anchor_block.height,
|
||||
":block_hash": Impl(anchor_block.hash),
|
||||
":anchor": Impl(anchor.clone()),
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl local_chain::ChangeSet {
|
||||
/// Schema name for the changeset.
|
||||
pub const SCHEMA_NAME: &'static str = "bdk_localchain";
|
||||
/// Name of sqlite table that stores blocks of [`LocalChain`](local_chain::LocalChain).
|
||||
pub const BLOCKS_TABLE_NAME: &'static str = "bdk_blocks";
|
||||
|
||||
/// Initialize sqlite tables for persisting [`local_chain::LocalChain`].
|
||||
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
|
||||
let schema_v0: &[&str] = &[
|
||||
// blocks
|
||||
&format!(
|
||||
"CREATE TABLE {} ( \
|
||||
block_height INTEGER PRIMARY KEY NOT NULL, \
|
||||
block_hash TEXT NOT NULL \
|
||||
) STRICT",
|
||||
Self::BLOCKS_TABLE_NAME,
|
||||
),
|
||||
];
|
||||
migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
|
||||
}
|
||||
|
||||
/// Construct a [`LocalChain`](local_chain::LocalChain) from sqlite database.
|
||||
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
|
||||
Self::init_sqlite_tables(db_tx)?;
|
||||
|
||||
let mut changeset = Self::default();
|
||||
|
||||
let mut statement = db_tx.prepare(&format!(
|
||||
"SELECT block_height, block_hash FROM {}",
|
||||
Self::BLOCKS_TABLE_NAME,
|
||||
))?;
|
||||
let row_iter = statement.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, u32>("block_height")?,
|
||||
row.get::<_, Impl<bitcoin::BlockHash>>("block_hash")?,
|
||||
))
|
||||
})?;
|
||||
for row in row_iter {
|
||||
let (height, Impl(hash)) = row?;
|
||||
changeset.blocks.insert(height, Some(hash));
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Persist `changeset` to the sqlite database.
|
||||
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
|
||||
Self::init_sqlite_tables(db_tx)?;
|
||||
|
||||
let mut replace_statement = db_tx.prepare_cached(&format!(
|
||||
"REPLACE INTO {}(block_height, block_hash) VALUES(:block_height, :block_hash)",
|
||||
Self::BLOCKS_TABLE_NAME,
|
||||
))?;
|
||||
let mut delete_statement = db_tx.prepare_cached(&format!(
|
||||
"DELETE FROM {} WHERE block_height=:block_height",
|
||||
Self::BLOCKS_TABLE_NAME,
|
||||
))?;
|
||||
for (&height, &hash) in &self.blocks {
|
||||
match hash {
|
||||
Some(hash) => replace_statement.execute(named_params! {
|
||||
":block_height": height,
|
||||
":block_hash": Impl(hash),
|
||||
})?,
|
||||
None => delete_statement.execute(named_params! {
|
||||
":block_height": height,
|
||||
})?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
impl keychain_txout::ChangeSet {
|
||||
/// Schema name for the changeset.
|
||||
pub const SCHEMA_NAME: &'static str = "bdk_keychaintxout";
|
||||
/// Name for table that stores last revealed indices per descriptor id.
|
||||
pub const LAST_REVEALED_TABLE_NAME: &'static str = "bdk_descriptor_last_revealed";
|
||||
|
||||
/// Initialize sqlite tables for persisting
|
||||
/// [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex).
|
||||
fn init_sqlite_tables(db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
|
||||
let schema_v0: &[&str] = &[
|
||||
// last revealed
|
||||
&format!(
|
||||
"CREATE TABLE {} ( \
|
||||
descriptor_id TEXT PRIMARY KEY NOT NULL, \
|
||||
last_revealed INTEGER NOT NULL \
|
||||
) STRICT",
|
||||
Self::LAST_REVEALED_TABLE_NAME,
|
||||
),
|
||||
];
|
||||
migrate_schema(db_tx, Self::SCHEMA_NAME, &[schema_v0])
|
||||
}
|
||||
|
||||
/// Construct [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex) from sqlite database
|
||||
/// and given parameters.
|
||||
pub fn from_sqlite(db_tx: &rusqlite::Transaction) -> rusqlite::Result<Self> {
|
||||
Self::init_sqlite_tables(db_tx)?;
|
||||
|
||||
let mut changeset = Self::default();
|
||||
|
||||
let mut statement = db_tx.prepare(&format!(
|
||||
"SELECT descriptor_id, last_revealed FROM {}",
|
||||
Self::LAST_REVEALED_TABLE_NAME,
|
||||
))?;
|
||||
let row_iter = statement.query_map([], |row| {
|
||||
Ok((
|
||||
row.get::<_, Impl<DescriptorId>>("descriptor_id")?,
|
||||
row.get::<_, u32>("last_revealed")?,
|
||||
))
|
||||
})?;
|
||||
for row in row_iter {
|
||||
let (Impl(descriptor_id), last_revealed) = row?;
|
||||
changeset.last_revealed.insert(descriptor_id, last_revealed);
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Persist `changeset` to the sqlite database.
|
||||
pub fn persist_to_sqlite(&self, db_tx: &rusqlite::Transaction) -> rusqlite::Result<()> {
|
||||
Self::init_sqlite_tables(db_tx)?;
|
||||
|
||||
let mut statement = db_tx.prepare_cached(&format!(
|
||||
"REPLACE INTO {}(descriptor_id, last_revealed) VALUES(:descriptor_id, :last_revealed)",
|
||||
Self::LAST_REVEALED_TABLE_NAME,
|
||||
))?;
|
||||
for (&descriptor_id, &last_revealed) in &self.last_revealed {
|
||||
statement.execute(named_params! {
|
||||
":descriptor_id": Impl(descriptor_id),
|
||||
":last_revealed": last_revealed,
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Helper types for spk-based blockchain clients.
|
||||
|
||||
use crate::{
|
||||
collections::BTreeMap, keychain::Indexed, local_chain::CheckPoint,
|
||||
ConfirmationTimeHeightAnchor, TxGraph,
|
||||
collections::BTreeMap, local_chain::CheckPoint, ConfirmationBlockTime, Indexed, TxGraph,
|
||||
};
|
||||
use alloc::boxed::Box;
|
||||
use bitcoin::{OutPoint, Script, ScriptBuf, Txid};
|
||||
@@ -160,7 +159,7 @@ impl SyncRequest {
|
||||
#[must_use]
|
||||
pub fn populate_with_revealed_spks<K: Clone + Ord + core::fmt::Debug + Send + Sync>(
|
||||
self,
|
||||
index: &crate::keychain::KeychainTxOutIndex<K>,
|
||||
index: &crate::indexer::keychain_txout::KeychainTxOutIndex<K>,
|
||||
spk_range: impl core::ops::RangeBounds<K>,
|
||||
) -> Self {
|
||||
use alloc::borrow::ToOwned;
|
||||
@@ -177,7 +176,7 @@ impl SyncRequest {
|
||||
/// Data returned from a spk-based blockchain client sync.
|
||||
///
|
||||
/// See also [`SyncRequest`].
|
||||
pub struct SyncResult<A = ConfirmationTimeHeightAnchor> {
|
||||
pub struct SyncResult<A = ConfirmationBlockTime> {
|
||||
/// The update to apply to the receiving [`TxGraph`].
|
||||
pub graph_update: TxGraph<A>,
|
||||
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
||||
@@ -216,12 +215,12 @@ impl<K: Ord + Clone> FullScanRequest<K> {
|
||||
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`] and is used to populate the
|
||||
/// [`FullScanRequest`].
|
||||
///
|
||||
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::keychain::KeychainTxOutIndex::all_unbounded_spk_iters
|
||||
/// [`KeychainTxOutIndex::all_unbounded_spk_iters`]: crate::indexer::keychain_txout::KeychainTxOutIndex::all_unbounded_spk_iters
|
||||
#[cfg(feature = "miniscript")]
|
||||
#[must_use]
|
||||
pub fn from_keychain_txout_index(
|
||||
chain_tip: CheckPoint,
|
||||
index: &crate::keychain::KeychainTxOutIndex<K>,
|
||||
index: &crate::indexer::keychain_txout::KeychainTxOutIndex<K>,
|
||||
) -> Self
|
||||
where
|
||||
K: core::fmt::Debug,
|
||||
@@ -318,7 +317,7 @@ impl<K: Ord + Clone> FullScanRequest<K> {
|
||||
/// Data returned from a spk-based blockchain client full scan.
|
||||
///
|
||||
/// See also [`FullScanRequest`].
|
||||
pub struct FullScanResult<K, A = ConfirmationTimeHeightAnchor> {
|
||||
pub struct FullScanResult<K, A = ConfirmationBlockTime> {
|
||||
/// The update to apply to the receiving [`LocalChain`](crate::local_chain::LocalChain).
|
||||
pub graph_update: TxGraph<A>,
|
||||
/// The update to apply to the receiving [`TxGraph`].
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
bitcoin::{secp256k1::Secp256k1, ScriptBuf},
|
||||
keychain::Indexed,
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
Indexed,
|
||||
};
|
||||
use core::{borrow::Borrow, ops::Bound, ops::RangeBounds};
|
||||
|
||||
@@ -137,7 +137,7 @@ where
|
||||
mod test {
|
||||
use crate::{
|
||||
bitcoin::secp256k1::Secp256k1,
|
||||
keychain::KeychainTxOutIndex,
|
||||
indexer::keychain_txout::KeychainTxOutIndex,
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
spk_iter::{SpkIterator, BIP32_MAX_INDEX},
|
||||
};
|
||||
|
||||
@@ -20,8 +20,7 @@ use alloc::vec::Vec;
|
||||
/// # use bdk_chain::local_chain::LocalChain;
|
||||
/// # use bdk_chain::tx_graph::TxGraph;
|
||||
/// # use bdk_chain::BlockId;
|
||||
/// # use bdk_chain::ConfirmationHeightAnchor;
|
||||
/// # use bdk_chain::ConfirmationTimeHeightAnchor;
|
||||
/// # use bdk_chain::ConfirmationBlockTime;
|
||||
/// # use bdk_chain::example_utils::*;
|
||||
/// # use bitcoin::hashes::Hash;
|
||||
/// // Initialize the local chain with two blocks.
|
||||
@@ -50,39 +49,19 @@ use alloc::vec::Vec;
|
||||
/// },
|
||||
/// );
|
||||
///
|
||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationHeightAnchor` as the anchor type.
|
||||
/// // This anchor records the anchor block and the confirmation height of the transaction.
|
||||
/// // When a transaction is anchored with `ConfirmationHeightAnchor`, the anchor block and
|
||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
||||
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
||||
/// let mut graph_b = TxGraph::<ConfirmationHeightAnchor>::default();
|
||||
/// let _ = graph_b.insert_tx(tx.clone());
|
||||
/// graph_b.insert_anchor(
|
||||
/// tx.compute_txid(),
|
||||
/// ConfirmationHeightAnchor {
|
||||
/// anchor_block: BlockId {
|
||||
/// height: 2,
|
||||
/// hash: Hash::hash("second".as_bytes()),
|
||||
/// },
|
||||
/// confirmation_height: 1,
|
||||
/// },
|
||||
/// );
|
||||
///
|
||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationTimeHeightAnchor` as the anchor type.
|
||||
/// // This anchor records the anchor block, the confirmation height and time of the transaction.
|
||||
/// // When a transaction is anchored with `ConfirmationTimeHeightAnchor`, the anchor block and
|
||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
||||
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
||||
/// let mut graph_c = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationBlockTime` as the anchor type.
|
||||
/// // This anchor records the anchor block and the confirmation time of the transaction. When a
|
||||
/// // transaction is anchored with `ConfirmationBlockTime`, the anchor block and confirmation block
|
||||
/// // of the transaction is the same block.
|
||||
/// let mut graph_c = TxGraph::<ConfirmationBlockTime>::default();
|
||||
/// let _ = graph_c.insert_tx(tx.clone());
|
||||
/// graph_c.insert_anchor(
|
||||
/// tx.compute_txid(),
|
||||
/// ConfirmationTimeHeightAnchor {
|
||||
/// anchor_block: BlockId {
|
||||
/// ConfirmationBlockTime {
|
||||
/// block_id: BlockId {
|
||||
/// height: 2,
|
||||
/// hash: Hash::hash("third".as_bytes()),
|
||||
/// },
|
||||
/// confirmation_height: 1,
|
||||
/// confirmation_time: 123,
|
||||
/// },
|
||||
/// );
|
||||
@@ -113,10 +92,10 @@ pub trait AnchorFromBlockPosition: Anchor {
|
||||
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, tx_pos: usize) -> Self;
|
||||
}
|
||||
|
||||
/// Trait that makes an object appendable.
|
||||
pub trait Append: Default {
|
||||
/// Append another object of the same type onto `self`.
|
||||
fn append(&mut self, other: Self);
|
||||
/// Trait that makes an object mergeable.
|
||||
pub trait Merge: Default {
|
||||
/// Merge another object of the same type onto `self`.
|
||||
fn merge(&mut self, other: Self);
|
||||
|
||||
/// Returns whether the structure is considered empty.
|
||||
fn is_empty(&self) -> bool;
|
||||
@@ -131,8 +110,8 @@ pub trait Append: Default {
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord, V> Append for BTreeMap<K, V> {
|
||||
fn append(&mut self, other: Self) {
|
||||
impl<K: Ord, V> Merge for BTreeMap<K, V> {
|
||||
fn merge(&mut self, other: Self) {
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
BTreeMap::extend(self, other)
|
||||
@@ -143,8 +122,8 @@ impl<K: Ord, V> Append for BTreeMap<K, V> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Append for BTreeSet<T> {
|
||||
fn append(&mut self, other: Self) {
|
||||
impl<T: Ord> Merge for BTreeSet<T> {
|
||||
fn merge(&mut self, other: Self) {
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
BTreeSet::extend(self, other)
|
||||
@@ -155,8 +134,8 @@ impl<T: Ord> Append for BTreeSet<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Append for Vec<T> {
|
||||
fn append(&mut self, mut other: Self) {
|
||||
impl<T> Merge for Vec<T> {
|
||||
fn merge(&mut self, mut other: Self) {
|
||||
Vec::append(self, &mut other)
|
||||
}
|
||||
|
||||
@@ -165,30 +144,30 @@ impl<T> Append for Vec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_append_for_tuple {
|
||||
macro_rules! impl_merge_for_tuple {
|
||||
($($a:ident $b:tt)*) => {
|
||||
impl<$($a),*> Append for ($($a,)*) where $($a: Append),* {
|
||||
impl<$($a),*> Merge for ($($a,)*) where $($a: Merge),* {
|
||||
|
||||
fn append(&mut self, _other: Self) {
|
||||
$(Append::append(&mut self.$b, _other.$b) );*
|
||||
fn merge(&mut self, _other: Self) {
|
||||
$(Merge::merge(&mut self.$b, _other.$b) );*
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
$(Append::is_empty(&self.$b) && )* true
|
||||
$(Merge::is_empty(&self.$b) && )* true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_append_for_tuple!();
|
||||
impl_append_for_tuple!(T0 0);
|
||||
impl_append_for_tuple!(T0 0 T1 1);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9 T10 10);
|
||||
impl_merge_for_tuple!();
|
||||
impl_merge_for_tuple!(T0 0);
|
||||
impl_merge_for_tuple!(T0 0 T1 1);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9);
|
||||
impl_merge_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9 T10 10);
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
//! A [`TxGraph`] can also be updated with another [`TxGraph`] which merges them together.
|
||||
//!
|
||||
//! ```
|
||||
//! # use bdk_chain::{Append, BlockId};
|
||||
//! # use bdk_chain::{Merge, BlockId};
|
||||
//! # use bdk_chain::tx_graph::TxGraph;
|
||||
//! # use bdk_chain::example_utils::*;
|
||||
//! # use bitcoin::Transaction;
|
||||
@@ -89,13 +89,12 @@
|
||||
//! [`insert_txout`]: TxGraph::insert_txout
|
||||
|
||||
use crate::{
|
||||
collections::*, keychain::Balance, Anchor, Append, BlockId, ChainOracle, ChainPosition,
|
||||
FullTxOut,
|
||||
collections::*, Anchor, Balance, BlockId, ChainOracle, ChainPosition, FullTxOut, Merge,
|
||||
};
|
||||
use alloc::collections::vec_deque::VecDeque;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{Amount, OutPoint, Script, SignedAmount, Transaction, TxOut, Txid};
|
||||
use bitcoin::{Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxOut, Txid};
|
||||
use core::fmt::{self, Formatter};
|
||||
use core::{
|
||||
convert::Infallible,
|
||||
@@ -109,10 +108,11 @@ use core::{
|
||||
/// [module-level documentation]: crate::tx_graph
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TxGraph<A = ()> {
|
||||
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors, tx_last_seen)`
|
||||
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>, u64)>,
|
||||
// all transactions that the graph is aware of in format: `(tx_node, tx_anchors)`
|
||||
txs: HashMap<Txid, (TxNodeInternal, BTreeSet<A>)>,
|
||||
spends: BTreeMap<OutPoint, HashSet<Txid>>,
|
||||
anchors: BTreeSet<(A, Txid)>,
|
||||
last_seen: HashMap<Txid, u64>,
|
||||
|
||||
// This atrocity exists so that `TxGraph::outspends()` can return a reference.
|
||||
// FIXME: This can be removed once `HashSet::new` is a const fn.
|
||||
@@ -125,6 +125,7 @@ impl<A> Default for TxGraph<A> {
|
||||
txs: Default::default(),
|
||||
spends: Default::default(),
|
||||
anchors: Default::default(),
|
||||
last_seen: Default::default(),
|
||||
empty_outspends: Default::default(),
|
||||
}
|
||||
}
|
||||
@@ -140,7 +141,7 @@ pub struct TxNode<'a, T, A> {
|
||||
/// The blocks that the transaction is "anchored" in.
|
||||
pub anchors: &'a BTreeSet<A>,
|
||||
/// The last-seen unix timestamp of the transaction as unconfirmed.
|
||||
pub last_seen_unconfirmed: u64,
|
||||
pub last_seen_unconfirmed: Option<u64>,
|
||||
}
|
||||
|
||||
impl<'a, T, A> Deref for TxNode<'a, T, A> {
|
||||
@@ -210,7 +211,7 @@ impl<A> TxGraph<A> {
|
||||
///
|
||||
/// This includes txouts of both full transactions as well as floating transactions.
|
||||
pub fn all_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||
self.txs.iter().flat_map(|(txid, (tx, _, _))| match tx {
|
||||
self.txs.iter().flat_map(|(txid, (tx, _))| match tx {
|
||||
TxNodeInternal::Whole(tx) => tx
|
||||
.as_ref()
|
||||
.output
|
||||
@@ -232,7 +233,7 @@ impl<A> TxGraph<A> {
|
||||
pub fn floating_txouts(&self) -> impl Iterator<Item = (OutPoint, &TxOut)> {
|
||||
self.txs
|
||||
.iter()
|
||||
.filter_map(|(txid, (tx_node, _, _))| match tx_node {
|
||||
.filter_map(|(txid, (tx_node, _))| match tx_node {
|
||||
TxNodeInternal::Whole(_) => None,
|
||||
TxNodeInternal::Partial(txouts) => Some(
|
||||
txouts
|
||||
@@ -247,17 +248,30 @@ impl<A> TxGraph<A> {
|
||||
pub fn full_txs(&self) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
|
||||
self.txs
|
||||
.iter()
|
||||
.filter_map(|(&txid, (tx, anchors, last_seen))| match tx {
|
||||
.filter_map(|(&txid, (tx, anchors))| match tx {
|
||||
TxNodeInternal::Whole(tx) => Some(TxNode {
|
||||
txid,
|
||||
tx: tx.clone(),
|
||||
anchors,
|
||||
last_seen_unconfirmed: *last_seen,
|
||||
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
|
||||
}),
|
||||
TxNodeInternal::Partial(_) => None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over graph transactions with no anchors or last-seen.
|
||||
pub fn txs_with_no_anchor_or_last_seen(
|
||||
&self,
|
||||
) -> impl Iterator<Item = TxNode<'_, Arc<Transaction>, A>> {
|
||||
self.full_txs().filter_map(|tx| {
|
||||
if tx.anchors.is_empty() && tx.last_seen_unconfirmed.is_none() {
|
||||
Some(tx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a transaction by txid. This only returns `Some` for full transactions.
|
||||
///
|
||||
/// Refer to [`get_txout`] for getting a specific [`TxOut`].
|
||||
@@ -270,11 +284,11 @@ impl<A> TxGraph<A> {
|
||||
/// Get a transaction node by txid. This only returns `Some` for full transactions.
|
||||
pub fn get_tx_node(&self, txid: Txid) -> Option<TxNode<'_, Arc<Transaction>, A>> {
|
||||
match &self.txs.get(&txid)? {
|
||||
(TxNodeInternal::Whole(tx), anchors, last_seen) => Some(TxNode {
|
||||
(TxNodeInternal::Whole(tx), anchors) => Some(TxNode {
|
||||
txid,
|
||||
tx: tx.clone(),
|
||||
anchors,
|
||||
last_seen_unconfirmed: *last_seen,
|
||||
last_seen_unconfirmed: self.last_seen.get(&txid).copied(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
@@ -504,7 +518,6 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
(
|
||||
TxNodeInternal::Partial([(outpoint.vout, txout)].into()),
|
||||
BTreeSet::new(),
|
||||
0,
|
||||
),
|
||||
);
|
||||
self.apply_update(update)
|
||||
@@ -518,7 +531,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
let mut update = Self::default();
|
||||
update.txs.insert(
|
||||
tx.compute_txid(),
|
||||
(TxNodeInternal::Whole(tx), BTreeSet::new(), 0),
|
||||
(TxNodeInternal::Whole(tx), BTreeSet::new()),
|
||||
);
|
||||
self.apply_update(update)
|
||||
}
|
||||
@@ -534,8 +547,8 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
) -> ChangeSet<A> {
|
||||
let mut changeset = ChangeSet::<A>::default();
|
||||
for (tx, seen_at) in txs {
|
||||
changeset.append(self.insert_seen_at(tx.compute_txid(), seen_at));
|
||||
changeset.append(self.insert_tx(tx));
|
||||
changeset.merge(self.insert_seen_at(tx.compute_txid(), seen_at));
|
||||
changeset.merge(self.insert_tx(tx));
|
||||
}
|
||||
changeset
|
||||
}
|
||||
@@ -559,8 +572,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
/// [`update_last_seen_unconfirmed`]: Self::update_last_seen_unconfirmed
|
||||
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A> {
|
||||
let mut update = Self::default();
|
||||
let (_, _, update_last_seen) = update.txs.entry(txid).or_default();
|
||||
*update_last_seen = seen_at;
|
||||
update.last_seen.insert(txid, seen_at);
|
||||
self.apply_update(update)
|
||||
}
|
||||
|
||||
@@ -607,7 +619,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
.txs
|
||||
.iter()
|
||||
.filter_map(
|
||||
|(&txid, (_, anchors, _))| {
|
||||
|(&txid, (_, anchors))| {
|
||||
if anchors.is_empty() {
|
||||
Some(txid)
|
||||
} else {
|
||||
@@ -618,7 +630,7 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
.collect();
|
||||
|
||||
for txid in unanchored_txs {
|
||||
changeset.append(self.insert_seen_at(txid, seen_at));
|
||||
changeset.merge(self.insert_seen_at(txid, seen_at));
|
||||
}
|
||||
changeset
|
||||
}
|
||||
@@ -656,10 +668,10 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
});
|
||||
|
||||
match self.txs.get_mut(&txid) {
|
||||
Some((tx_node @ TxNodeInternal::Partial(_), _, _)) => {
|
||||
Some((tx_node @ TxNodeInternal::Partial(_), _)) => {
|
||||
*tx_node = TxNodeInternal::Whole(wrapped_tx.clone());
|
||||
}
|
||||
Some((TxNodeInternal::Whole(tx), _, _)) => {
|
||||
Some((TxNodeInternal::Whole(tx), _)) => {
|
||||
debug_assert_eq!(
|
||||
tx.as_ref().compute_txid(),
|
||||
txid,
|
||||
@@ -667,10 +679,8 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
);
|
||||
}
|
||||
None => {
|
||||
self.txs.insert(
|
||||
txid,
|
||||
(TxNodeInternal::Whole(wrapped_tx), BTreeSet::new(), 0),
|
||||
);
|
||||
self.txs
|
||||
.insert(txid, (TxNodeInternal::Whole(wrapped_tx), BTreeSet::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -679,9 +689,8 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
let tx_entry = self.txs.entry(outpoint.txid).or_default();
|
||||
|
||||
match tx_entry {
|
||||
(TxNodeInternal::Whole(_), _, _) => { /* do nothing since we already have full tx */
|
||||
}
|
||||
(TxNodeInternal::Partial(txouts), _, _) => {
|
||||
(TxNodeInternal::Whole(_), _) => { /* do nothing since we already have full tx */ }
|
||||
(TxNodeInternal::Partial(txouts), _) => {
|
||||
txouts.insert(outpoint.vout, txout);
|
||||
}
|
||||
}
|
||||
@@ -689,13 +698,13 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
|
||||
for (anchor, txid) in changeset.anchors {
|
||||
if self.anchors.insert((anchor.clone(), txid)) {
|
||||
let (_, anchors, _) = self.txs.entry(txid).or_default();
|
||||
let (_, anchors) = self.txs.entry(txid).or_default();
|
||||
anchors.insert(anchor);
|
||||
}
|
||||
}
|
||||
|
||||
for (txid, new_last_seen) in changeset.last_seen {
|
||||
let (_, _, last_seen) = self.txs.entry(txid).or_default();
|
||||
let last_seen = self.last_seen.entry(txid).or_default();
|
||||
if new_last_seen > *last_seen {
|
||||
*last_seen = new_last_seen;
|
||||
}
|
||||
@@ -709,11 +718,10 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
pub(crate) fn determine_changeset(&self, update: TxGraph<A>) -> ChangeSet<A> {
|
||||
let mut changeset = ChangeSet::<A>::default();
|
||||
|
||||
for (&txid, (update_tx_node, _, update_last_seen)) in &update.txs {
|
||||
let prev_last_seen: u64 = match (self.txs.get(&txid), update_tx_node) {
|
||||
for (&txid, (update_tx_node, _)) in &update.txs {
|
||||
match (self.txs.get(&txid), update_tx_node) {
|
||||
(None, TxNodeInternal::Whole(update_tx)) => {
|
||||
changeset.txs.insert(update_tx.clone());
|
||||
0
|
||||
}
|
||||
(None, TxNodeInternal::Partial(update_txos)) => {
|
||||
changeset.txouts.extend(
|
||||
@@ -721,18 +729,13 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
.iter()
|
||||
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
|
||||
);
|
||||
0
|
||||
}
|
||||
(Some((TxNodeInternal::Whole(_), _, last_seen)), _) => *last_seen,
|
||||
(
|
||||
Some((TxNodeInternal::Partial(_), _, last_seen)),
|
||||
TxNodeInternal::Whole(update_tx),
|
||||
) => {
|
||||
(Some((TxNodeInternal::Whole(_), _)), _) => {}
|
||||
(Some((TxNodeInternal::Partial(_), _)), TxNodeInternal::Whole(update_tx)) => {
|
||||
changeset.txs.insert(update_tx.clone());
|
||||
*last_seen
|
||||
}
|
||||
(
|
||||
Some((TxNodeInternal::Partial(txos), _, last_seen)),
|
||||
Some((TxNodeInternal::Partial(txos), _)),
|
||||
TxNodeInternal::Partial(update_txos),
|
||||
) => {
|
||||
changeset.txouts.extend(
|
||||
@@ -741,12 +744,14 @@ impl<A: Clone + Ord> TxGraph<A> {
|
||||
.filter(|(vout, _)| !txos.contains_key(*vout))
|
||||
.map(|(&vout, txo)| (OutPoint::new(txid, vout), txo.clone())),
|
||||
);
|
||||
*last_seen
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if *update_last_seen > prev_last_seen {
|
||||
changeset.last_seen.insert(txid, *update_last_seen);
|
||||
for (txid, update_last_seen) in update.last_seen {
|
||||
let prev_last_seen = self.last_seen.get(&txid).copied();
|
||||
if Some(update_last_seen) > prev_last_seen {
|
||||
changeset.last_seen.insert(txid, update_last_seen);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -786,7 +791,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
chain_tip: BlockId,
|
||||
txid: Txid,
|
||||
) -> Result<Option<ChainPosition<&A>>, C::Error> {
|
||||
let (tx_node, anchors, last_seen) = match self.txs.get(&txid) {
|
||||
let (tx_node, anchors) = match self.txs.get(&txid) {
|
||||
Some(v) => v,
|
||||
None => return Ok(None),
|
||||
};
|
||||
@@ -798,6 +803,13 @@ impl<A: Anchor> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
// If no anchors are in best chain and we don't have a last_seen, we can return
|
||||
// early because by definition the tx doesn't have a chain position.
|
||||
let last_seen = match self.last_seen.get(&txid) {
|
||||
Some(t) => *t,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// The tx is not anchored to a block in the best chain, which means that it
|
||||
// might be in mempool, or it might have been dropped already.
|
||||
// Let's check conflicts to find out!
|
||||
@@ -884,7 +896,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
if conflicting_tx.last_seen_unconfirmed > tx_last_seen {
|
||||
return Ok(None);
|
||||
}
|
||||
if conflicting_tx.last_seen_unconfirmed == *last_seen
|
||||
if conflicting_tx.last_seen_unconfirmed == Some(last_seen)
|
||||
&& conflicting_tx.as_ref().compute_txid() > tx.as_ref().compute_txid()
|
||||
{
|
||||
// Conflicting tx has priority if txid of conflicting tx > txid of original tx
|
||||
@@ -893,7 +905,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(ChainPosition::Unconfirmed(*last_seen)))
|
||||
Ok(Some(ChainPosition::Unconfirmed(last_seen)))
|
||||
}
|
||||
|
||||
/// Get the position of the transaction in `chain` with tip `chain_tip`.
|
||||
@@ -971,10 +983,10 @@ impl<A: Anchor> TxGraph<A> {
|
||||
/// If the [`ChainOracle`] implementation (`chain`) fails, an error will be returned with the
|
||||
/// returned item.
|
||||
///
|
||||
/// If the [`ChainOracle`] is infallible, [`list_chain_txs`] can be used instead.
|
||||
/// If the [`ChainOracle`] is infallible, [`list_canonical_txs`] can be used instead.
|
||||
///
|
||||
/// [`list_chain_txs`]: Self::list_chain_txs
|
||||
pub fn try_list_chain_txs<'a, C: ChainOracle + 'a>(
|
||||
/// [`list_canonical_txs`]: Self::list_canonical_txs
|
||||
pub fn try_list_canonical_txs<'a, C: ChainOracle + 'a>(
|
||||
&'a self,
|
||||
chain: &'a C,
|
||||
chain_tip: BlockId,
|
||||
@@ -993,15 +1005,15 @@ impl<A: Anchor> TxGraph<A> {
|
||||
|
||||
/// List graph transactions that are in `chain` with `chain_tip`.
|
||||
///
|
||||
/// This is the infallible version of [`try_list_chain_txs`].
|
||||
/// This is the infallible version of [`try_list_canonical_txs`].
|
||||
///
|
||||
/// [`try_list_chain_txs`]: Self::try_list_chain_txs
|
||||
pub fn list_chain_txs<'a, C: ChainOracle + 'a>(
|
||||
/// [`try_list_canonical_txs`]: Self::try_list_canonical_txs
|
||||
pub fn list_canonical_txs<'a, C: ChainOracle + 'a>(
|
||||
&'a self,
|
||||
chain: &'a C,
|
||||
chain_tip: BlockId,
|
||||
) -> impl Iterator<Item = CanonicalTx<'a, Arc<Transaction>, A>> {
|
||||
self.try_list_chain_txs(chain, chain_tip)
|
||||
self.try_list_canonical_txs(chain, chain_tip)
|
||||
.map(|r| r.expect("oracle is infallible"))
|
||||
}
|
||||
|
||||
@@ -1151,7 +1163,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
chain: &C,
|
||||
chain_tip: BlockId,
|
||||
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
|
||||
mut trust_predicate: impl FnMut(&OI, &Script) -> bool,
|
||||
mut trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
|
||||
) -> Result<Balance, C::Error> {
|
||||
let mut immature = Amount::ZERO;
|
||||
let mut trusted_pending = Amount::ZERO;
|
||||
@@ -1170,7 +1182,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
}
|
||||
}
|
||||
ChainPosition::Unconfirmed(_) => {
|
||||
if trust_predicate(&spk_i, &txout.txout.script_pubkey) {
|
||||
if trust_predicate(&spk_i, txout.txout.script_pubkey) {
|
||||
trusted_pending += txout.txout.value;
|
||||
} else {
|
||||
untrusted_pending += txout.txout.value;
|
||||
@@ -1197,7 +1209,7 @@ impl<A: Anchor> TxGraph<A> {
|
||||
chain: &C,
|
||||
chain_tip: BlockId,
|
||||
outpoints: impl IntoIterator<Item = (OI, OutPoint)>,
|
||||
trust_predicate: impl FnMut(&OI, &Script) -> bool,
|
||||
trust_predicate: impl FnMut(&OI, ScriptBuf) -> bool,
|
||||
) -> Balance {
|
||||
self.try_balance(chain, chain_tip, outpoints, trust_predicate)
|
||||
.expect("oracle is infallible")
|
||||
@@ -1281,8 +1293,8 @@ impl<A> ChangeSet<A> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Ord> Append for ChangeSet<A> {
|
||||
fn append(&mut self, other: Self) {
|
||||
impl<A: Ord> Merge for ChangeSet<A> {
|
||||
fn merge(&mut self, other: Self) {
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
self.txs.extend(other.txs);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
|
||||
use bdk_chain::{spk_txout::SpkTxOutIndex, tx_graph::TxGraph, Anchor};
|
||||
use bitcoin::{
|
||||
locktime::absolute::LockTime, secp256k1::Secp256k1, transaction, Amount, OutPoint, ScriptBuf,
|
||||
Sequence, Transaction, TxIn, TxOut, Txid, Witness,
|
||||
@@ -119,7 +119,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
},
|
||||
Some(index) => TxOut {
|
||||
value: Amount::from_sat(output.value),
|
||||
script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(),
|
||||
script_pubkey: spk_index.spk_at_index(index).unwrap(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
@@ -131,9 +131,7 @@ pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
for anchor in tx_tmp.anchors.iter() {
|
||||
let _ = graph.insert_anchor(tx.compute_txid(), anchor.clone());
|
||||
}
|
||||
if let Some(seen_at) = tx_tmp.last_seen {
|
||||
let _ = graph.insert_seen_at(tx.compute_txid(), seen_at);
|
||||
}
|
||||
let _ = graph.insert_seen_at(tx.compute_txid(), tx_tmp.last_seen.unwrap_or(0));
|
||||
}
|
||||
(graph, spk_index, tx_ids)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,11 @@ use std::{collections::BTreeSet, sync::Arc};
|
||||
use crate::common::DESCRIPTORS;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain::{self, Balance, KeychainTxOutIndex},
|
||||
indexer::keychain_txout::KeychainTxOutIndex,
|
||||
local_chain::LocalChain,
|
||||
tx_graph, Append, ChainPosition, ConfirmationHeightAnchor, DescriptorExt,
|
||||
};
|
||||
use bitcoin::{
|
||||
secp256k1::Secp256k1, Amount, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut,
|
||||
tx_graph, Balance, ChainPosition, ConfirmationBlockTime, DescriptorExt,
|
||||
};
|
||||
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
||||
@@ -26,12 +24,13 @@ use miniscript::Descriptor;
|
||||
/// agnostic.
|
||||
#[test]
|
||||
fn insert_relevant_txs() {
|
||||
use bdk_chain::indexer::keychain_txout;
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTORS[0])
|
||||
.expect("must be valid");
|
||||
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
|
||||
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
|
||||
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<()>>::new(
|
||||
KeychainTxOutIndex::new(10),
|
||||
);
|
||||
let _ = graph
|
||||
@@ -72,13 +71,12 @@ fn insert_relevant_txs() {
|
||||
let txs = [tx_c, tx_b, tx_a];
|
||||
|
||||
let changeset = indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph::ChangeSet {
|
||||
tx_graph: tx_graph::ChangeSet {
|
||||
txs: txs.iter().cloned().map(Arc::new).collect(),
|
||||
..Default::default()
|
||||
},
|
||||
indexer: keychain::ChangeSet {
|
||||
indexer: keychain_txout::ChangeSet {
|
||||
last_revealed: [(descriptor.descriptor_id(), 9_u32)].into(),
|
||||
keychains_added: [].into(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -89,10 +87,9 @@ fn insert_relevant_txs() {
|
||||
|
||||
// The initial changeset will also contain info about the keychain we added
|
||||
let initial_changeset = indexed_tx_graph::ChangeSet {
|
||||
graph: changeset.graph,
|
||||
indexer: keychain::ChangeSet {
|
||||
tx_graph: changeset.tx_graph,
|
||||
indexer: keychain_txout::ChangeSet {
|
||||
last_revealed: changeset.indexer.last_revealed,
|
||||
keychains_added: [((), descriptor)].into(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -116,8 +113,8 @@ fn insert_relevant_txs() {
|
||||
/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
|
||||
/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
|
||||
/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
|
||||
/// tx4: Mempool tx, sending 20000 sats to "trusted" address.
|
||||
/// tx5: Mempool tx, sending 15000 sats to "untested" address.
|
||||
/// tx4: Mempool tx, sending 20000 sats to "untrusted" address.
|
||||
/// tx5: Mempool tx, sending 15000 sats to "trusted" address.
|
||||
/// tx6: Complete unrelated tx. [Block 3]
|
||||
///
|
||||
/// Different transactions are added via `insert_relevant_txs`.
|
||||
@@ -139,20 +136,18 @@ fn test_list_owned_txouts() {
|
||||
let (desc_2, _) =
|
||||
Descriptor::parse_descriptor(&Secp256k1::signing_only(), common::DESCRIPTORS[3]).unwrap();
|
||||
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
|
||||
let mut graph = IndexedTxGraph::<ConfirmationBlockTime, KeychainTxOutIndex<String>>::new(
|
||||
KeychainTxOutIndex::new(10),
|
||||
);
|
||||
|
||||
assert!(!graph
|
||||
assert!(graph
|
||||
.index
|
||||
.insert_descriptor("keychain_1".into(), desc_1)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
assert!(!graph
|
||||
.unwrap());
|
||||
assert!(graph
|
||||
.index
|
||||
.insert_descriptor("keychain_2".into(), desc_2)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
.unwrap());
|
||||
|
||||
// Get trusted and untrusted addresses
|
||||
|
||||
@@ -160,11 +155,11 @@ fn test_list_owned_txouts() {
|
||||
let mut untrusted_spks: Vec<ScriptBuf> = Vec::new();
|
||||
|
||||
{
|
||||
// we need to scope here to take immutanble reference of the graph
|
||||
// we need to scope here to take immutable reference of the graph
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph
|
||||
.index
|
||||
.reveal_next_spk(&"keychain_1".to_string())
|
||||
.reveal_next_spk("keychain_1".to_string())
|
||||
.unwrap();
|
||||
// TODO Assert indexes
|
||||
trusted_spks.push(script.to_owned());
|
||||
@@ -174,7 +169,7 @@ fn test_list_owned_txouts() {
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph
|
||||
.index
|
||||
.reveal_next_spk(&"keychain_2".to_string())
|
||||
.reveal_next_spk("keychain_2".to_string())
|
||||
.unwrap();
|
||||
untrusted_spks.push(script.to_owned());
|
||||
}
|
||||
@@ -226,7 +221,7 @@ fn test_list_owned_txouts() {
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
|
||||
// tx5 is an external transaction receiving at trusted keychain, unconfirmed.
|
||||
let tx5 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(15000),
|
||||
@@ -239,7 +234,7 @@ fn test_list_owned_txouts() {
|
||||
let tx6 = common::new_tx(0);
|
||||
|
||||
// Insert transactions into graph with respective anchors
|
||||
// For unconfirmed txs we pass in `None`.
|
||||
// Insert unconfirmed txs with a last_seen timestamp
|
||||
|
||||
let _ =
|
||||
graph.batch_insert_relevant([&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| {
|
||||
@@ -249,9 +244,9 @@ fn test_list_owned_txouts() {
|
||||
local_chain
|
||||
.get(height)
|
||||
.map(|cp| cp.block_id())
|
||||
.map(|anchor_block| ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: anchor_block.height,
|
||||
.map(|block_id| ConfirmationBlockTime {
|
||||
block_id,
|
||||
confirmation_time: 100,
|
||||
}),
|
||||
)
|
||||
}));
|
||||
@@ -260,8 +255,7 @@ fn test_list_owned_txouts() {
|
||||
|
||||
// A helper lambda to extract and filter data from the graph.
|
||||
let fetch =
|
||||
|height: u32,
|
||||
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
||||
|height: u32, graph: &IndexedTxGraph<ConfirmationBlockTime, KeychainTxOutIndex<String>>| {
|
||||
let chain_tip = local_chain
|
||||
.get(height)
|
||||
.map(|cp| cp.block_id())
|
||||
@@ -288,12 +282,9 @@ fn test_list_owned_txouts() {
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|_, spk: &Script| trusted_spks.contains(&spk.to_owned()),
|
||||
|_, spk: ScriptBuf| trusted_spks.contains(&spk),
|
||||
);
|
||||
|
||||
assert_eq!(txouts.len(), 5);
|
||||
assert_eq!(utxos.len(), 4);
|
||||
|
||||
let confirmed_txouts_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|(_, full_txout)| {
|
||||
@@ -359,29 +350,25 @@ fn test_list_owned_txouts() {
|
||||
balance,
|
||||
) = fetch(0, &graph);
|
||||
|
||||
// tx1 is a confirmed txout and is unspent
|
||||
// tx4, tx5 are unconfirmed
|
||||
assert_eq!(confirmed_txouts_txid, [tx1.compute_txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_txouts_txid,
|
||||
[
|
||||
tx2.compute_txid(),
|
||||
tx3.compute_txid(),
|
||||
tx4.compute_txid(),
|
||||
tx5.compute_txid()
|
||||
]
|
||||
.into()
|
||||
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::ZERO // Nothing is confirmed yet
|
||||
}
|
||||
@@ -405,23 +392,26 @@ fn test_list_owned_txouts() {
|
||||
);
|
||||
assert_eq!(
|
||||
unconfirmed_txouts_txid,
|
||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
);
|
||||
|
||||
// tx2 doesn't get into confirmed utxos set
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.compute_txid()].into());
|
||||
// tx2 gets into confirmed utxos set
|
||||
assert_eq!(
|
||||
confirmed_utxos_txid,
|
||||
[tx1.compute_txid(), tx2.compute_txid()].into()
|
||||
);
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx3.compute_txid(), tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
[tx4.compute_txid(), tx5.compute_txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(25000), // tx3 + tx5
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::ZERO // Nothing is confirmed yet
|
||||
confirmed: Amount::from_sat(30_000) // tx2 got confirmed
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -477,6 +467,7 @@ fn test_list_owned_txouts() {
|
||||
balance,
|
||||
) = fetch(98, &graph);
|
||||
|
||||
// no change compared to block 2
|
||||
assert_eq!(
|
||||
confirmed_txouts_txid,
|
||||
[tx1.compute_txid(), tx2.compute_txid(), tx3.compute_txid()].into()
|
||||
@@ -502,14 +493,14 @@ fn test_list_owned_txouts() {
|
||||
immature: Amount::from_sat(70000), // immature coinbase
|
||||
trusted_pending: Amount::from_sat(15000), // tx5
|
||||
untrusted_pending: Amount::from_sat(20000), // tx4
|
||||
confirmed: Amount::from_sat(10000) // tx1 got matured
|
||||
confirmed: Amount::from_sat(10000) // tx3 is confirmed
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AT Block 99
|
||||
{
|
||||
let (_, _, _, _, balance) = fetch(100, &graph);
|
||||
let (_, _, _, _, balance) = fetch(99, &graph);
|
||||
|
||||
// Coinbase maturity hits
|
||||
assert_eq!(
|
||||
@@ -523,3 +514,147 @@ fn test_list_owned_txouts() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a `LocalChain`, `IndexedTxGraph`, and a `Transaction`, when we insert some anchor
|
||||
/// (possibly non-canonical) and/or a last-seen timestamp into the graph, we expect the
|
||||
/// result of `get_chain_position` in these cases:
|
||||
///
|
||||
/// - tx with no anchors or last_seen has no `ChainPosition`
|
||||
/// - tx with any last_seen will be `Unconfirmed`
|
||||
/// - tx with an anchor in best chain will be `Confirmed`
|
||||
/// - tx with an anchor not in best chain (no last_seen) has no `ChainPosition`
|
||||
#[test]
|
||||
fn test_get_chain_position() {
|
||||
use bdk_chain::local_chain::CheckPoint;
|
||||
use bdk_chain::spk_txout::SpkTxOutIndex;
|
||||
use bdk_chain::BlockId;
|
||||
|
||||
struct TestCase<A> {
|
||||
name: &'static str,
|
||||
tx: Transaction,
|
||||
anchor: Option<A>,
|
||||
last_seen: Option<u64>,
|
||||
exp_pos: Option<ChainPosition<A>>,
|
||||
}
|
||||
|
||||
// addr: bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm
|
||||
let spk = ScriptBuf::from_hex("0014c692ecf13534982a9a2834565cbd37add8027140").unwrap();
|
||||
let mut graph = IndexedTxGraph::new({
|
||||
let mut index = SpkTxOutIndex::default();
|
||||
let _ = index.insert_spk(0u32, spk.clone());
|
||||
index
|
||||
});
|
||||
|
||||
// Anchors to test
|
||||
let blocks = vec![block_id!(0, "g"), block_id!(1, "A"), block_id!(2, "B")];
|
||||
|
||||
let cp = CheckPoint::from_block_ids(blocks.clone()).unwrap();
|
||||
let chain = LocalChain::from_tip(cp).unwrap();
|
||||
|
||||
// The test will insert a transaction into the indexed tx graph
|
||||
// along with any anchors and timestamps, then check the value
|
||||
// returned by `get_chain_position`.
|
||||
fn run(
|
||||
chain: &LocalChain,
|
||||
graph: &mut IndexedTxGraph<BlockId, SpkTxOutIndex<u32>>,
|
||||
test: TestCase<BlockId>,
|
||||
) {
|
||||
let TestCase {
|
||||
name,
|
||||
tx,
|
||||
anchor,
|
||||
last_seen,
|
||||
exp_pos,
|
||||
} = test;
|
||||
|
||||
// add data to graph
|
||||
let txid = tx.compute_txid();
|
||||
let _ = graph.insert_tx(tx);
|
||||
if let Some(anchor) = anchor {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
if let Some(seen_at) = last_seen {
|
||||
let _ = graph.insert_seen_at(txid, seen_at);
|
||||
}
|
||||
|
||||
// check chain position
|
||||
let res = graph
|
||||
.graph()
|
||||
.get_chain_position(chain, chain.tip().block_id(), txid);
|
||||
assert_eq!(
|
||||
res.map(ChainPosition::cloned),
|
||||
exp_pos,
|
||||
"failed test case: {name}"
|
||||
);
|
||||
}
|
||||
|
||||
[
|
||||
TestCase {
|
||||
name: "tx no anchors or last_seen - no chain pos",
|
||||
tx: Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::ONE_BTC,
|
||||
script_pubkey: spk.clone(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
},
|
||||
anchor: None,
|
||||
last_seen: None,
|
||||
exp_pos: None,
|
||||
},
|
||||
TestCase {
|
||||
name: "tx last_seen - unconfirmed",
|
||||
tx: Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::ONE_BTC,
|
||||
script_pubkey: spk.clone(),
|
||||
}],
|
||||
..common::new_tx(1)
|
||||
},
|
||||
anchor: None,
|
||||
last_seen: Some(2),
|
||||
exp_pos: Some(ChainPosition::Unconfirmed(2)),
|
||||
},
|
||||
TestCase {
|
||||
name: "tx anchor in best chain - confirmed",
|
||||
tx: Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::ONE_BTC,
|
||||
script_pubkey: spk.clone(),
|
||||
}],
|
||||
..common::new_tx(2)
|
||||
},
|
||||
anchor: Some(blocks[1]),
|
||||
last_seen: None,
|
||||
exp_pos: Some(ChainPosition::Confirmed(blocks[1])),
|
||||
},
|
||||
TestCase {
|
||||
name: "tx unknown anchor with last_seen - unconfirmed",
|
||||
tx: Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::ONE_BTC,
|
||||
script_pubkey: spk.clone(),
|
||||
}],
|
||||
..common::new_tx(3)
|
||||
},
|
||||
anchor: Some(block_id!(2, "B'")),
|
||||
last_seen: Some(2),
|
||||
exp_pos: Some(ChainPosition::Unconfirmed(2)),
|
||||
},
|
||||
TestCase {
|
||||
name: "tx unknown anchor - no chain pos",
|
||||
tx: Transaction {
|
||||
output: vec![TxOut {
|
||||
value: Amount::ONE_BTC,
|
||||
script_pubkey: spk.clone(),
|
||||
}],
|
||||
..common::new_tx(4)
|
||||
},
|
||||
anchor: Some(block_id!(2, "B'")),
|
||||
last_seen: None,
|
||||
exp_pos: None,
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(|t| run(&chain, &mut graph, t));
|
||||
}
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
mod common;
|
||||
use bdk_chain::{
|
||||
collections::BTreeMap,
|
||||
indexed_tx_graph::Indexer,
|
||||
keychain::{self, ChangeSet, KeychainTxOutIndex},
|
||||
Append, DescriptorExt, DescriptorId,
|
||||
indexer::keychain_txout::{ChangeSet, KeychainTxOutIndex},
|
||||
DescriptorExt, DescriptorId, Indexer, Merge,
|
||||
};
|
||||
|
||||
use bitcoin::{secp256k1::Secp256k1, Amount, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||
@@ -31,8 +30,8 @@ fn init_txout_index(
|
||||
external_descriptor: Descriptor<DescriptorPublicKey>,
|
||||
internal_descriptor: Descriptor<DescriptorPublicKey>,
|
||||
lookahead: u32,
|
||||
) -> bdk_chain::keychain::KeychainTxOutIndex<TestKeychain> {
|
||||
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::new(lookahead);
|
||||
) -> KeychainTxOutIndex<TestKeychain> {
|
||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(lookahead);
|
||||
|
||||
let _ = txout_index
|
||||
.insert_descriptor(TestKeychain::External, external_descriptor)
|
||||
@@ -52,13 +51,13 @@ fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> Scr
|
||||
}
|
||||
|
||||
// We create two empty changesets lhs and rhs, we then insert various descriptors with various
|
||||
// last_revealed, append rhs to lhs, and check that the result is consistent with these rules:
|
||||
// last_revealed, merge rhs to lhs, and check that the result is consistent with these rules:
|
||||
// - Existing index doesn't update if the new index in `other` is lower than `self`.
|
||||
// - Existing index updates if the new index in `other` is higher than `self`.
|
||||
// - Existing index is unchanged if keychain doesn't exist in `other`.
|
||||
// - New keychain gets added if the keychain is in `other` but not in `self`.
|
||||
#[test]
|
||||
fn append_changesets_check_last_revealed() {
|
||||
fn merge_changesets_check_last_revealed() {
|
||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let descriptor_ids: Vec<_> = DESCRIPTORS
|
||||
.iter()
|
||||
@@ -82,14 +81,12 @@ fn append_changesets_check_last_revealed() {
|
||||
lhs_di.insert(descriptor_ids[3], 4); // key doesn't exist in lhs
|
||||
|
||||
let mut lhs = ChangeSet {
|
||||
keychains_added: BTreeMap::<(), _>::new(),
|
||||
last_revealed: lhs_di,
|
||||
};
|
||||
let rhs = ChangeSet {
|
||||
keychains_added: BTreeMap::<(), _>::new(),
|
||||
last_revealed: rhs_di,
|
||||
};
|
||||
lhs.append(rhs);
|
||||
lhs.merge(rhs);
|
||||
|
||||
// Existing index doesn't update if the new index in `other` is lower than `self`.
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[0]), Some(&7));
|
||||
@@ -101,53 +98,8 @@ fn append_changesets_check_last_revealed() {
|
||||
assert_eq!(lhs.last_revealed.get(&descriptor_ids[3]), Some(&4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_apply_contradictory_changesets_they_are_ignored() {
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
init_txout_index(external_descriptor.clone(), internal_descriptor.clone(), 0);
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &external_descriptor),
|
||||
(&TestKeychain::Internal, &internal_descriptor)
|
||||
]
|
||||
);
|
||||
|
||||
let changeset = ChangeSet {
|
||||
keychains_added: [(TestKeychain::External, internal_descriptor.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
};
|
||||
txout_index.apply_changeset(changeset);
|
||||
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &external_descriptor),
|
||||
(&TestKeychain::Internal, &internal_descriptor)
|
||||
]
|
||||
);
|
||||
|
||||
let changeset = ChangeSet {
|
||||
keychains_added: [(TestKeychain::Internal, external_descriptor.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
};
|
||||
txout_index.apply_changeset(changeset);
|
||||
|
||||
assert_eq!(
|
||||
txout_index.keychains().collect::<Vec<_>>(),
|
||||
vec![
|
||||
(&TestKeychain::External, &external_descriptor),
|
||||
(&TestKeychain::Internal, &internal_descriptor)
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_all_derivation_indices() {
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
|
||||
let external_descriptor = parse_descriptor(DESCRIPTORS[0]);
|
||||
let internal_descriptor = parse_descriptor(DESCRIPTORS[1]);
|
||||
let mut txout_index =
|
||||
@@ -162,14 +114,13 @@ fn test_set_all_derivation_indices() {
|
||||
assert_eq!(
|
||||
txout_index.reveal_to_target_multi(&derive_to),
|
||||
ChangeSet {
|
||||
keychains_added: BTreeMap::new(),
|
||||
last_revealed: last_revealed.clone()
|
||||
}
|
||||
);
|
||||
assert_eq!(txout_index.last_revealed_indices(), derive_to);
|
||||
assert_eq!(
|
||||
txout_index.reveal_to_target_multi(&derive_to),
|
||||
keychain::ChangeSet::default(),
|
||||
ChangeSet::default(),
|
||||
"no changes if we set to the same thing"
|
||||
);
|
||||
assert_eq!(txout_index.initial_changeset().last_revealed, last_revealed);
|
||||
@@ -191,7 +142,7 @@ fn test_lookahead() {
|
||||
// - stored scripts of external keychain should be of expected counts
|
||||
for index in (0..20).skip_while(|i| i % 2 == 1) {
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::External, index)
|
||||
.reveal_to_target(TestKeychain::External, index)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
revealed_spks,
|
||||
@@ -210,25 +161,25 @@ fn test_lookahead() {
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.revealed_keychain_spks(TestKeychain::External)
|
||||
.count(),
|
||||
index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.revealed_keychain_spks(TestKeychain::Internal)
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.unused_keychain_spks(&TestKeychain::External)
|
||||
.unused_keychain_spks(TestKeychain::External)
|
||||
.count(),
|
||||
index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.unused_keychain_spks(&TestKeychain::Internal)
|
||||
.unused_keychain_spks(TestKeychain::Internal)
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
@@ -242,7 +193,7 @@ fn test_lookahead() {
|
||||
// expect:
|
||||
// - scripts cached in spk_txout_index should increase correctly, a.k.a. no scripts are skipped
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::Internal, 24)
|
||||
.reveal_to_target(TestKeychain::Internal, 24)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
revealed_spks,
|
||||
@@ -263,17 +214,17 @@ fn test_lookahead() {
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.revealed_keychain_spks(TestKeychain::Internal)
|
||||
.count(),
|
||||
25,
|
||||
);
|
||||
|
||||
// ensure derivation indices are expected for each keychain
|
||||
let last_external_index = txout_index
|
||||
.last_revealed_index(&TestKeychain::External)
|
||||
.last_revealed_index(TestKeychain::External)
|
||||
.expect("already derived");
|
||||
let last_internal_index = txout_index
|
||||
.last_revealed_index(&TestKeychain::Internal)
|
||||
.last_revealed_index(TestKeychain::Internal)
|
||||
.expect("already derived");
|
||||
assert_eq!(last_external_index, 19);
|
||||
assert_eq!(last_internal_index, 24);
|
||||
@@ -304,24 +255,24 @@ fn test_lookahead() {
|
||||
],
|
||||
..common::new_tx(external_index)
|
||||
};
|
||||
assert_eq!(txout_index.index_tx(&tx), keychain::ChangeSet::default());
|
||||
assert_eq!(txout_index.index_tx(&tx), ChangeSet::default());
|
||||
assert_eq!(
|
||||
txout_index.last_revealed_index(&TestKeychain::External),
|
||||
txout_index.last_revealed_index(TestKeychain::External),
|
||||
Some(last_external_index)
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.last_revealed_index(&TestKeychain::Internal),
|
||||
txout_index.last_revealed_index(TestKeychain::Internal),
|
||||
Some(last_internal_index)
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.revealed_keychain_spks(TestKeychain::External)
|
||||
.count(),
|
||||
last_external_index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.revealed_keychain_spks(TestKeychain::Internal)
|
||||
.count(),
|
||||
last_internal_index as usize + 1,
|
||||
);
|
||||
@@ -366,11 +317,11 @@ fn test_scan_with_lookahead() {
|
||||
&[(external_descriptor.descriptor_id(), spk_i)].into()
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.last_revealed_index(&TestKeychain::External),
|
||||
txout_index.last_revealed_index(TestKeychain::External),
|
||||
Some(spk_i)
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.last_used_index(&TestKeychain::External),
|
||||
txout_index.last_used_index(TestKeychain::External),
|
||||
Some(spk_i)
|
||||
);
|
||||
}
|
||||
@@ -406,11 +357,11 @@ fn test_wildcard_derivations() {
|
||||
// - next_derivation_index() == (0, true)
|
||||
// - derive_new() == ((0, <spk>), keychain::ChangeSet)
|
||||
// - next_unused() == ((0, <spk>), keychain::ChangeSet:is_empty())
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
|
||||
assert_eq!(txout_index.next_index(TestKeychain::External).unwrap(), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0_u32, external_spk_0.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 0)].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0_u32, external_spk_0.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
|
||||
@@ -422,20 +373,20 @@ fn test_wildcard_derivations() {
|
||||
// - next_derivation_index() = (26, true)
|
||||
// - derive_new() = ((26, <spk>), keychain::ChangeSet)
|
||||
// - next_unused() == ((16, <spk>), keychain::ChangeSet::is_empty())
|
||||
let _ = txout_index.reveal_to_target(&TestKeychain::External, 25);
|
||||
let _ = txout_index.reveal_to_target(TestKeychain::External, 25);
|
||||
|
||||
(0..=15)
|
||||
.chain([17, 20, 23])
|
||||
.for_each(|index| assert!(txout_index.mark_used(TestKeychain::External, index)));
|
||||
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External).unwrap(), (26, true));
|
||||
assert_eq!(txout_index.next_index(TestKeychain::External).unwrap(), (26, true));
|
||||
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External).unwrap();
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (26, external_spk_26));
|
||||
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 26)].into());
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (16, external_spk_16));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
|
||||
@@ -445,7 +396,7 @@ fn test_wildcard_derivations() {
|
||||
txout_index.mark_used(TestKeychain::External, index);
|
||||
});
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External).unwrap();
|
||||
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (27, external_spk_27));
|
||||
assert_eq!(&changeset.last_revealed, &[(external_descriptor.descriptor_id(), 27)].into());
|
||||
}
|
||||
@@ -473,21 +424,17 @@ fn test_non_wildcard_derivations() {
|
||||
// - when we derive a new script, script @ index 0
|
||||
// - when we get the next unused script, script @ index 0
|
||||
assert_eq!(
|
||||
txout_index.next_index(&TestKeychain::External).unwrap(),
|
||||
txout_index.next_index(TestKeychain::External).unwrap(),
|
||||
(0, true)
|
||||
);
|
||||
let (spk, changeset) = txout_index
|
||||
.reveal_next_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0, external_spk.clone()));
|
||||
assert_eq!(
|
||||
&changeset.last_revealed,
|
||||
&[(no_wildcard_descriptor.descriptor_id(), 0)].into()
|
||||
);
|
||||
|
||||
let (spk, changeset) = txout_index
|
||||
.next_unused_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0, external_spk.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
|
||||
@@ -498,24 +445,20 @@ fn test_non_wildcard_derivations() {
|
||||
// - derive new and next unused should return the old script
|
||||
// - store_up_to should not panic and return empty changeset
|
||||
assert_eq!(
|
||||
txout_index.next_index(&TestKeychain::External).unwrap(),
|
||||
txout_index.next_index(TestKeychain::External).unwrap(),
|
||||
(0, false)
|
||||
);
|
||||
txout_index.mark_used(TestKeychain::External, 0);
|
||||
|
||||
let (spk, changeset) = txout_index
|
||||
.reveal_next_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0, external_spk.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
|
||||
let (spk, changeset) = txout_index
|
||||
.next_unused_spk(&TestKeychain::External)
|
||||
.unwrap();
|
||||
let (spk, changeset) = txout_index.next_unused_spk(TestKeychain::External).unwrap();
|
||||
assert_eq!(spk, (0, external_spk.clone()));
|
||||
assert_eq!(&changeset.last_revealed, &[].into());
|
||||
let (revealed_spks, revealed_changeset) = txout_index
|
||||
.reveal_to_target(&TestKeychain::External, 200)
|
||||
.reveal_to_target(TestKeychain::External, 200)
|
||||
.unwrap();
|
||||
assert_eq!(revealed_spks.len(), 0);
|
||||
assert!(revealed_changeset.is_empty());
|
||||
@@ -523,7 +466,7 @@ fn test_non_wildcard_derivations() {
|
||||
// we check that spks_of_keychain returns a SpkIterator with just one element
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.revealed_keychain_spks(TestKeychain::External)
|
||||
.count(),
|
||||
1,
|
||||
);
|
||||
@@ -589,10 +532,10 @@ fn lookahead_to_target() {
|
||||
);
|
||||
|
||||
if let Some(last_revealed) = t.external_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::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 _ = index.reveal_to_target(TestKeychain::Internal, last_revealed);
|
||||
}
|
||||
|
||||
let keychain_test_cases = [
|
||||
@@ -619,7 +562,7 @@ fn lookahead_to_target() {
|
||||
}
|
||||
None => target,
|
||||
};
|
||||
index.lookahead_to_target(&keychain, target);
|
||||
index.lookahead_to_target(keychain.clone(), target);
|
||||
let keys = index
|
||||
.inner()
|
||||
.all_spks()
|
||||
@@ -636,51 +579,34 @@ fn lookahead_to_target() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_descriptor_no_change() {
|
||||
let secp = Secp256k1::signing_only();
|
||||
let (desc, _) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, DESCRIPTORS[0]).unwrap();
|
||||
let mut txout_index = KeychainTxOutIndex::<()>::default();
|
||||
assert_eq!(
|
||||
txout_index.insert_descriptor((), desc.clone()),
|
||||
Ok(keychain::ChangeSet {
|
||||
keychains_added: [((), desc.clone())].into(),
|
||||
last_revealed: Default::default()
|
||||
}),
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.insert_descriptor((), desc.clone()),
|
||||
Ok(keychain::ChangeSet::default()),
|
||||
"inserting the same descriptor for keychain should return an empty changeset",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(debug_assertions))]
|
||||
fn applying_changesets_one_by_one_vs_aggregate_must_have_same_result() {
|
||||
let desc = parse_descriptor(DESCRIPTORS[0]);
|
||||
let changesets: &[ChangeSet<TestKeychain>] = &[
|
||||
let changesets: &[ChangeSet] = &[
|
||||
ChangeSet {
|
||||
keychains_added: [(TestKeychain::Internal, desc.clone())].into(),
|
||||
last_revealed: [].into(),
|
||||
last_revealed: [(desc.descriptor_id(), 10)].into(),
|
||||
},
|
||||
ChangeSet {
|
||||
keychains_added: [(TestKeychain::External, desc.clone())].into(),
|
||||
last_revealed: [(desc.descriptor_id(), 12)].into(),
|
||||
},
|
||||
];
|
||||
|
||||
let mut indexer_a = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
indexer_a
|
||||
.insert_descriptor(TestKeychain::External, desc.clone())
|
||||
.expect("must insert keychain");
|
||||
for changeset in changesets {
|
||||
indexer_a.apply_changeset(changeset.clone());
|
||||
}
|
||||
|
||||
let mut indexer_b = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
indexer_b
|
||||
.insert_descriptor(TestKeychain::External, desc.clone())
|
||||
.expect("must insert keychain");
|
||||
let aggregate_changesets = changesets
|
||||
.iter()
|
||||
.cloned()
|
||||
.reduce(|mut agg, cs| {
|
||||
agg.append(cs);
|
||||
agg.merge(cs);
|
||||
agg
|
||||
})
|
||||
.expect("must aggregate changesets");
|
||||
@@ -737,7 +663,7 @@ fn when_querying_over_a_range_of_keychains_the_utxos_should_show_up() {
|
||||
let _ = indexer.insert_descriptor(i, descriptor.clone()).unwrap();
|
||||
if i != 4 {
|
||||
// skip one in the middle to see if uncovers any bugs
|
||||
indexer.reveal_next_spk(&i);
|
||||
indexer.reveal_next_spk(i);
|
||||
}
|
||||
tx.output.push(TxOut {
|
||||
script_pubkey: descriptor.at_derivation_index(0).unwrap().script_pubkey(),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
|
||||
use bdk_chain::{spk_txout::SpkTxOutIndex, Indexer};
|
||||
use bitcoin::{
|
||||
absolute, transaction, Amount, OutPoint, ScriptBuf, SignedAmount, Transaction, TxIn, TxOut,
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ use bdk_chain::{
|
||||
collections::*,
|
||||
local_chain::LocalChain,
|
||||
tx_graph::{ChangeSet, TxGraph},
|
||||
Anchor, Append, BlockId, ChainOracle, ChainPosition, ConfirmationHeightAnchor,
|
||||
Anchor, BlockId, ChainOracle, ChainPosition, ConfirmationBlockTime, Merge,
|
||||
};
|
||||
use bitcoin::{
|
||||
absolute, hashes::Hash, transaction, Amount, BlockHash, OutPoint, ScriptBuf, SignedAmount,
|
||||
@@ -935,7 +935,7 @@ fn test_chain_spends() {
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
let mut graph = TxGraph::<ConfirmationHeightAnchor>::default();
|
||||
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
|
||||
|
||||
let _ = graph.insert_tx(tx_0.clone());
|
||||
let _ = graph.insert_tx(tx_1.clone());
|
||||
@@ -944,9 +944,9 @@ fn test_chain_spends() {
|
||||
for (ht, tx) in [(95, &tx_0), (98, &tx_1)] {
|
||||
let _ = graph.insert_anchor(
|
||||
tx.compute_txid(),
|
||||
ConfirmationHeightAnchor {
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: ht,
|
||||
ConfirmationBlockTime {
|
||||
block_id: tip.get(ht).unwrap().block_id(),
|
||||
confirmation_time: 100,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -959,9 +959,12 @@ fn test_chain_spends() {
|
||||
OutPoint::new(tx_0.compute_txid(), 0)
|
||||
),
|
||||
Some((
|
||||
ChainPosition::Confirmed(&ConfirmationHeightAnchor {
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: 98
|
||||
ChainPosition::Confirmed(&ConfirmationBlockTime {
|
||||
block_id: BlockId {
|
||||
hash: tip.get(98).unwrap().hash(),
|
||||
height: 98,
|
||||
},
|
||||
confirmation_time: 100
|
||||
}),
|
||||
tx_1.compute_txid(),
|
||||
)),
|
||||
@@ -971,22 +974,15 @@ fn test_chain_spends() {
|
||||
assert_eq!(
|
||||
graph.get_chain_position(&local_chain, tip.block_id(), tx_0.compute_txid()),
|
||||
// Some(ObservedAs::Confirmed(&local_chain.get_block(95).expect("block expected"))),
|
||||
Some(ChainPosition::Confirmed(&ConfirmationHeightAnchor {
|
||||
anchor_block: tip.block_id(),
|
||||
confirmation_height: 95
|
||||
Some(ChainPosition::Confirmed(&ConfirmationBlockTime {
|
||||
block_id: BlockId {
|
||||
hash: tip.get(95).unwrap().hash(),
|
||||
height: 95,
|
||||
},
|
||||
confirmation_time: 100
|
||||
}))
|
||||
);
|
||||
|
||||
// Even if unconfirmed tx has a last_seen of 0, it can still be part of a chain spend.
|
||||
assert_eq!(
|
||||
graph.get_chain_spend(
|
||||
&local_chain,
|
||||
tip.block_id(),
|
||||
OutPoint::new(tx_0.compute_txid(), 1)
|
||||
),
|
||||
Some((ChainPosition::Unconfirmed(0), tx_2.compute_txid())),
|
||||
);
|
||||
|
||||
// Mark the unconfirmed as seen and check correct ObservedAs status is returned.
|
||||
let _ = graph.insert_seen_at(tx_2.compute_txid(), 1234567);
|
||||
|
||||
@@ -1059,9 +1055,9 @@ fn test_chain_spends() {
|
||||
.is_none());
|
||||
}
|
||||
|
||||
/// Ensure that `last_seen` values only increase during [`Append::append`].
|
||||
/// Ensure that `last_seen` values only increase during [`Merge::merge`].
|
||||
#[test]
|
||||
fn test_changeset_last_seen_append() {
|
||||
fn test_changeset_last_seen_merge() {
|
||||
let txid: Txid = h!("test txid");
|
||||
|
||||
let test_cases: &[(Option<u64>, Option<u64>)] = &[
|
||||
@@ -1084,7 +1080,7 @@ fn test_changeset_last_seen_append() {
|
||||
};
|
||||
assert!(!update.is_empty() || update_ls.is_none());
|
||||
|
||||
original.append(update);
|
||||
original.merge(update);
|
||||
assert_eq!(
|
||||
&original.last_seen.get(&txid).cloned(),
|
||||
Ord::max(original_ls, update_ls),
|
||||
@@ -1099,10 +1095,10 @@ fn update_last_seen_unconfirmed() {
|
||||
let txid = tx.compute_txid();
|
||||
|
||||
// insert a new tx
|
||||
// initially we have a last_seen of 0, and no anchors
|
||||
// initially we have a last_seen of None and no anchors
|
||||
let _ = graph.insert_tx(tx);
|
||||
let tx = graph.full_txs().next().unwrap();
|
||||
assert_eq!(tx.last_seen_unconfirmed, 0);
|
||||
assert_eq!(tx.last_seen_unconfirmed, None);
|
||||
assert!(tx.anchors.is_empty());
|
||||
|
||||
// higher timestamp should update last seen
|
||||
@@ -1117,7 +1113,56 @@ fn update_last_seen_unconfirmed() {
|
||||
let _ = graph.insert_anchor(txid, ());
|
||||
let changeset = graph.update_last_seen_unconfirmed(4);
|
||||
assert!(changeset.is_empty());
|
||||
assert_eq!(graph.full_txs().next().unwrap().last_seen_unconfirmed, 2);
|
||||
assert_eq!(
|
||||
graph
|
||||
.full_txs()
|
||||
.next()
|
||||
.unwrap()
|
||||
.last_seen_unconfirmed
|
||||
.unwrap(),
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transactions_inserted_into_tx_graph_are_not_canonical_until_they_have_an_anchor_in_best_chain() {
|
||||
let txs = vec![new_tx(0), new_tx(1)];
|
||||
let txids: Vec<Txid> = txs.iter().map(Transaction::compute_txid).collect();
|
||||
|
||||
// graph
|
||||
let mut graph = TxGraph::<BlockId>::new(txs);
|
||||
let full_txs: Vec<_> = graph.full_txs().collect();
|
||||
assert_eq!(full_txs.len(), 2);
|
||||
let unseen_txs: Vec<_> = graph.txs_with_no_anchor_or_last_seen().collect();
|
||||
assert_eq!(unseen_txs.len(), 2);
|
||||
|
||||
// chain
|
||||
let blocks: BTreeMap<u32, BlockHash> = [(0, h!("g")), (1, h!("A")), (2, h!("B"))]
|
||||
.into_iter()
|
||||
.collect();
|
||||
let chain = LocalChain::from_blocks(blocks).unwrap();
|
||||
let canonical_txs: Vec<_> = graph
|
||||
.list_canonical_txs(&chain, chain.tip().block_id())
|
||||
.collect();
|
||||
assert!(canonical_txs.is_empty());
|
||||
|
||||
// tx0 with seen_at should be returned by canonical txs
|
||||
let _ = graph.insert_seen_at(txids[0], 2);
|
||||
let mut canonical_txs = graph.list_canonical_txs(&chain, chain.tip().block_id());
|
||||
assert_eq!(
|
||||
canonical_txs.next().map(|tx| tx.tx_node.txid).unwrap(),
|
||||
txids[0]
|
||||
);
|
||||
drop(canonical_txs);
|
||||
|
||||
// tx1 with anchor is also canonical
|
||||
let _ = graph.insert_anchor(txids[1], block_id!(2, "B"));
|
||||
let canonical_txids: Vec<_> = graph
|
||||
.list_canonical_txs(&chain, chain.tip().block_id())
|
||||
.map(|tx| tx.tx_node.txid)
|
||||
.collect();
|
||||
assert!(canonical_txids.contains(&txids[1]));
|
||||
assert!(graph.txs_with_no_anchor_or_last_seen().next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -5,8 +5,8 @@ mod common;
|
||||
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use bdk_chain::{keychain::Balance, BlockId};
|
||||
use bitcoin::{Amount, OutPoint, Script};
|
||||
use bdk_chain::{Balance, BlockId};
|
||||
use bitcoin::{Amount, OutPoint, ScriptBuf};
|
||||
use common::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -15,7 +15,7 @@ struct Scenario<'a> {
|
||||
name: &'a str,
|
||||
/// Transaction templates
|
||||
tx_templates: &'a [TxTemplate<'a, BlockId>],
|
||||
/// Names of txs that must exist in the output of `list_chain_txs`
|
||||
/// Names of txs that must exist in the output of `list_canonical_txs`
|
||||
exp_chain_txs: HashSet<&'a str>,
|
||||
/// Outpoints that must exist in the output of `filter_chain_txouts`
|
||||
exp_chain_txouts: HashSet<(&'a str, u32)>,
|
||||
@@ -27,7 +27,7 @@ struct Scenario<'a> {
|
||||
|
||||
/// This test ensures that [`TxGraph`] will reliably filter out irrelevant transactions when
|
||||
/// presented with multiple conflicting transaction scenarios using the [`TxTemplate`] structure.
|
||||
/// This test also checks that [`TxGraph::list_chain_txs`], [`TxGraph::filter_chain_txouts`],
|
||||
/// This test also checks that [`TxGraph::list_canonical_txs`], [`TxGraph::filter_chain_txouts`],
|
||||
/// [`TxGraph::filter_chain_unspents`], and [`TxGraph::balance`] return correct data.
|
||||
#[test]
|
||||
fn test_tx_conflict_handling() {
|
||||
@@ -597,7 +597,7 @@ fn test_tx_conflict_handling() {
|
||||
let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter());
|
||||
|
||||
let txs = tx_graph
|
||||
.list_chain_txs(&local_chain, chain_tip)
|
||||
.list_canonical_txs(&local_chain, chain_tip)
|
||||
.map(|tx| tx.tx_node.txid)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_txs = scenario
|
||||
@@ -607,7 +607,7 @@ fn test_tx_conflict_handling() {
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
txs, exp_txs,
|
||||
"\n[{}] 'list_chain_txs' failed",
|
||||
"\n[{}] 'list_canonical_txs' failed",
|
||||
scenario.name
|
||||
);
|
||||
|
||||
@@ -659,7 +659,7 @@ fn test_tx_conflict_handling() {
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
spk_index.outpoints().iter().cloned(),
|
||||
|_, spk: &Script| spk_index.index_of_spk(spk).is_some(),
|
||||
|_, spk: ScriptBuf| spk_index.index_of_spk(spk).is_some(),
|
||||
);
|
||||
assert_eq!(
|
||||
balance, scenario.exp_balance,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_electrum"
|
||||
version = "0.15.0"
|
||||
version = "0.16.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.16.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.17.0" }
|
||||
electrum-client = { version = "0.20" }
|
||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use bdk_chain::{
|
||||
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
bitcoin::{block::Header, BlockHash, OutPoint, ScriptBuf, Transaction, Txid},
|
||||
collections::{BTreeMap, HashMap},
|
||||
local_chain::CheckPoint,
|
||||
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
||||
tx_graph::TxGraph,
|
||||
BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
|
||||
Anchor, BlockId, ConfirmationBlockTime,
|
||||
};
|
||||
use core::str::FromStr;
|
||||
use electrum_client::{ElectrumApi, Error, HeaderNotification};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
/// We include a chain suffix of a certain length for the purpose of robustness.
|
||||
const CHAIN_SUFFIX_LENGTH: u32 = 8;
|
||||
@@ -21,6 +23,8 @@ pub struct BdkElectrumClient<E> {
|
||||
pub inner: E,
|
||||
/// The transaction cache
|
||||
tx_cache: Mutex<HashMap<Txid, Arc<Transaction>>>,
|
||||
/// The header cache
|
||||
block_header_cache: Mutex<HashMap<u32, Header>>,
|
||||
}
|
||||
|
||||
impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
@@ -29,6 +33,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
Self {
|
||||
inner: client,
|
||||
tx_cache: Default::default(),
|
||||
block_header_cache: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +70,33 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
Ok(tx)
|
||||
}
|
||||
|
||||
/// Fetch block header of given `height`.
|
||||
///
|
||||
/// If it hits the cache it will return the cached version and avoid making the request.
|
||||
fn fetch_header(&self, height: u32) -> Result<Header, Error> {
|
||||
let block_header_cache = self.block_header_cache.lock().unwrap();
|
||||
|
||||
if let Some(header) = block_header_cache.get(&height) {
|
||||
return Ok(*header);
|
||||
}
|
||||
|
||||
drop(block_header_cache);
|
||||
|
||||
self.update_header(height)
|
||||
}
|
||||
|
||||
/// Update a block header at given `height`. Returns the updated header.
|
||||
fn update_header(&self, height: u32) -> Result<Header, Error> {
|
||||
let header = self.inner.block_header(height as usize)?;
|
||||
|
||||
self.block_header_cache
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(height, header);
|
||||
|
||||
Ok(header)
|
||||
}
|
||||
|
||||
/// Broadcasts a transaction to the network.
|
||||
///
|
||||
/// This is a re-export of [`ElectrumApi::transaction_broadcast`].
|
||||
@@ -88,87 +120,32 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<ElectrumFullScanResult<K>, Error> {
|
||||
let mut request_spks = request.spks_by_keychain;
|
||||
) -> Result<FullScanResult<K>, Error> {
|
||||
let (tip, latest_blocks) =
|
||||
fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?;
|
||||
let mut graph_update = TxGraph::<ConfirmationBlockTime>::default();
|
||||
let mut last_active_indices = BTreeMap::<K, u32>::new();
|
||||
|
||||
// We keep track of already-scanned spks just in case a reorg happens and we need to do a
|
||||
// rescan. We need to keep track of this as iterators in `keychain_spks` are "unbounded" so
|
||||
// cannot be collected. In addition, we keep track of whether an spk has an active tx
|
||||
// history for determining the `last_active_index`.
|
||||
// * key: (keychain, spk_index) that identifies the spk.
|
||||
// * val: (script_pubkey, has_tx_history).
|
||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||
|
||||
let update = loop {
|
||||
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip.clone())?;
|
||||
let mut graph_update = TxGraph::<ConfirmationHeightAnchor>::default();
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|cp| (cp.height(), cp))
|
||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||
|
||||
if !request_spks.is_empty() {
|
||||
if !scanned_spks.is_empty() {
|
||||
scanned_spks.append(
|
||||
&mut self.populate_with_spks(
|
||||
&cps,
|
||||
&mut graph_update,
|
||||
&mut scanned_spks
|
||||
.iter()
|
||||
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
|
||||
stop_gap,
|
||||
batch_size,
|
||||
)?,
|
||||
);
|
||||
}
|
||||
for (keychain, keychain_spks) in &mut request_spks {
|
||||
scanned_spks.extend(
|
||||
self.populate_with_spks(
|
||||
&cps,
|
||||
&mut graph_update,
|
||||
keychain_spks,
|
||||
stop_gap,
|
||||
batch_size,
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
|
||||
);
|
||||
}
|
||||
for (keychain, spks) in request.spks_by_keychain {
|
||||
if let Some(last_active_index) =
|
||||
self.populate_with_spks(&mut graph_update, spks, stop_gap, batch_size)?
|
||||
{
|
||||
last_active_indices.insert(keychain, last_active_index);
|
||||
}
|
||||
}
|
||||
|
||||
// check for reorgs during scan process
|
||||
let server_blockhash = self.inner.block_header(tip.height() as usize)?.block_hash();
|
||||
if tip.hash() != server_blockhash {
|
||||
continue; // reorg
|
||||
}
|
||||
let chain_update = chain_update(tip, &latest_blocks, graph_update.all_anchors())?;
|
||||
|
||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||
if fetch_prev_txouts {
|
||||
self.fetch_prev_txout(&mut graph_update)?;
|
||||
}
|
||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||
if fetch_prev_txouts {
|
||||
self.fetch_prev_txout(&mut graph_update)?;
|
||||
}
|
||||
|
||||
let chain_update = tip;
|
||||
|
||||
let keychain_update = request_spks
|
||||
.into_keys()
|
||||
.filter_map(|k| {
|
||||
scanned_spks
|
||||
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
|
||||
.rev()
|
||||
.find(|(_, (_, active))| *active)
|
||||
.map(|((_, i), _)| (k, *i))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
break FullScanResult {
|
||||
graph_update,
|
||||
chain_update,
|
||||
last_active_indices: keychain_update,
|
||||
};
|
||||
};
|
||||
|
||||
Ok(ElectrumFullScanResult(update))
|
||||
Ok(FullScanResult {
|
||||
graph_update,
|
||||
chain_update,
|
||||
last_active_indices,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
|
||||
@@ -190,32 +167,31 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
request: SyncRequest,
|
||||
batch_size: usize,
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<ElectrumSyncResult, Error> {
|
||||
) -> Result<SyncResult, Error> {
|
||||
let full_scan_req = FullScanRequest::from_chain_tip(request.chain_tip.clone())
|
||||
.set_spks_for_keychain((), request.spks.enumerate().map(|(i, spk)| (i as u32, spk)));
|
||||
let mut full_scan_res = self
|
||||
.full_scan(full_scan_req, usize::MAX, batch_size, false)?
|
||||
.with_confirmation_height_anchor();
|
||||
let mut full_scan_res = self.full_scan(full_scan_req, usize::MAX, batch_size, false)?;
|
||||
let (tip, latest_blocks) =
|
||||
fetch_tip_and_latest_blocks(&self.inner, request.chain_tip.clone())?;
|
||||
|
||||
let (tip, _) = construct_update_tip(&self.inner, request.chain_tip)?;
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|cp| (cp.height(), cp))
|
||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||
self.populate_with_txids(&mut full_scan_res.graph_update, request.txids)?;
|
||||
self.populate_with_outpoints(&mut full_scan_res.graph_update, request.outpoints)?;
|
||||
|
||||
self.populate_with_txids(&cps, &mut full_scan_res.graph_update, request.txids)?;
|
||||
self.populate_with_outpoints(&cps, &mut full_scan_res.graph_update, request.outpoints)?;
|
||||
let chain_update = chain_update(
|
||||
tip,
|
||||
&latest_blocks,
|
||||
full_scan_res.graph_update.all_anchors(),
|
||||
)?;
|
||||
|
||||
// Fetch previous `TxOut`s for fee calculation if flag is enabled.
|
||||
if fetch_prev_txouts {
|
||||
self.fetch_prev_txout(&mut full_scan_res.graph_update)?;
|
||||
}
|
||||
|
||||
Ok(ElectrumSyncResult(SyncResult {
|
||||
chain_update: full_scan_res.chain_update,
|
||||
Ok(SyncResult {
|
||||
chain_update,
|
||||
graph_update: full_scan_res.graph_update,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
/// Populate the `graph_update` with transactions/anchors associated with the given `spks`.
|
||||
@@ -223,84 +199,55 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
/// Transactions that contains an output with requested spk, or spends form an output with
|
||||
/// requested spk will be added to `graph_update`. Anchors of the aforementioned transactions are
|
||||
/// also included.
|
||||
///
|
||||
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
|
||||
fn populate_with_spks<I: Ord + Clone>(
|
||||
fn populate_with_spks(
|
||||
&self,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
|
||||
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||
mut spks: impl Iterator<Item = (u32, ScriptBuf)>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
|
||||
) -> Result<Option<u32>, Error> {
|
||||
let mut unused_spk_count = 0_usize;
|
||||
let mut scanned_spks = BTreeMap::new();
|
||||
let mut last_active_index = Option::<u32>::None;
|
||||
|
||||
loop {
|
||||
let spks = (0..batch_size)
|
||||
.map_while(|_| spks.next())
|
||||
.collect::<Vec<_>>();
|
||||
if spks.is_empty() {
|
||||
return Ok(scanned_spks);
|
||||
return Ok(last_active_index);
|
||||
}
|
||||
|
||||
let spk_histories = self
|
||||
.inner
|
||||
.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
|
||||
|
||||
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
|
||||
for ((spk_index, _spk), spk_history) in spks.into_iter().zip(spk_histories) {
|
||||
if spk_history.is_empty() {
|
||||
scanned_spks.insert(spk_index, (spk, false));
|
||||
unused_spk_count += 1;
|
||||
if unused_spk_count > stop_gap {
|
||||
return Ok(scanned_spks);
|
||||
unused_spk_count = unused_spk_count.saturating_add(1);
|
||||
if unused_spk_count >= stop_gap {
|
||||
return Ok(last_active_index);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
scanned_spks.insert(spk_index, (spk, true));
|
||||
last_active_index = Some(spk_index);
|
||||
unused_spk_count = 0;
|
||||
}
|
||||
|
||||
for tx_res in spk_history {
|
||||
let _ = graph_update.insert_tx(self.fetch_tx(tx_res.tx_hash)?);
|
||||
if let Some(anchor) = determine_tx_anchor(cps, tx_res.height, tx_res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(tx_res.tx_hash, anchor);
|
||||
}
|
||||
self.validate_merkle_for_anchor(graph_update, tx_res.tx_hash, tx_res.height)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
|
||||
// which we do not have by default. This data is needed to calculate the transaction fee.
|
||||
fn fetch_prev_txout(
|
||||
&self,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
) -> Result<(), Error> {
|
||||
let full_txs: Vec<Arc<Transaction>> =
|
||||
graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
|
||||
for tx in full_txs {
|
||||
for vin in &tx.input {
|
||||
let outpoint = vin.previous_output;
|
||||
let vout = outpoint.vout;
|
||||
let prev_tx = self.fetch_tx(outpoint.txid)?;
|
||||
let txout = prev_tx.output[vout as usize].clone();
|
||||
let _ = graph_update.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Populate the `graph_update` with associated transactions/anchors of `outpoints`.
|
||||
///
|
||||
/// Transactions in which the outpoint resides, and transactions that spend from the outpoint are
|
||||
/// included. Anchors of the aforementioned transactions are included.
|
||||
///
|
||||
/// Checkpoints (in `cps`) are used to create anchors. The `tx_cache` is self-explanatory.
|
||||
fn populate_with_outpoints(
|
||||
&self,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
) -> Result<(), Error> {
|
||||
for outpoint in outpoints {
|
||||
@@ -324,9 +271,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
if !has_residing && res.tx_hash == op_txid {
|
||||
has_residing = true;
|
||||
let _ = graph_update.insert_tx(Arc::clone(&op_tx));
|
||||
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
|
||||
}
|
||||
self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?;
|
||||
}
|
||||
|
||||
if !has_spending && res.tx_hash != op_txid {
|
||||
@@ -340,9 +285,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
continue;
|
||||
}
|
||||
let _ = graph_update.insert_tx(Arc::clone(&res_tx));
|
||||
if let Some(anchor) = determine_tx_anchor(cps, res.height, res.tx_hash) {
|
||||
let _ = graph_update.insert_anchor(res.tx_hash, anchor);
|
||||
}
|
||||
self.validate_merkle_for_anchor(graph_update, res.tx_hash, res.height)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,8 +295,7 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
/// Populate the `graph_update` with transactions/anchors of the provided `txids`.
|
||||
fn populate_with_txids(
|
||||
&self,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
graph_update: &mut TxGraph<ConfirmationHeightAnchor>,
|
||||
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
) -> Result<(), Error> {
|
||||
for txid in txids {
|
||||
@@ -371,120 +313,100 @@ impl<E: ElectrumApi> BdkElectrumClient<E> {
|
||||
|
||||
// because of restrictions of the Electrum API, we have to use the `script_get_history`
|
||||
// call to get confirmation status of our transaction
|
||||
let anchor = match self
|
||||
if let Some(r) = self
|
||||
.inner
|
||||
.script_get_history(spk)?
|
||||
.into_iter()
|
||||
.find(|r| r.tx_hash == txid)
|
||||
{
|
||||
Some(r) => determine_tx_anchor(cps, r.height, txid),
|
||||
None => continue,
|
||||
};
|
||||
self.validate_merkle_for_anchor(graph_update, txid, r.height)?;
|
||||
}
|
||||
|
||||
let _ = graph_update.insert_tx(tx);
|
||||
if let Some(anchor) = anchor {
|
||||
let _ = graph_update.insert_anchor(txid, anchor);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper function which checks if a transaction is confirmed by validating the merkle proof.
|
||||
// An anchor is inserted if the transaction is validated to be in a confirmed block.
|
||||
fn validate_merkle_for_anchor(
|
||||
&self,
|
||||
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||
txid: Txid,
|
||||
confirmation_height: i32,
|
||||
) -> Result<(), Error> {
|
||||
if let Ok(merkle_res) = self
|
||||
.inner
|
||||
.transaction_get_merkle(&txid, confirmation_height as usize)
|
||||
{
|
||||
let mut header = self.fetch_header(merkle_res.block_height as u32)?;
|
||||
let mut is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
|
||||
&txid,
|
||||
&header.merkle_root,
|
||||
&merkle_res,
|
||||
);
|
||||
|
||||
// Merkle validation will fail if the header in `block_header_cache` is outdated, so we
|
||||
// want to check if there is a new header and validate against the new one.
|
||||
if !is_confirmed_tx {
|
||||
header = self.update_header(merkle_res.block_height as u32)?;
|
||||
is_confirmed_tx = electrum_client::utils::validate_merkle_proof(
|
||||
&txid,
|
||||
&header.merkle_root,
|
||||
&merkle_res,
|
||||
);
|
||||
}
|
||||
|
||||
if is_confirmed_tx {
|
||||
let _ = graph_update.insert_anchor(
|
||||
txid,
|
||||
ConfirmationBlockTime {
|
||||
confirmation_time: header.time as u64,
|
||||
block_id: BlockId {
|
||||
height: merkle_res.block_height as u32,
|
||||
hash: header.block_hash(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Helper function which fetches the `TxOut`s of our relevant transactions' previous transactions,
|
||||
// which we do not have by default. This data is needed to calculate the transaction fee.
|
||||
fn fetch_prev_txout(
|
||||
&self,
|
||||
graph_update: &mut TxGraph<ConfirmationBlockTime>,
|
||||
) -> Result<(), Error> {
|
||||
let full_txs: Vec<Arc<Transaction>> =
|
||||
graph_update.full_txs().map(|tx_node| tx_node.tx).collect();
|
||||
for tx in full_txs {
|
||||
for vin in &tx.input {
|
||||
let outpoint = vin.previous_output;
|
||||
let vout = outpoint.vout;
|
||||
let prev_tx = self.fetch_tx(outpoint.txid)?;
|
||||
let txout = prev_tx.output[vout as usize].clone();
|
||||
let _ = graph_update.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of [`BdkElectrumClient::full_scan`].
|
||||
///
|
||||
/// This can be transformed into a [`FullScanResult`] with either [`ConfirmationHeightAnchor`] or
|
||||
/// [`ConfirmationTimeHeightAnchor`] anchor types.
|
||||
pub struct ElectrumFullScanResult<K>(FullScanResult<K, ConfirmationHeightAnchor>);
|
||||
|
||||
impl<K> ElectrumFullScanResult<K> {
|
||||
/// Return [`FullScanResult`] with [`ConfirmationHeightAnchor`].
|
||||
pub fn with_confirmation_height_anchor(self) -> FullScanResult<K, ConfirmationHeightAnchor> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return [`FullScanResult`] with [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// This requires additional calls to the Electrum server.
|
||||
pub fn with_confirmation_time_height_anchor(
|
||||
self,
|
||||
client: &BdkElectrumClient<impl ElectrumApi>,
|
||||
) -> Result<FullScanResult<K, ConfirmationTimeHeightAnchor>, Error> {
|
||||
let res = self.0;
|
||||
Ok(FullScanResult {
|
||||
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
|
||||
chain_update: res.chain_update,
|
||||
last_active_indices: res.last_active_indices,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of [`BdkElectrumClient::sync`].
|
||||
///
|
||||
/// This can be transformed into a [`SyncResult`] with either [`ConfirmationHeightAnchor`] or
|
||||
/// [`ConfirmationTimeHeightAnchor`] anchor types.
|
||||
pub struct ElectrumSyncResult(SyncResult<ConfirmationHeightAnchor>);
|
||||
|
||||
impl ElectrumSyncResult {
|
||||
/// Return [`SyncResult`] with [`ConfirmationHeightAnchor`].
|
||||
pub fn with_confirmation_height_anchor(self) -> SyncResult<ConfirmationHeightAnchor> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return [`SyncResult`] with [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// This requires additional calls to the Electrum server.
|
||||
pub fn with_confirmation_time_height_anchor(
|
||||
self,
|
||||
client: &BdkElectrumClient<impl ElectrumApi>,
|
||||
) -> Result<SyncResult<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let res = self.0;
|
||||
Ok(SyncResult {
|
||||
graph_update: try_into_confirmation_time_result(res.graph_update, &client.inner)?,
|
||||
chain_update: res.chain_update,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn try_into_confirmation_time_result(
|
||||
graph_update: TxGraph<ConfirmationHeightAnchor>,
|
||||
client: &impl ElectrumApi,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let relevant_heights = graph_update
|
||||
.all_anchors()
|
||||
.iter()
|
||||
.map(|(a, _)| a.confirmation_height)
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let height_to_time = relevant_heights
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(
|
||||
client
|
||||
.batch_block_header(relevant_heights)?
|
||||
.into_iter()
|
||||
.map(|bh| bh.time as u64),
|
||||
)
|
||||
.collect::<HashMap<u32, u64>>();
|
||||
|
||||
Ok(graph_update.map_anchors(|a| ConfirmationTimeHeightAnchor {
|
||||
anchor_block: a.anchor_block,
|
||||
confirmation_height: a.confirmation_height,
|
||||
confirmation_time: height_to_time[&a.confirmation_height],
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
||||
fn construct_update_tip(
|
||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`. The latest blocks are
|
||||
/// fetched to construct checkpoint updates with the proper [`BlockHash`] in case of re-org.
|
||||
fn fetch_tip_and_latest_blocks(
|
||||
client: &impl ElectrumApi,
|
||||
prev_tip: CheckPoint,
|
||||
) -> Result<(CheckPoint, Option<u32>), Error> {
|
||||
) -> Result<(CheckPoint, BTreeMap<u32, BlockHash>), Error> {
|
||||
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
|
||||
let new_tip_height = height as u32;
|
||||
|
||||
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
|
||||
// not need updating. We just return the previous tip and use that as the point of agreement.
|
||||
if new_tip_height < prev_tip.height() {
|
||||
return Ok((prev_tip.clone(), Some(prev_tip.height())));
|
||||
return Ok((prev_tip, BTreeMap::new()));
|
||||
}
|
||||
|
||||
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
|
||||
@@ -527,10 +449,13 @@ fn construct_update_tip(
|
||||
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
|
||||
|
||||
let new_tip = new_blocks
|
||||
.into_iter()
|
||||
.iter()
|
||||
// Prune `new_blocks` to only include blocks that are actually new.
|
||||
.filter(|(height, _)| Some(*height) > agreement_height)
|
||||
.map(|(height, hash)| BlockId { height, hash })
|
||||
.filter(|(height, _)| Some(*<&u32>::clone(height)) > agreement_height)
|
||||
.map(|(height, hash)| BlockId {
|
||||
height: *height,
|
||||
hash: *hash,
|
||||
})
|
||||
.fold(agreement_cp, |prev_cp, block| {
|
||||
Some(match prev_cp {
|
||||
Some(cp) => cp.push(block).expect("must extend checkpoint"),
|
||||
@@ -539,51 +464,28 @@ fn construct_update_tip(
|
||||
})
|
||||
.expect("must have at least one checkpoint");
|
||||
|
||||
Ok((new_tip, agreement_height))
|
||||
Ok((new_tip, new_blocks))
|
||||
}
|
||||
|
||||
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
|
||||
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
|
||||
///
|
||||
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
|
||||
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
|
||||
///
|
||||
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
|
||||
fn determine_tx_anchor(
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
raw_height: i32,
|
||||
txid: Txid,
|
||||
) -> Option<ConfirmationHeightAnchor> {
|
||||
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
|
||||
// height of 0. To avoid invalid representation in our data structures, we manually set
|
||||
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
|
||||
// unconfirmed for all other transactions.
|
||||
if txid
|
||||
== Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
|
||||
.expect("must deserialize genesis coinbase txid")
|
||||
{
|
||||
let anchor_block = cps.values().next()?.block_id();
|
||||
return Some(ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: 0,
|
||||
});
|
||||
}
|
||||
match raw_height {
|
||||
h if h <= 0 => {
|
||||
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
|
||||
None
|
||||
}
|
||||
h => {
|
||||
let h = h as u32;
|
||||
let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
|
||||
if h > anchor_block.height {
|
||||
None
|
||||
} else {
|
||||
Some(ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: h,
|
||||
})
|
||||
}
|
||||
// Add a corresponding checkpoint per anchor height if it does not yet exist. Checkpoints should not
|
||||
// surpass `latest_blocks`.
|
||||
fn chain_update<A: Anchor>(
|
||||
mut tip: CheckPoint,
|
||||
latest_blocks: &BTreeMap<u32, BlockHash>,
|
||||
anchors: &BTreeSet<(A, Txid)>,
|
||||
) -> Result<CheckPoint, Error> {
|
||||
for anchor in anchors {
|
||||
let height = anchor.0.anchor_block().height;
|
||||
|
||||
// Checkpoint uses the `BlockHash` from `latest_blocks` so that the hash will be consistent
|
||||
// in case of a re-org.
|
||||
if tip.get(height).is_none() && height <= tip.height() {
|
||||
let hash = match latest_blocks.get(&height) {
|
||||
Some(&hash) => hash,
|
||||
None => anchor.0.anchor_block().hash,
|
||||
};
|
||||
tip = tip.insert(BlockId { hash, height });
|
||||
}
|
||||
}
|
||||
Ok(tip)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use bdk_chain::{
|
||||
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
|
||||
keychain::Balance,
|
||||
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, Txid, WScriptHash},
|
||||
local_chain::LocalChain,
|
||||
spk_client::SyncRequest,
|
||||
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
|
||||
spk_client::{FullScanRequest, SyncRequest},
|
||||
spk_txout::SpkTxOutIndex,
|
||||
Balance, ConfirmationBlockTime, IndexedTxGraph,
|
||||
};
|
||||
use bdk_electrum::BdkElectrumClient;
|
||||
use bdk_testenv::{anyhow, bitcoincore_rpc::RpcApi, TestEnv};
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::str::FromStr;
|
||||
|
||||
fn get_balance(
|
||||
recv_chain: &LocalChain,
|
||||
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
|
||||
recv_graph: &IndexedTxGraph<ConfirmationBlockTime, SpkTxOutIndex<()>>,
|
||||
) -> anyhow::Result<Balance> {
|
||||
let chain_tip = recv_chain.tip().block_id();
|
||||
let outpoints = recv_graph.index.outpoints().clone();
|
||||
@@ -20,6 +22,222 @@ fn get_balance(
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||
let client = BdkElectrumClient::new(electrum_client);
|
||||
|
||||
let receive_address0 =
|
||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||
let receive_address1 =
|
||||
Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked();
|
||||
|
||||
let misc_spks = [
|
||||
receive_address0.script_pubkey(),
|
||||
receive_address1.script_pubkey(),
|
||||
];
|
||||
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
let txid1 = env.bitcoind.client.send_to_address(
|
||||
&receive_address1,
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let txid2 = env.bitcoind.client.send_to_address(
|
||||
&receive_address0,
|
||||
Amount::from_sat(20000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
env.mine_blocks(1, None)?;
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
let sync_update = {
|
||||
let request = SyncRequest::from_chain_tip(cp_tip.clone()).set_spks(misc_spks);
|
||||
client.sync(request, 1, true)?
|
||||
};
|
||||
|
||||
assert!(
|
||||
{
|
||||
let update_cps = sync_update
|
||||
.chain_update
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let superset_cps = cp_tip
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
superset_cps.is_superset(&update_cps)
|
||||
},
|
||||
"update should not alter original checkpoint tip since we already started with all checkpoints",
|
||||
);
|
||||
|
||||
let graph_update = sync_update.graph_update;
|
||||
// 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_unsigned()
|
||||
.expect("valid `Amount`");
|
||||
|
||||
// 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];
|
||||
expected_txids.sort();
|
||||
assert_eq!(graph_update_txids, expected_txids);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test the bounds of the address scan depending on the `stop_gap`.
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let electrum_client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||
let client = BdkElectrumClient::new(electrum_client);
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
|
||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||
let addresses = [
|
||||
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
|
||||
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
|
||||
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
|
||||
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
|
||||
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
|
||||
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
|
||||
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
|
||||
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
|
||||
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
|
||||
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
|
||||
];
|
||||
let addresses: Vec<_> = addresses
|
||||
.into_iter()
|
||||
.map(|s| Address::from_str(s).unwrap().assume_checked())
|
||||
.collect();
|
||||
let spks: Vec<_> = addresses
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
||||
.collect();
|
||||
|
||||
// Then receive coins on the 4th address.
|
||||
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
||||
&addresses[3],
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
env.mine_blocks(1, None)?;
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
|
||||
// use a full checkpoint linked list (since this is not what we are testing)
|
||||
let cp_tip = env.make_checkpoint_tip();
|
||||
|
||||
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||
// will.
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 3, 1, false)?
|
||||
};
|
||||
assert!(full_scan_update.graph_update.full_txs().next().is_none());
|
||||
assert!(full_scan_update.last_active_indices.is_empty());
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 4, 1, false)?
|
||||
};
|
||||
assert_eq!(
|
||||
full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.next()
|
||||
.unwrap()
|
||||
.txid,
|
||||
txid_4th_addr
|
||||
);
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
|
||||
// Now receive a coin on the last address.
|
||||
let txid_last_addr = env.bitcoind.client.send_to_address(
|
||||
&addresses[addresses.len() - 1],
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
env.mine_blocks(1, None)?;
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
|
||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 5, 1, false)?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 3);
|
||||
let full_scan_update = {
|
||||
let request =
|
||||
FullScanRequest::from_chain_tip(cp_tip.clone()).set_spks_for_keychain(0, spks.clone());
|
||||
client.full_scan(request, 6, 1, false)?
|
||||
};
|
||||
let txs: HashSet<_> = full_scan_update
|
||||
.graph_update
|
||||
.full_txs()
|
||||
.map(|tx| tx.txid)
|
||||
.collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
assert_eq!(full_scan_update.last_active_indices[&0], 9);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that [`ElectrumExt`] can sync properly.
|
||||
///
|
||||
/// 1. Mine 101 blocks.
|
||||
@@ -45,7 +263,7 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
|
||||
|
||||
// Setup receiver.
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
||||
let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
recv_index
|
||||
@@ -62,14 +280,11 @@ fn scan_detects_confirmed_tx() -> anyhow::Result<()> {
|
||||
|
||||
// Sync up to tip.
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip())
|
||||
.chain_spks(core::iter::once(spk_to_track)),
|
||||
5,
|
||||
true,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
let update = client.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks(core::iter::once(spk_to_track)),
|
||||
5,
|
||||
true,
|
||||
)?;
|
||||
|
||||
let _ = recv_chain
|
||||
.apply_update(update.chain_update)
|
||||
@@ -138,7 +353,7 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
|
||||
// Setup receiver.
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
||||
let mut recv_graph = IndexedTxGraph::<ConfirmationBlockTime, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
recv_index
|
||||
@@ -148,20 +363,20 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
env.mine_blocks(101, Some(addr_to_mine))?;
|
||||
|
||||
// Create transactions that are tracked by our receiver.
|
||||
let mut txids = vec![];
|
||||
let mut hashes = vec![];
|
||||
for _ in 0..REORG_COUNT {
|
||||
env.send(&addr_to_track, SEND_AMOUNT)?;
|
||||
env.mine_blocks(1, None)?;
|
||||
txids.push(env.send(&addr_to_track, SEND_AMOUNT)?);
|
||||
hashes.extend(env.mine_blocks(1, None)?);
|
||||
}
|
||||
|
||||
// Sync up to tip.
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||
5,
|
||||
false,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
let update = client.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||
5,
|
||||
false,
|
||||
)?;
|
||||
|
||||
let _ = recv_chain
|
||||
.apply_update(update.chain_update)
|
||||
@@ -170,6 +385,13 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
|
||||
// Retain a snapshot of all anchors before reorg process.
|
||||
let initial_anchors = update.graph_update.all_anchors();
|
||||
let anchors: Vec<_> = initial_anchors.iter().cloned().collect();
|
||||
assert_eq!(anchors.len(), REORG_COUNT);
|
||||
for i in 0..REORG_COUNT {
|
||||
let (anchor, txid) = anchors[i];
|
||||
assert_eq!(anchor.block_id.hash, hashes[i]);
|
||||
assert_eq!(txid, txids[i]);
|
||||
}
|
||||
|
||||
// Check if initial balance is correct.
|
||||
assert_eq!(
|
||||
@@ -186,29 +408,24 @@ fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
env.reorg_empty_blocks(depth)?;
|
||||
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let update = client
|
||||
.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||
5,
|
||||
false,
|
||||
)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
let update = client.sync(
|
||||
SyncRequest::from_chain_tip(recv_chain.tip()).chain_spks([spk_to_track.clone()]),
|
||||
5,
|
||||
false,
|
||||
)?;
|
||||
|
||||
let _ = recv_chain
|
||||
.apply_update(update.chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
|
||||
// Check to see if a new anchor is added during current reorg.
|
||||
if !initial_anchors.is_superset(update.graph_update.all_anchors()) {
|
||||
println!("New anchor added at reorg depth {}", depth);
|
||||
}
|
||||
// Check that no new anchors are added during current reorg.
|
||||
assert!(initial_anchors.is_superset(update.graph_update.all_anchors()));
|
||||
let _ = recv_graph.apply_update(update.graph_update);
|
||||
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT * (REORG_COUNT - depth) as u64,
|
||||
trusted_pending: SEND_AMOUNT * depth as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.15.0"
|
||||
version = "0.16.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.16.0", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.17.0", default-features = false }
|
||||
esplora-client = { version = "0.8.0", default-features = false }
|
||||
async-trait = { version = "0.1.66", optional = true }
|
||||
futures = { version = "0.3.26", optional = true }
|
||||
|
||||
@@ -6,7 +6,7 @@ use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
collections::BTreeMap,
|
||||
local_chain::CheckPoint,
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
BlockId, ConfirmationBlockTime, TxGraph,
|
||||
};
|
||||
use bdk_chain::{Anchor, Indexed};
|
||||
use esplora_client::{Amount, TxStatus};
|
||||
@@ -231,7 +231,7 @@ async fn chain_update<A: Anchor>(
|
||||
}
|
||||
|
||||
/// This performs a full scan to get an update for the [`TxGraph`] and
|
||||
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
|
||||
/// [`KeychainTxOutIndex`](bdk_chain::indexer::keychain_txout::KeychainTxOutIndex).
|
||||
async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
||||
client: &esplora_client::AsyncClient,
|
||||
keychain_spks: BTreeMap<
|
||||
@@ -240,10 +240,10 @@ async fn full_scan_for_index_and_graph<K: Ord + Clone + Send>(
|
||||
>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||
) -> Result<(TxGraph<ConfirmationBlockTime>, BTreeMap<K, u32>), Error> {
|
||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut graph = TxGraph::<ConfirmationBlockTime>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
@@ -333,7 +333,7 @@ async fn sync_for_index_and_graph(
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
|
||||
let mut graph = full_scan_for_index_and_graph(
|
||||
client,
|
||||
[(
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::thread::JoinHandle;
|
||||
use std::usize;
|
||||
|
||||
use bdk_chain::collections::BTreeMap;
|
||||
use bdk_chain::spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult};
|
||||
use bdk_chain::{
|
||||
bitcoin::{Amount, BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
local_chain::CheckPoint,
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
BlockId, ConfirmationBlockTime, TxGraph,
|
||||
};
|
||||
use bdk_chain::{Anchor, Indexed};
|
||||
use esplora_client::TxStatus;
|
||||
@@ -214,16 +213,16 @@ fn chain_update<A: Anchor>(
|
||||
}
|
||||
|
||||
/// This performs a full scan to get an update for the [`TxGraph`] and
|
||||
/// [`KeychainTxOutIndex`](bdk_chain::keychain::KeychainTxOutIndex).
|
||||
/// [`KeychainTxOutIndex`](bdk_chain::indexer::keychain_txout::KeychainTxOutIndex).
|
||||
fn full_scan_for_index_and_graph_blocking<K: Ord + Clone>(
|
||||
client: &esplora_client::BlockingClient,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = Indexed<ScriptBuf>>>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||
) -> Result<(TxGraph<ConfirmationBlockTime>, BTreeMap<K, u32>), Error> {
|
||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut tx_graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut tx_graph = TxGraph::<ConfirmationBlockTime>::default();
|
||||
let mut last_active_indices = BTreeMap::<K, u32>::new();
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
@@ -316,7 +315,7 @@ fn sync_for_index_and_graph_blocking(
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
) -> Result<TxGraph<ConfirmationBlockTime>, Error> {
|
||||
let (mut tx_graph, _) = full_scan_for_index_and_graph_blocking(
|
||||
client,
|
||||
{
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
|
||||
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
|
||||
|
||||
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
|
||||
use bdk_chain::{BlockId, ConfirmationBlockTime};
|
||||
use esplora_client::TxStatus;
|
||||
|
||||
pub use esplora_client;
|
||||
@@ -31,7 +31,7 @@ mod async_ext;
|
||||
#[cfg(feature = "async")]
|
||||
pub use async_ext::*;
|
||||
|
||||
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor> {
|
||||
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationBlockTime> {
|
||||
if let TxStatus {
|
||||
block_height: Some(height),
|
||||
block_hash: Some(hash),
|
||||
@@ -39,9 +39,8 @@ fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor>
|
||||
..
|
||||
} = status.clone()
|
||||
{
|
||||
Some(ConfirmationTimeHeightAnchor {
|
||||
anchor_block: BlockId { height, hash },
|
||||
confirmation_height: height,
|
||||
Some(ConfirmationBlockTime {
|
||||
block_id: BlockId { height, hash },
|
||||
confirmation_time: time,
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_file_store"
|
||||
version = "0.13.0"
|
||||
version = "0.14.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.16.0", features = [ "serde", "miniscript" ] }
|
||||
bdk_chain = { path = "../chain", version = "0.17.0", features = [ "serde", "miniscript" ] }
|
||||
bincode = { version = "1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::{bincode_options, EntryIter, FileError, IterError};
|
||||
use bdk_chain::Append;
|
||||
use bdk_chain::Merge;
|
||||
use bincode::Options;
|
||||
use std::{
|
||||
fmt::{self, Debug},
|
||||
@@ -22,7 +22,7 @@ where
|
||||
|
||||
impl<C> Store<C>
|
||||
where
|
||||
C: Append
|
||||
C: Merge
|
||||
+ serde::Serialize
|
||||
+ serde::de::DeserializeOwned
|
||||
+ core::marker::Send
|
||||
@@ -147,7 +147,7 @@ where
|
||||
}
|
||||
};
|
||||
match &mut changeset {
|
||||
Some(changeset) => changeset.append(next_changeset),
|
||||
Some(changeset) => changeset.merge(next_changeset),
|
||||
changeset => *changeset = Some(next_changeset),
|
||||
}
|
||||
}
|
||||
@@ -365,7 +365,7 @@ mod test {
|
||||
assert_eq!(
|
||||
err.changeset,
|
||||
changesets.iter().cloned().reduce(|mut acc, cs| {
|
||||
Append::append(&mut acc, cs);
|
||||
Merge::merge(&mut acc, cs);
|
||||
acc
|
||||
}),
|
||||
"should recover all changesets that are written in full",
|
||||
@@ -386,7 +386,7 @@ mod test {
|
||||
.cloned()
|
||||
.chain(core::iter::once(last_changeset.clone()))
|
||||
.reduce(|mut acc, cs| {
|
||||
Append::append(&mut acc, cs);
|
||||
Merge::merge(&mut acc, cs);
|
||||
acc
|
||||
}),
|
||||
"should recover all changesets",
|
||||
@@ -422,13 +422,13 @@ mod test {
|
||||
.take(read_count)
|
||||
.map(|r| r.expect("must read valid changeset"))
|
||||
.fold(TestChangeSet::default(), |mut acc, v| {
|
||||
Append::append(&mut acc, v);
|
||||
Merge::merge(&mut acc, v);
|
||||
acc
|
||||
});
|
||||
// We write after a short read.
|
||||
db.append_changeset(&last_changeset)
|
||||
.expect("last write must succeed");
|
||||
Append::append(&mut exp_aggregation, last_changeset.clone());
|
||||
Merge::merge(&mut exp_aggregation, last_changeset.clone());
|
||||
drop(db);
|
||||
|
||||
// We open the file again and check whether aggregate changeset is expected.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_hwi"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
@@ -9,5 +9,5 @@ license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_wallet = { path = "../wallet", version = "1.0.0-alpha.13" }
|
||||
bdk_wallet = { path = "../wallet", version = "1.0.0-beta.1" }
|
||||
hwi = { version = "0.9.0", features = [ "miniscript"] }
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
//! used with hardware wallets.
|
||||
//! ```no_run
|
||||
//! # use bdk_wallet::bitcoin::Network;
|
||||
//! # use bdk_wallet::descriptor::Descriptor;
|
||||
//! # use bdk_wallet::signer::SignerOrdering;
|
||||
//! # use bdk_hwi::HWISigner;
|
||||
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! # use std::str::FromStr;
|
||||
//! #
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let mut devices = HWIClient::enumerate()?;
|
||||
@@ -18,11 +20,7 @@
|
||||
//! let first_device = devices.remove(0)?;
|
||||
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
|
||||
//!
|
||||
//! # let mut wallet = Wallet::new(
|
||||
//! # "",
|
||||
//! # "",
|
||||
//! # Network::Testnet,
|
||||
//! # )?;
|
||||
//! # let mut wallet = Wallet::create("", "").network(Network::Testnet).create_wallet_no_persist()?;
|
||||
//! #
|
||||
//! // Adding the hardware signer to the BDK wallet
|
||||
//! wallet.add_signer(
|
||||
@@ -35,7 +33,7 @@
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! [`TransactionSigner`]: bdk_wallet::wallet::signer::TransactionSigner
|
||||
//! [`TransactionSigner`]: bdk_wallet::signer::TransactionSigner
|
||||
|
||||
mod signer;
|
||||
pub use signer::*;
|
||||
|
||||
@@ -83,7 +83,7 @@ impl TransactionSigner for HWISigner {
|
||||
// Arc::new(custom_signer),
|
||||
// );
|
||||
//
|
||||
// let addr = wallet.get_address(bdk_wallet::wallet::AddressIndex::LastUnused);
|
||||
// let addr = wallet.get_address(bdk_wallet::AddressIndex::LastUnused);
|
||||
// let mut builder = wallet.build_tx();
|
||||
// builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
// let (mut psbt, _) = builder.finish().unwrap();
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
[package]
|
||||
name = "bdk_sqlite"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_sqlite"
|
||||
description = "A simple SQLite relational database client for persisting bdk_chain data."
|
||||
keywords = ["bitcoin", "persist", "persistence", "bdk", "sqlite"]
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.16.0", features = ["serde", "miniscript"] }
|
||||
rusqlite = { version = "0.31.0", features = ["bundled"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
@@ -1,8 +0,0 @@
|
||||
# BDK SQLite
|
||||
|
||||
This is a simple [SQLite] relational database client for persisting [`bdk_chain`] changesets.
|
||||
|
||||
The main structure is `Store` which persists `CombinedChangeSet` data into a SQLite database file.
|
||||
|
||||
[`bdk_chain`]:https://docs.rs/bdk_chain/latest/bdk_chain/
|
||||
[SQLite]: https://www.sqlite.org/index.html
|
||||
@@ -1,69 +0,0 @@
|
||||
-- schema version control
|
||||
CREATE TABLE version
|
||||
(
|
||||
version INTEGER
|
||||
) STRICT;
|
||||
INSERT INTO version
|
||||
VALUES (1);
|
||||
|
||||
-- network is the valid network for all other table data
|
||||
CREATE TABLE network
|
||||
(
|
||||
name TEXT UNIQUE NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- keychain is the json serialized keychain structure as JSONB,
|
||||
-- descriptor is the complete descriptor string,
|
||||
-- descriptor_id is a sha256::Hash id of the descriptor string w/o the checksum,
|
||||
-- last revealed index is a u32
|
||||
CREATE TABLE keychain
|
||||
(
|
||||
keychain BLOB PRIMARY KEY NOT NULL,
|
||||
descriptor TEXT NOT NULL,
|
||||
descriptor_id BLOB NOT NULL,
|
||||
last_revealed INTEGER
|
||||
) STRICT;
|
||||
|
||||
-- hash is block hash hex string,
|
||||
-- block height is a u32,
|
||||
CREATE TABLE block
|
||||
(
|
||||
hash TEXT PRIMARY KEY NOT NULL,
|
||||
height INTEGER NOT NULL
|
||||
) STRICT;
|
||||
|
||||
-- txid is transaction hash hex string (reversed)
|
||||
-- whole_tx is a consensus encoded transaction,
|
||||
-- last seen is a u64 unix epoch seconds
|
||||
CREATE TABLE tx
|
||||
(
|
||||
txid TEXT PRIMARY KEY NOT NULL,
|
||||
whole_tx BLOB,
|
||||
last_seen INTEGER
|
||||
) STRICT;
|
||||
|
||||
-- Outpoint txid hash hex string (reversed)
|
||||
-- Outpoint vout
|
||||
-- TxOut value as SATs
|
||||
-- TxOut script consensus encoded
|
||||
CREATE TABLE txout
|
||||
(
|
||||
txid TEXT NOT NULL,
|
||||
vout INTEGER NOT NULL,
|
||||
value INTEGER NOT NULL,
|
||||
script BLOB NOT NULL,
|
||||
PRIMARY KEY (txid, vout)
|
||||
) STRICT;
|
||||
|
||||
-- join table between anchor and tx
|
||||
-- block hash hex string
|
||||
-- anchor is a json serialized Anchor structure as JSONB,
|
||||
-- txid is transaction hash hex string (reversed)
|
||||
CREATE TABLE anchor_tx
|
||||
(
|
||||
block_hash TEXT NOT NULL,
|
||||
anchor BLOB NOT NULL,
|
||||
txid TEXT NOT NULL REFERENCES tx (txid),
|
||||
UNIQUE (anchor, txid),
|
||||
FOREIGN KEY (block_hash) REFERENCES block(hash)
|
||||
) STRICT;
|
||||
@@ -1,34 +0,0 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
|
||||
mod schema;
|
||||
mod store;
|
||||
|
||||
use bdk_chain::bitcoin::Network;
|
||||
pub use rusqlite;
|
||||
pub use store::Store;
|
||||
|
||||
/// Error that occurs while reading or writing change sets with the SQLite database.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Invalid network, cannot change the one already stored in the database.
|
||||
Network { expected: Network, given: Network },
|
||||
/// SQLite error.
|
||||
Sqlite(rusqlite::Error),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Network { expected, given } => write!(
|
||||
f,
|
||||
"network error trying to read or write change set, expected {}, given {}",
|
||||
expected, given
|
||||
),
|
||||
Self::Sqlite(e) => write!(f, "sqlite error reading or writing changeset: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
@@ -1,96 +0,0 @@
|
||||
use crate::Store;
|
||||
use rusqlite::{named_params, Connection, Error};
|
||||
|
||||
const SCHEMA_0: &str = include_str!("../schema/schema_0.sql");
|
||||
const MIGRATIONS: &[&str] = &[SCHEMA_0];
|
||||
|
||||
/// Schema migration related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Migrate sqlite db schema to latest version.
|
||||
pub(crate) fn migrate(conn: &mut Connection) -> Result<(), Error> {
|
||||
let stmts = &MIGRATIONS
|
||||
.iter()
|
||||
.flat_map(|stmt| {
|
||||
// remove comment lines
|
||||
let s = stmt
|
||||
.split('\n')
|
||||
.filter(|l| !l.starts_with("--") && !l.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
// split into statements
|
||||
s.split(';')
|
||||
// remove extra spaces
|
||||
.map(|s| {
|
||||
s.trim()
|
||||
.split(' ')
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
// remove empty statements
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
let version = Self::get_schema_version(conn)?;
|
||||
let stmts = &stmts[(version as usize)..];
|
||||
|
||||
// begin transaction, all migration statements and new schema version commit or rollback
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
// execute every statement and return `Some` new schema version
|
||||
// if execution fails, return `Error::Rusqlite`
|
||||
// if no statements executed returns `None`
|
||||
let new_version = stmts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|version_stmt| {
|
||||
tx.execute(version_stmt.1.as_str(), [])
|
||||
// map result value to next migration version
|
||||
.map(|_| version_stmt.0 as i32 + version + 1)
|
||||
})
|
||||
.last()
|
||||
.transpose()?;
|
||||
|
||||
// if `Some` new statement version, set new schema version
|
||||
if let Some(version) = new_version {
|
||||
Self::set_schema_version(&tx, version)?;
|
||||
}
|
||||
|
||||
// commit transaction
|
||||
tx.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_schema_version(conn: &Connection) -> rusqlite::Result<i32> {
|
||||
let statement = conn.prepare_cached("SELECT version FROM version");
|
||||
match statement {
|
||||
Err(Error::SqliteFailure(e, Some(msg))) => {
|
||||
if msg == "no such table: version" {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(Error::SqliteFailure(e, Some(msg)))
|
||||
}
|
||||
}
|
||||
Ok(mut stmt) => {
|
||||
let mut rows = stmt.query([])?;
|
||||
match rows.next()? {
|
||||
Some(row) => {
|
||||
let version: i32 = row.get(0)?;
|
||||
Ok(version)
|
||||
}
|
||||
None => Ok(0),
|
||||
}
|
||||
}
|
||||
_ => Ok(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_schema_version(conn: &Connection, version: i32) -> rusqlite::Result<usize> {
|
||||
conn.execute(
|
||||
"UPDATE version SET version=:version",
|
||||
named_params! {":version": version},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,758 +0,0 @@
|
||||
use bdk_chain::bitcoin::consensus::{deserialize, serialize};
|
||||
use bdk_chain::bitcoin::hashes::Hash;
|
||||
use bdk_chain::bitcoin::{Amount, Network, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||
use bdk_chain::bitcoin::{BlockHash, Txid};
|
||||
use bdk_chain::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
use rusqlite::{named_params, Connection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fmt::Debug;
|
||||
use std::marker::PhantomData;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::Error;
|
||||
use bdk_chain::CombinedChangeSet;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph, keychain, local_chain, tx_graph, Anchor, Append, DescriptorExt, DescriptorId,
|
||||
};
|
||||
|
||||
/// Persists data in to a relational schema based [SQLite] database file.
|
||||
///
|
||||
/// The changesets loaded or stored represent changes to keychain and blockchain data.
|
||||
///
|
||||
/// [SQLite]: https://www.sqlite.org/index.html
|
||||
pub struct Store<K, A> {
|
||||
// A rusqlite connection to the SQLite database. Uses a Mutex for thread safety.
|
||||
conn: Mutex<Connection>,
|
||||
keychain_marker: PhantomData<K>,
|
||||
anchor_marker: PhantomData<A>,
|
||||
}
|
||||
|
||||
impl<K, A> Debug for Store<K, A> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
Debug::fmt(&self.conn, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
/// Creates a new store from a [`Connection`].
|
||||
pub fn new(mut conn: Connection) -> Result<Self, rusqlite::Error> {
|
||||
Self::migrate(&mut conn)?;
|
||||
|
||||
Ok(Self {
|
||||
conn: Mutex::new(conn),
|
||||
keychain_marker: Default::default(),
|
||||
anchor_marker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn db_transaction(&mut self) -> Result<rusqlite::Transaction, Error> {
|
||||
let connection = self.conn.get_mut().expect("unlocked connection mutex");
|
||||
connection.transaction().map_err(Error::Sqlite)
|
||||
}
|
||||
}
|
||||
|
||||
/// Network table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert [`Network`] for which all other tables data is valid.
|
||||
///
|
||||
/// Error if trying to insert different network value.
|
||||
fn insert_network(
|
||||
current_network: &Option<Network>,
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
network_changeset: &Option<Network>,
|
||||
) -> Result<(), Error> {
|
||||
if let Some(network) = network_changeset {
|
||||
match current_network {
|
||||
// if no network change do nothing
|
||||
Some(current_network) if current_network == network => Ok(()),
|
||||
// if new network not the same as current, error
|
||||
Some(current_network) => Err(Error::Network {
|
||||
expected: *current_network,
|
||||
given: *network,
|
||||
}),
|
||||
// insert network if none exists
|
||||
None => {
|
||||
let insert_network_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO network (name) VALUES (:name)")
|
||||
.expect("insert network statement");
|
||||
let name = network.to_string();
|
||||
insert_network_stmt
|
||||
.execute(named_params! {":name": name })
|
||||
.map_err(Error::Sqlite)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the valid [`Network`] for this database, or `None` if not set.
|
||||
fn select_network(db_transaction: &rusqlite::Transaction) -> Result<Option<Network>, Error> {
|
||||
let mut select_network_stmt = db_transaction
|
||||
.prepare_cached("SELECT name FROM network WHERE rowid = 1")
|
||||
.expect("select network statement");
|
||||
|
||||
let network = select_network_stmt
|
||||
.query_row([], |row| {
|
||||
let network = row.get_unwrap::<usize, String>(0);
|
||||
let network = Network::from_str(network.as_str()).expect("valid network");
|
||||
Ok(network)
|
||||
})
|
||||
.map_err(Error::Sqlite);
|
||||
match network {
|
||||
Ok(network) => Ok(Some(network)),
|
||||
Err(Error::Sqlite(rusqlite::Error::QueryReturnedNoRows)) => Ok(None),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert or delete local chain blocks.
|
||||
///
|
||||
/// Error if trying to insert existing block hash.
|
||||
fn insert_or_delete_blocks(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
chain_changeset: &local_chain::ChangeSet,
|
||||
) -> Result<(), Error> {
|
||||
for (height, hash) in chain_changeset.iter() {
|
||||
match hash {
|
||||
// add new hash at height
|
||||
Some(hash) => {
|
||||
let insert_block_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO block (hash, height) VALUES (:hash, :height)")
|
||||
.expect("insert block statement");
|
||||
let hash = hash.to_string();
|
||||
insert_block_stmt
|
||||
.execute(named_params! {":hash": hash, ":height": height })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
// delete block at height
|
||||
None => {
|
||||
let delete_block_stmt = &mut db_transaction
|
||||
.prepare_cached("DELETE FROM block WHERE height IS :height")
|
||||
.expect("delete block statement");
|
||||
delete_block_stmt
|
||||
.execute(named_params! {":height": height })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all blocks.
|
||||
fn select_blocks(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<u32, Option<BlockHash>>, Error> {
|
||||
let mut select_blocks_stmt = db_transaction
|
||||
.prepare_cached("SELECT height, hash FROM block")
|
||||
.expect("select blocks statement");
|
||||
|
||||
let blocks = select_blocks_stmt
|
||||
.query_map([], |row| {
|
||||
let height = row.get_unwrap::<usize, u32>(0);
|
||||
let hash = row.get_unwrap::<usize, String>(1);
|
||||
let hash = Some(BlockHash::from_str(hash.as_str()).expect("block hash"));
|
||||
Ok((height, hash))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Keychain table related functions.
|
||||
///
|
||||
/// The keychain objects are stored as [`JSONB`] data.
|
||||
/// [`JSONB`]: https://sqlite.org/json1.html#jsonb
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + Send,
|
||||
{
|
||||
/// Insert keychain with descriptor and last active index.
|
||||
///
|
||||
/// If keychain exists only update last active index.
|
||||
fn insert_keychains(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
let keychain_changeset = &tx_graph_changeset.indexer;
|
||||
for (keychain, descriptor) in keychain_changeset.keychains_added.iter() {
|
||||
let insert_keychain_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO keychain (keychain, descriptor, descriptor_id) VALUES (jsonb(:keychain), :descriptor, :descriptor_id)")
|
||||
.expect("insert keychain statement");
|
||||
let keychain_json = serde_json::to_string(keychain).expect("keychain json");
|
||||
let descriptor_id = descriptor.descriptor_id().to_byte_array();
|
||||
let descriptor = descriptor.to_string();
|
||||
insert_keychain_stmt.execute(named_params! {":keychain": keychain_json, ":descriptor": descriptor, ":descriptor_id": descriptor_id })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update descriptor last revealed index.
|
||||
fn update_last_revealed(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
let keychain_changeset = &tx_graph_changeset.indexer;
|
||||
for (descriptor_id, last_revealed) in keychain_changeset.last_revealed.iter() {
|
||||
let update_last_revealed_stmt = &mut db_transaction
|
||||
.prepare_cached(
|
||||
"UPDATE keychain SET last_revealed = :last_revealed
|
||||
WHERE descriptor_id = :descriptor_id",
|
||||
)
|
||||
.expect("update last revealed statement");
|
||||
let descriptor_id = descriptor_id.to_byte_array();
|
||||
update_last_revealed_stmt.execute(named_params! {":descriptor_id": descriptor_id, ":last_revealed": * last_revealed })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select keychains added.
|
||||
fn select_keychains(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<K, Descriptor<DescriptorPublicKey>>, Error> {
|
||||
let mut select_keychains_added_stmt = db_transaction
|
||||
.prepare_cached("SELECT json(keychain), descriptor FROM keychain")
|
||||
.expect("select keychains statement");
|
||||
|
||||
let keychains = select_keychains_added_stmt
|
||||
.query_map([], |row| {
|
||||
let keychain = row.get_unwrap::<usize, String>(0);
|
||||
let keychain = serde_json::from_str::<K>(keychain.as_str()).expect("keychain");
|
||||
let descriptor = row.get_unwrap::<usize, String>(1);
|
||||
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
|
||||
Ok((keychain, descriptor))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
keychains
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Select descriptor last revealed indexes.
|
||||
fn select_last_revealed(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<DescriptorId, u32>, Error> {
|
||||
let mut select_last_revealed_stmt = db_transaction
|
||||
.prepare_cached(
|
||||
"SELECT descriptor, last_revealed FROM keychain WHERE last_revealed IS NOT NULL",
|
||||
)
|
||||
.expect("select last revealed statement");
|
||||
|
||||
let last_revealed = select_last_revealed_stmt
|
||||
.query_map([], |row| {
|
||||
let descriptor = row.get_unwrap::<usize, String>(0);
|
||||
let descriptor = Descriptor::from_str(descriptor.as_str()).expect("descriptor");
|
||||
let descriptor_id = descriptor.descriptor_id();
|
||||
let last_revealed = row.get_unwrap::<usize, u32>(1);
|
||||
Ok((descriptor_id, last_revealed))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
last_revealed
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tx (transaction) and txout (transaction output) table related functions.
|
||||
impl<K, A> Store<K, A> {
|
||||
/// Insert transactions.
|
||||
///
|
||||
/// Error if trying to insert existing txid.
|
||||
fn insert_txs(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for tx in tx_graph_changeset.graph.txs.iter() {
|
||||
let insert_tx_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO tx (txid, whole_tx) VALUES (:txid, :whole_tx) ON CONFLICT (txid) DO UPDATE SET whole_tx = :whole_tx WHERE txid = :txid")
|
||||
.expect("insert or update tx whole_tx statement");
|
||||
let txid = tx.compute_txid().to_string();
|
||||
let whole_tx = serialize(&tx);
|
||||
insert_tx_stmt
|
||||
.execute(named_params! {":txid": txid, ":whole_tx": whole_tx })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all transactions.
|
||||
fn select_txs(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeSet<Arc<Transaction>>, Error> {
|
||||
let mut select_tx_stmt = db_transaction
|
||||
.prepare_cached("SELECT whole_tx FROM tx WHERE whole_tx IS NOT NULL")
|
||||
.expect("select tx statement");
|
||||
|
||||
let txs = select_tx_stmt
|
||||
.query_map([], |row| {
|
||||
let whole_tx = row.get_unwrap::<usize, Vec<u8>>(0);
|
||||
let whole_tx: Transaction = deserialize(&whole_tx).expect("transaction");
|
||||
Ok(Arc::new(whole_tx))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
|
||||
txs.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Select all transactions with last_seen values.
|
||||
fn select_last_seen(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<Txid, u64>, Error> {
|
||||
// load tx last_seen
|
||||
let mut select_last_seen_stmt = db_transaction
|
||||
.prepare_cached("SELECT txid, last_seen FROM tx WHERE last_seen IS NOT NULL")
|
||||
.expect("select tx last seen statement");
|
||||
|
||||
let last_seen = select_last_seen_stmt
|
||||
.query_map([], |row| {
|
||||
let txid = row.get_unwrap::<usize, String>(0);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
let last_seen = row.get_unwrap::<usize, u64>(1);
|
||||
Ok((txid, last_seen))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
last_seen
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Insert txouts.
|
||||
///
|
||||
/// Error if trying to insert existing outpoint.
|
||||
fn insert_txouts(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for txout in tx_graph_changeset.graph.txouts.iter() {
|
||||
let insert_txout_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO txout (txid, vout, value, script) VALUES (:txid, :vout, :value, :script)")
|
||||
.expect("insert txout statement");
|
||||
let txid = txout.0.txid.to_string();
|
||||
let vout = txout.0.vout;
|
||||
let value = txout.1.value.to_sat();
|
||||
let script = txout.1.script_pubkey.as_bytes();
|
||||
insert_txout_stmt.execute(named_params! {":txid": txid, ":vout": vout, ":value": value, ":script": script })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all transaction outputs.
|
||||
fn select_txouts(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeMap<OutPoint, TxOut>, Error> {
|
||||
// load tx outs
|
||||
let mut select_txout_stmt = db_transaction
|
||||
.prepare_cached("SELECT txid, vout, value, script FROM txout")
|
||||
.expect("select txout statement");
|
||||
|
||||
let txouts = select_txout_stmt
|
||||
.query_map([], |row| {
|
||||
let txid = row.get_unwrap::<usize, String>(0);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
let vout = row.get_unwrap::<usize, u32>(1);
|
||||
let outpoint = OutPoint::new(txid, vout);
|
||||
let value = row.get_unwrap::<usize, u64>(2);
|
||||
let script_pubkey = row.get_unwrap::<usize, Vec<u8>>(3);
|
||||
let script_pubkey = ScriptBuf::from_bytes(script_pubkey);
|
||||
let txout = TxOut {
|
||||
value: Amount::from_sat(value),
|
||||
script_pubkey,
|
||||
};
|
||||
Ok((outpoint, txout))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
txouts
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Update transaction last seen times.
|
||||
fn update_last_seen(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
for tx_last_seen in tx_graph_changeset.graph.last_seen.iter() {
|
||||
let insert_or_update_tx_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO tx (txid, last_seen) VALUES (:txid, :last_seen) ON CONFLICT (txid) DO UPDATE SET last_seen = :last_seen WHERE txid = :txid")
|
||||
.expect("insert or update tx last_seen statement");
|
||||
let txid = tx_last_seen.0.to_string();
|
||||
let last_seen = *tx_last_seen.1;
|
||||
insert_or_update_tx_stmt
|
||||
.execute(named_params! {":txid": txid, ":last_seen": last_seen })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Anchor table related functions.
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
/// Insert anchors.
|
||||
fn insert_anchors(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
tx_graph_changeset: &indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>>,
|
||||
) -> Result<(), Error> {
|
||||
// serde_json::to_string
|
||||
for anchor in tx_graph_changeset.graph.anchors.iter() {
|
||||
let insert_anchor_stmt = &mut db_transaction
|
||||
.prepare_cached("INSERT INTO anchor_tx (block_hash, anchor, txid) VALUES (:block_hash, jsonb(:anchor), :txid)")
|
||||
.expect("insert anchor statement");
|
||||
let block_hash = anchor.0.anchor_block().hash.to_string();
|
||||
let anchor_json = serde_json::to_string(&anchor.0).expect("anchor json");
|
||||
let txid = anchor.1.to_string();
|
||||
insert_anchor_stmt.execute(named_params! {":block_hash": block_hash, ":anchor": anchor_json, ":txid": txid })
|
||||
.map_err(Error::Sqlite)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Select all anchors.
|
||||
fn select_anchors(
|
||||
db_transaction: &rusqlite::Transaction,
|
||||
) -> Result<BTreeSet<(A, Txid)>, Error> {
|
||||
// serde_json::from_str
|
||||
let mut select_anchor_stmt = db_transaction
|
||||
.prepare_cached("SELECT block_hash, json(anchor), txid FROM anchor_tx")
|
||||
.expect("select anchor statement");
|
||||
let anchors = select_anchor_stmt
|
||||
.query_map([], |row| {
|
||||
let hash = row.get_unwrap::<usize, String>(0);
|
||||
let hash = BlockHash::from_str(hash.as_str()).expect("block hash");
|
||||
let anchor = row.get_unwrap::<usize, String>(1);
|
||||
let anchor: A = serde_json::from_str(anchor.as_str()).expect("anchor");
|
||||
// double check anchor blob block hash matches
|
||||
assert_eq!(hash, anchor.anchor_block().hash);
|
||||
let txid = row.get_unwrap::<usize, String>(2);
|
||||
let txid = Txid::from_str(&txid).expect("txid");
|
||||
Ok((anchor, txid))
|
||||
})
|
||||
.map_err(Error::Sqlite)?;
|
||||
anchors
|
||||
.into_iter()
|
||||
.map(|row| row.map_err(Error::Sqlite))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Functions to read and write all [`CombinedChangeSet`] data.
|
||||
impl<K, A> Store<K, A>
|
||||
where
|
||||
K: Ord + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
A: Anchor + for<'de> Deserialize<'de> + Serialize + Send,
|
||||
{
|
||||
/// Write the given `changeset` atomically.
|
||||
pub fn write(&mut self, changeset: &CombinedChangeSet<K, A>) -> Result<(), Error> {
|
||||
// no need to write anything if changeset is empty
|
||||
if changeset.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let db_transaction = self.db_transaction()?;
|
||||
|
||||
let network_changeset = &changeset.network;
|
||||
let current_network = Self::select_network(&db_transaction)?;
|
||||
Self::insert_network(¤t_network, &db_transaction, network_changeset)?;
|
||||
|
||||
let chain_changeset = &changeset.chain;
|
||||
Self::insert_or_delete_blocks(&db_transaction, chain_changeset)?;
|
||||
|
||||
let tx_graph_changeset = &changeset.indexed_tx_graph;
|
||||
Self::insert_keychains(&db_transaction, tx_graph_changeset)?;
|
||||
Self::update_last_revealed(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_txs(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_txouts(&db_transaction, tx_graph_changeset)?;
|
||||
Self::insert_anchors(&db_transaction, tx_graph_changeset)?;
|
||||
Self::update_last_seen(&db_transaction, tx_graph_changeset)?;
|
||||
db_transaction.commit().map_err(Error::Sqlite)
|
||||
}
|
||||
|
||||
/// Read the entire database and return the aggregate [`CombinedChangeSet`].
|
||||
pub fn read(&mut self) -> Result<Option<CombinedChangeSet<K, A>>, Error> {
|
||||
let db_transaction = self.db_transaction()?;
|
||||
|
||||
let network = Self::select_network(&db_transaction)?;
|
||||
let chain = Self::select_blocks(&db_transaction)?;
|
||||
let keychains_added = Self::select_keychains(&db_transaction)?;
|
||||
let last_revealed = Self::select_last_revealed(&db_transaction)?;
|
||||
let txs = Self::select_txs(&db_transaction)?;
|
||||
let last_seen = Self::select_last_seen(&db_transaction)?;
|
||||
let txouts = Self::select_txouts(&db_transaction)?;
|
||||
let anchors = Self::select_anchors(&db_transaction)?;
|
||||
|
||||
let graph: tx_graph::ChangeSet<A> = tx_graph::ChangeSet {
|
||||
txs,
|
||||
txouts,
|
||||
anchors,
|
||||
last_seen,
|
||||
};
|
||||
|
||||
let indexer: keychain::ChangeSet<K> = keychain::ChangeSet {
|
||||
keychains_added,
|
||||
last_revealed,
|
||||
};
|
||||
|
||||
let indexed_tx_graph: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<K>> =
|
||||
indexed_tx_graph::ChangeSet { graph, indexer };
|
||||
|
||||
if network.is_none() && chain.is_empty() && indexed_tx_graph.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(CombinedChangeSet {
|
||||
chain,
|
||||
indexed_tx_graph,
|
||||
network,
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::store::Append;
|
||||
use bdk_chain::bitcoin::consensus::encode::deserialize;
|
||||
use bdk_chain::bitcoin::constants::genesis_block;
|
||||
use bdk_chain::bitcoin::hashes::hex::FromHex;
|
||||
use bdk_chain::bitcoin::transaction::Transaction;
|
||||
use bdk_chain::bitcoin::Network::Testnet;
|
||||
use bdk_chain::bitcoin::{secp256k1, BlockHash, OutPoint};
|
||||
use bdk_chain::miniscript::Descriptor;
|
||||
use bdk_chain::CombinedChangeSet;
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph, keychain, tx_graph, BlockId, ConfirmationHeightAnchor,
|
||||
ConfirmationTimeHeightAnchor, DescriptorExt,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Hash, Debug, Serialize, Deserialize)]
|
||||
enum Keychain {
|
||||
External { account: u32, name: String },
|
||||
Internal { account: u32, name: String },
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_confirmation_time_height_anchor() {
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, time, hash| ConfirmationTimeHeightAnchor {
|
||||
confirmation_height: height,
|
||||
confirmation_time: time,
|
||||
anchor_block: (height, hash).into(),
|
||||
});
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, ConfirmationTimeHeightAnchor>::new(conn)
|
||||
.expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.read().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_confirmation_height_anchor() {
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, _time, hash| ConfirmationHeightAnchor {
|
||||
confirmation_height: height,
|
||||
anchor_block: (height, hash).into(),
|
||||
});
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, ConfirmationHeightAnchor>::new(conn)
|
||||
.expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.read().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_and_load_aggregate_changesets_with_blockid_anchor() {
|
||||
let (test_changesets, agg_test_changesets) =
|
||||
create_test_changesets(&|height, _time, hash| BlockId { height, hash });
|
||||
|
||||
let conn = Connection::open_in_memory().expect("in memory connection");
|
||||
let mut store = Store::<Keychain, BlockId>::new(conn).expect("create new memory db store");
|
||||
|
||||
test_changesets.iter().for_each(|changeset| {
|
||||
store.write(changeset).expect("write changeset");
|
||||
});
|
||||
|
||||
let agg_changeset = store.read().expect("aggregated changeset");
|
||||
|
||||
assert_eq!(agg_changeset, Some(agg_test_changesets));
|
||||
}
|
||||
|
||||
fn create_test_changesets<A: Anchor + Copy>(
|
||||
anchor_fn: &dyn Fn(u32, u64, BlockHash) -> A,
|
||||
) -> (
|
||||
Vec<CombinedChangeSet<Keychain, A>>,
|
||||
CombinedChangeSet<Keychain, A>,
|
||||
) {
|
||||
let secp = &secp256k1::Secp256k1::signing_only();
|
||||
|
||||
let network_changeset = Some(Testnet);
|
||||
|
||||
let block_hash_0: BlockHash = genesis_block(Testnet).block_hash();
|
||||
let block_hash_1 =
|
||||
BlockHash::from_str("00000000b873e79784647a6c82962c70d228557d24a747ea4d1b8bbe878e1206")
|
||||
.unwrap();
|
||||
let block_hash_2 =
|
||||
BlockHash::from_str("000000006c02c8ea6e4ff69651f7fcde348fb9d557a06e6957b65552002a7820")
|
||||
.unwrap();
|
||||
|
||||
let block_changeset = [
|
||||
(0, Some(block_hash_0)),
|
||||
(1, Some(block_hash_1)),
|
||||
(2, Some(block_hash_2)),
|
||||
]
|
||||
.into();
|
||||
|
||||
let ext_keychain = Keychain::External {
|
||||
account: 0,
|
||||
name: "ext test".to_string(),
|
||||
};
|
||||
let (ext_desc, _ext_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/0/*)").unwrap();
|
||||
let ext_desc_id = ext_desc.descriptor_id();
|
||||
let int_keychain = Keychain::Internal {
|
||||
account: 0,
|
||||
name: "int test".to_string(),
|
||||
};
|
||||
let (int_desc, _int_keymap) = Descriptor::parse_descriptor(secp, "wpkh(tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy/1/*)").unwrap();
|
||||
let int_desc_id = int_desc.descriptor_id();
|
||||
|
||||
let tx0_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff4d04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73ffffffff0100f2052a01000000434104678afdb0fe5548271967f1a67130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c384df7ba0b8d578a4c702b6bf11d5fac00000000").unwrap();
|
||||
let tx0: Arc<Transaction> = Arc::new(deserialize(tx0_hex.as_slice()).unwrap());
|
||||
let tx1_hex = Vec::<u8>::from_hex("010000000001010000000000000000000000000000000000000000000000000000000000000000ffffffff025151feffffff0200f2052a010000001600149243f727dd5343293eb83174324019ec16c2630f0000000000000000776a24aa21a9ede2f61c3f71d1defd3fa999dfa36953755c690689799962b48bebd836974e8cf94c4fecc7daa2490047304402205e423a8754336ca99dbe16509b877ef1bf98d008836c725005b3c787c41ebe46022047246e4467ad7cc7f1ad98662afcaf14c115e0095a227c7b05c5182591c23e7e01000120000000000000000000000000000000000000000000000000000000000000000000000000").unwrap();
|
||||
let tx1: Arc<Transaction> = Arc::new(deserialize(tx1_hex.as_slice()).unwrap());
|
||||
let tx2_hex = Vec::<u8>::from_hex("01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff0e0432e7494d010e062f503253482fffffffff0100f2052a010000002321038a7f6ef1c8ca0c588aa53fa860128077c9e6c11e6830f4d7ee4e763a56b7718fac00000000").unwrap();
|
||||
let tx2: Arc<Transaction> = Arc::new(deserialize(tx2_hex.as_slice()).unwrap());
|
||||
|
||||
let outpoint0_0 = OutPoint::new(tx0.compute_txid(), 0);
|
||||
let txout0_0 = tx0.output.first().unwrap().clone();
|
||||
let outpoint1_0 = OutPoint::new(tx1.compute_txid(), 0);
|
||||
let txout1_0 = tx1.output.first().unwrap().clone();
|
||||
|
||||
let anchor1 = anchor_fn(1, 1296667328, block_hash_1);
|
||||
let anchor2 = anchor_fn(2, 1296688946, block_hash_2);
|
||||
|
||||
let tx_graph_changeset = tx_graph::ChangeSet::<A> {
|
||||
txs: [tx0.clone(), tx1.clone()].into(),
|
||||
txouts: [(outpoint0_0, txout0_0), (outpoint1_0, txout1_0)].into(),
|
||||
anchors: [(anchor1, tx0.compute_txid()), (anchor1, tx1.compute_txid())].into(),
|
||||
last_seen: [
|
||||
(tx0.compute_txid(), 1598918400),
|
||||
(tx1.compute_txid(), 1598919121),
|
||||
(tx2.compute_txid(), 1608919121),
|
||||
]
|
||||
.into(),
|
||||
};
|
||||
|
||||
let keychain_changeset = keychain::ChangeSet {
|
||||
keychains_added: [(ext_keychain, ext_desc), (int_keychain, int_desc)].into(),
|
||||
last_revealed: [(ext_desc_id, 124), (int_desc_id, 421)].into(),
|
||||
};
|
||||
|
||||
let graph_changeset: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset,
|
||||
indexer: keychain_changeset,
|
||||
};
|
||||
|
||||
// test changesets to write to db
|
||||
let mut changesets = Vec::new();
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: block_changeset,
|
||||
indexed_tx_graph: graph_changeset,
|
||||
network: network_changeset,
|
||||
});
|
||||
|
||||
// create changeset that sets the whole tx2 and updates it's lastseen where before there was only the txid and last_seen
|
||||
let tx_graph_changeset2 = tx_graph::ChangeSet::<A> {
|
||||
txs: [tx2.clone()].into(),
|
||||
txouts: BTreeMap::default(),
|
||||
anchors: BTreeSet::default(),
|
||||
last_seen: [(tx2.compute_txid(), 1708919121)].into(),
|
||||
};
|
||||
|
||||
let graph_changeset2: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset2,
|
||||
indexer: keychain::ChangeSet::default(),
|
||||
};
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph: graph_changeset2,
|
||||
network: None,
|
||||
});
|
||||
|
||||
// create changeset that adds a new anchor2 for tx0 and tx1
|
||||
let tx_graph_changeset3 = tx_graph::ChangeSet::<A> {
|
||||
txs: BTreeSet::default(),
|
||||
txouts: BTreeMap::default(),
|
||||
anchors: [(anchor2, tx0.compute_txid()), (anchor2, tx1.compute_txid())].into(),
|
||||
last_seen: BTreeMap::default(),
|
||||
};
|
||||
|
||||
let graph_changeset3: indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>> =
|
||||
indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph_changeset3,
|
||||
indexer: keychain::ChangeSet::default(),
|
||||
};
|
||||
|
||||
changesets.push(CombinedChangeSet {
|
||||
chain: local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph: graph_changeset3,
|
||||
network: None,
|
||||
});
|
||||
|
||||
// aggregated test changesets
|
||||
let agg_test_changesets =
|
||||
changesets
|
||||
.iter()
|
||||
.fold(CombinedChangeSet::<Keychain, A>::default(), |mut i, cs| {
|
||||
i.append(cs.clone());
|
||||
i
|
||||
});
|
||||
|
||||
(changesets, agg_test_changesets)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bdk_testenv"
|
||||
version = "0.6.0"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
@@ -13,7 +13,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.16", default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.17", default-features = false }
|
||||
electrsd = { version = "0.28.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "bdk_wallet"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "1.0.0-alpha.13"
|
||||
version = "1.0.0-beta.1"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk"
|
||||
description = "A modern, lightweight, descriptor-based wallet library"
|
||||
@@ -13,39 +13,35 @@ edition = "2021"
|
||||
rust-version = "1.63"
|
||||
|
||||
[dependencies]
|
||||
rand = "^0.8"
|
||||
rand_core = { version = "0.6.0" }
|
||||
miniscript = { version = "12.0.0", features = ["serde"], default-features = false }
|
||||
bitcoin = { version = "0.32.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||
bitcoin = { version = "0.32.0", features = ["serde", "base64"], default-features = false }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.16.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_chain = { path = "../chain", version = "0.17.0", features = ["miniscript", "serde"], default-features = false }
|
||||
bdk_file_store = { path = "../file_store", version = "0.14.0", optional = true }
|
||||
|
||||
# Optional dependencies
|
||||
bip39 = { version = "2.0", optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = "0.2"
|
||||
js-sys = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
|
||||
std = ["bitcoin/std", "bitcoin/rand-std", "miniscript/std", "bdk_chain/std"]
|
||||
compiler = ["miniscript/compiler"]
|
||||
all-keys = ["keys-bip39"]
|
||||
keys-bip39 = ["bip39"]
|
||||
|
||||
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
|
||||
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
|
||||
# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
|
||||
dev-getrandom-wasm = ["getrandom/js"]
|
||||
rusqlite = ["bdk_chain/rusqlite"]
|
||||
file_store = ["bdk_file_store"]
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1.4"
|
||||
assert_matches = "1.5.0"
|
||||
tempfile = "3"
|
||||
bdk_sqlite = { path = "../sqlite" }
|
||||
bdk_chain = { path = "../chain", features = ["rusqlite"] }
|
||||
bdk_wallet = { path = ".", features = ["rusqlite", "file_store"] }
|
||||
bdk_file_store = { path = "../file_store" }
|
||||
anyhow = "1"
|
||||
rand = "^0.8"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
|
||||
@@ -57,43 +57,45 @@ that the `Wallet` can use to update its view of the chain.
|
||||
|
||||
## Persistence
|
||||
|
||||
To persist `Wallet` state data use a data store crate that reads and writes [`bdk_chain::CombinedChangeSet`].
|
||||
To persist `Wallet` state data use a data store crate that reads and writes [`ChangeSet`].
|
||||
|
||||
**Implementations**
|
||||
|
||||
* [`bdk_file_store`]: Stores wallet changes in a simple flat file.
|
||||
* [`bdk_sqlite`]: Stores wallet changes in a SQLite relational database file.
|
||||
|
||||
**Example**
|
||||
|
||||
<!-- compile_fail because outpoint and txout are fake variables -->
|
||||
```rust,no_run
|
||||
use bdk_wallet::{bitcoin::Network, KeychainKind, wallet::{ChangeSet, Wallet}};
|
||||
use bdk_wallet::{bitcoin::Network, KeychainKind, ChangeSet, Wallet};
|
||||
|
||||
fn main() {
|
||||
// Open or create a new file store for wallet data.
|
||||
let mut db =
|
||||
bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db")
|
||||
.expect("create store");
|
||||
// Open or create a new file store for wallet data.
|
||||
let mut db =
|
||||
bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "/tmp/my_wallet.db")
|
||||
.expect("create store");
|
||||
|
||||
// Create a wallet with initial wallet data read from the file store.
|
||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
|
||||
let changeset = db.aggregate_changesets().expect("changeset loaded");
|
||||
let mut wallet =
|
||||
Wallet::new_or_load(descriptor, change_descriptor, changeset, Network::Testnet)
|
||||
.expect("create or load wallet");
|
||||
// Create a wallet with initial wallet data read from the file store.
|
||||
let network = Network::Testnet;
|
||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/1/*)";
|
||||
let wallet_opt = Wallet::load()
|
||||
.descriptors(descriptor, change_descriptor)
|
||||
.network(network)
|
||||
.load_wallet(&mut db)
|
||||
.expect("wallet");
|
||||
let mut wallet = match wallet_opt {
|
||||
Some(wallet) => wallet,
|
||||
None => Wallet::create(descriptor, change_descriptor)
|
||||
.network(network)
|
||||
.create_wallet(&mut db)
|
||||
.expect("wallet"),
|
||||
};
|
||||
|
||||
// Get a new address to receive bitcoin.
|
||||
let receive_address = wallet.reveal_next_address(KeychainKind::External);
|
||||
// Persist staged wallet data changes to the file store.
|
||||
let staged_changeset = wallet.take_staged();
|
||||
if let Some(changeset) = staged_changeset {
|
||||
db.append_changeset(&changeset)
|
||||
.expect("must commit changes to database");
|
||||
}
|
||||
println!("Your new receive address is: {}", receive_address.address);
|
||||
}
|
||||
// Get a new address to receive bitcoin.
|
||||
let receive_address = wallet.reveal_next_address(KeychainKind::External);
|
||||
// Persist staged wallet data changes to the file store.
|
||||
wallet.persist(&mut db).expect("persist");
|
||||
println!("Your new receive address is: {}", receive_address.address);
|
||||
```
|
||||
|
||||
<!-- ### Sync the balance of a descriptor -->
|
||||
@@ -124,7 +126,7 @@ fn main() {
|
||||
|
||||
<!-- ```rust -->
|
||||
<!-- use bdk_wallet::Wallet; -->
|
||||
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk_wallet::AddressIndex::New; -->
|
||||
<!-- use bdk_wallet::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk_wallet::Error> { -->
|
||||
@@ -149,7 +151,7 @@ fn main() {
|
||||
<!-- use bdk_wallet::blockchain::ElectrumBlockchain; -->
|
||||
|
||||
<!-- use bdk_wallet::electrum_client::Client; -->
|
||||
<!-- use bdk_wallet::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk_wallet::AddressIndex::New; -->
|
||||
|
||||
<!-- use bitcoin::base64; -->
|
||||
<!-- use bdk_wallet::bitcoin::consensus::serialize; -->
|
||||
@@ -235,7 +237,6 @@ conditions.
|
||||
[`Wallet`]: https://docs.rs/bdk_wallet/latest/bdk_wallet/wallet/struct.Wallet.html
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
||||
[`bdk_sqlite`]: https://docs.rs/bdk_sqlite/latest
|
||||
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
||||
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
|
||||
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
|
||||
|
||||
@@ -77,7 +77,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
);
|
||||
|
||||
// Create a new wallet from descriptors
|
||||
let mut wallet = Wallet::new(&descriptor, &internal_descriptor, Network::Regtest)?;
|
||||
let mut wallet = Wallet::create(descriptor, internal_descriptor)
|
||||
.network(Network::Regtest)
|
||||
.create_wallet_no_persist()?;
|
||||
|
||||
println!(
|
||||
"First derived address from the descriptor: \n{}",
|
||||
|
||||
@@ -14,7 +14,7 @@ use std::error::Error;
|
||||
|
||||
use bdk_wallet::bitcoin::Network;
|
||||
use bdk_wallet::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
|
||||
use bdk_wallet::wallet::signer::SignersContainer;
|
||||
use bdk_wallet::signer::SignersContainer;
|
||||
|
||||
/// This example describes the use of the BDK's [`bdk_wallet::descriptor::policy`] module.
|
||||
///
|
||||
@@ -38,7 +38,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
// While the `keymap` can be used to create a `SignerContainer`.
|
||||
//
|
||||
// The `SignerContainer` can sign for `PSBT`s.
|
||||
// a bdk_wallet::wallet internally uses these to handle transaction signing.
|
||||
// a `bdk_wallet::Wallet` internally uses these to handle transaction signing.
|
||||
// But they can be used as independent tools also.
|
||||
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
use core::fmt;
|
||||
|
||||
/// Errors related to the parsing and usage of descriptors
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
/// Invalid HD Key path, such as having a wildcard but a length != 1
|
||||
InvalidHdKeyPath,
|
||||
|
||||
@@ -112,6 +112,16 @@ impl IntoWalletDescriptor for &String {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoWalletDescriptor for String {
|
||||
fn into_wallet_descriptor(
|
||||
self,
|
||||
secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
self.as_str().into_wallet_descriptor(secp, network)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoWalletDescriptor for ExtendedDescriptor {
|
||||
fn into_wallet_descriptor(
|
||||
self,
|
||||
@@ -281,15 +291,10 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper for `IntoWalletDescriptor` that performs additional checks on the keys contained in the
|
||||
/// descriptor
|
||||
pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||
inner: T,
|
||||
secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
let (descriptor, keymap) = inner.into_wallet_descriptor(secp, network)?;
|
||||
|
||||
/// Extra checks for [`ExtendedDescriptor`].
|
||||
pub(crate) fn check_wallet_descriptor(
|
||||
descriptor: &Descriptor<DescriptorPublicKey>,
|
||||
) -> Result<(), DescriptorError> {
|
||||
// Ensure the keys don't contain any hardened derivation steps or hardened wildcards
|
||||
let descriptor_contains_hardened_steps = descriptor.for_any_key(|k| {
|
||||
if let DescriptorPublicKey::XPub(DescriptorXKey {
|
||||
@@ -316,7 +321,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||
// issues
|
||||
descriptor.sanity_check()?;
|
||||
|
||||
Ok((descriptor, keymap))
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@@ -855,22 +860,31 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_wallet_descriptor_checked() {
|
||||
fn test_check_wallet_descriptor() {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
let (descriptor, _) = descriptor
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.expect("must parse");
|
||||
let result = check_wallet_descriptor(&descriptor);
|
||||
|
||||
assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
|
||||
|
||||
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
let (descriptor, _) = descriptor
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.expect("must parse");
|
||||
let result = check_wallet_descriptor(&descriptor);
|
||||
|
||||
assert_matches!(result, Err(DescriptorError::MultiPath));
|
||||
|
||||
// repeated pubkeys
|
||||
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
let (descriptor, _) = descriptor
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.expect("must parse");
|
||||
let result = check_wallet_descriptor(&descriptor);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -882,8 +896,10 @@ mod test {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let descriptor = "sh(wsh(sortedmulti(3,tpubDEsqS36T4DVsKJd9UH8pAKzrkGBYPLEt9jZMwpKtzh1G6mgYehfHt9WCgk7MJG5QGSFWf176KaBNoXbcuFcuadAFKxDpUdMDKGBha7bY3QM/0/*,tpubDF3cpwfs7fMvXXuoQbohXtLjNM6ehwYT287LWtmLsd4r77YLg6MZg4vTETx5MSJ2zkfigbYWu31VA2Z2Vc1cZugCYXgS7FQu6pE8V6TriEH/0/*,tpubDE1SKfcW76Tb2AASv5bQWMuScYNAdoqLHoexw13sNDXwmUhQDBbCD3QAedKGLhxMrWQdMDKENzYtnXPDRvexQPNuDrLj52wAjHhNEm8sJ4p/0/*,tpubDFLc6oXwJmhm3FGGzXkfJNTh2KitoY3WhmmQvuAjMhD8YbyWn5mAqckbxXfm2etM3p5J6JoTpSrMqRSTfMLtNW46poDaEZJ1kjd3csRSjwH/0/*,tpubDEWD9NBeWP59xXmdqSNt4VYdtTGwbpyP8WS962BuqpQeMZmX9Pur14dhXdZT5a7wR1pK6dPtZ9fP5WR493hPzemnBvkfLLYxnUjAKj1JCQV/0/*,tpubDEHyZkkwd7gZWCTgQuYQ9C4myF2hMEmyHsBCCmLssGqoqUxeT3gzohF5uEVURkf9TtmeepJgkSUmteac38FwZqirjApzNX59XSHLcwaTZCH/0/*,tpubDEqLouCekwnMUWN486kxGzD44qVgeyuqHyxUypNEiQt5RnUZNJe386TKPK99fqRV1vRkZjYAjtXGTECz98MCsdLcnkM67U6KdYRzVubeCgZ/0/*)))";
|
||||
let (descriptor, _) =
|
||||
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
|
||||
let (descriptor, _) = descriptor
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
check_wallet_descriptor(&descriptor).expect("descriptor");
|
||||
|
||||
let descriptor = descriptor.at_derivation_index(0).unwrap();
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
//! ```
|
||||
//! # use std::sync::Arc;
|
||||
//! # use bdk_wallet::descriptor::*;
|
||||
//! # use bdk_wallet::wallet::signer::*;
|
||||
//! # use bdk_wallet::signer::*;
|
||||
//! # use bdk_wallet::bitcoin::secp256k1::Secp256k1;
|
||||
//! use bdk_wallet::descriptor::policy::BuildSatisfaction;
|
||||
//! let secp = Secp256k1::new();
|
||||
|
||||
@@ -81,7 +81,9 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let key_internal =
|
||||
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
|
||||
/// let mut wallet = Wallet::new(P2Pkh(key_external), P2Pkh(key_internal), Network::Testnet)?;
|
||||
/// let mut wallet = Wallet::create(P2Pkh(key_external), P2Pkh(key_internal))
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet
|
||||
@@ -91,6 +93,7 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct P2Pkh<K: IntoDescriptorKey<Legacy>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
@@ -113,11 +116,9 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let key_internal =
|
||||
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// P2Wpkh_P2Sh(key_external),
|
||||
/// P2Wpkh_P2Sh(key_internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// let mut wallet = Wallet::create(P2Wpkh_P2Sh(key_external), P2Wpkh_P2Sh(key_internal))
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet
|
||||
@@ -128,6 +129,7 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[allow(non_camel_case_types)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct P2Wpkh_P2Sh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
@@ -142,7 +144,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet};
|
||||
/// # use bdk_wallet::Wallet;
|
||||
/// # use bdk_wallet::KeychainKind;
|
||||
/// use bdk_wallet::template::P2Wpkh;
|
||||
///
|
||||
@@ -150,7 +152,9 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let key_internal =
|
||||
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
|
||||
/// let mut wallet = Wallet::new(P2Wpkh(key_external), P2Wpkh(key_internal), Network::Testnet)?;
|
||||
/// let mut wallet = Wallet::create(P2Wpkh(key_external), P2Wpkh(key_internal))
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet
|
||||
@@ -160,6 +164,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct P2Wpkh<K: IntoDescriptorKey<Segwitv0>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
@@ -182,7 +187,9 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let key_internal =
|
||||
/// bitcoin::PrivateKey::from_wif("cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW")?;
|
||||
/// let mut wallet = Wallet::new(P2TR(key_external), P2TR(key_internal), Network::Testnet)?;
|
||||
/// let mut wallet = Wallet::create(P2TR(key_external), P2TR(key_internal))
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet
|
||||
@@ -192,6 +199,7 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct P2TR<K: IntoDescriptorKey<Tap>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
|
||||
@@ -208,23 +216,22 @@ impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip44;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// Bip44(key.clone(), KeychainKind::External),
|
||||
/// Bip44(key, KeychainKind::Internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// let mut wallet = Wallet::create(Bip44(key.clone(), KeychainKind::External), Bip44(key, KeychainKind::Internal))
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
@@ -247,21 +254,23 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::{KeychainKind, Wallet};
|
||||
/// use bdk_wallet::template::Bip44Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::create(
|
||||
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Bip44Public(key, fingerprint, KeychainKind::Internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// )
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
@@ -284,20 +293,22 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip49;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::create(
|
||||
/// Bip49(key.clone(), KeychainKind::External),
|
||||
/// Bip49(key, KeychainKind::Internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// )
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
@@ -320,21 +331,23 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip49Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::create(
|
||||
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Bip49Public(key, fingerprint, KeychainKind::Internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// )
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
@@ -357,20 +370,22 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip84;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::create(
|
||||
/// Bip84(key.clone(), KeychainKind::External),
|
||||
/// Bip84(key, KeychainKind::Internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// )
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
@@ -393,21 +408,23 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip84Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::create(
|
||||
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Bip84Public(key, fingerprint, KeychainKind::Internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// )
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
@@ -430,20 +447,22 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip86;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpriv::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::create(
|
||||
/// Bip86(key.clone(), KeychainKind::External),
|
||||
/// Bip86(key, KeychainKind::Internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// )
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bip86<K: DerivableKey<Tap>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
|
||||
@@ -466,21 +485,23 @@ impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk_wallet::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// # use bdk_wallet::{Wallet, KeychainKind};
|
||||
/// use bdk_wallet::template::Bip86Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::Xpub::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new(
|
||||
/// let mut wallet = Wallet::create(
|
||||
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Bip86Public(key, fingerprint, KeychainKind::Internal),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
/// )
|
||||
/// .network(Network::Testnet)
|
||||
/// .create_wallet_no_persist()?;
|
||||
///
|
||||
/// assert_eq!(wallet.next_unused_address(KeychainKind::External).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bip86Public<K: DerivableKey<Tap>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86Public<K> {
|
||||
|
||||
@@ -20,6 +20,8 @@ use core::marker::PhantomData;
|
||||
use core::ops::Deref;
|
||||
use core::str::FromStr;
|
||||
|
||||
use rand_core::{CryptoRng, RngCore};
|
||||
|
||||
use bitcoin::secp256k1::{self, Secp256k1, Signing};
|
||||
|
||||
use bitcoin::bip32;
|
||||
@@ -631,12 +633,23 @@ pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
|
||||
entropy: Self::Entropy,
|
||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error>;
|
||||
|
||||
/// Generate a key given the options with a random entropy
|
||||
/// Generate a key given the options with random entropy.
|
||||
///
|
||||
/// Uses the thread-local random number generator.
|
||||
#[cfg(feature = "std")]
|
||||
fn generate(options: Self::Options) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
use rand::{thread_rng, Rng};
|
||||
Self::generate_with_aux_rand(options, &mut bitcoin::key::rand::thread_rng())
|
||||
}
|
||||
|
||||
/// Generate a key given the options with random entropy.
|
||||
///
|
||||
/// Uses a provided random number generator (rng).
|
||||
fn generate_with_aux_rand(
|
||||
options: Self::Options,
|
||||
rng: &mut (impl CryptoRng + RngCore),
|
||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
let mut entropy = Self::Entropy::default();
|
||||
thread_rng().fill(entropy.as_mut());
|
||||
rng.fill_bytes(entropy.as_mut());
|
||||
Self::generate_with_entropy(options, entropy)
|
||||
}
|
||||
}
|
||||
@@ -657,8 +670,20 @@ where
|
||||
}
|
||||
|
||||
/// Generate a key with the default options and a random entropy
|
||||
///
|
||||
/// Uses the thread-local random number generator.
|
||||
#[cfg(feature = "std")]
|
||||
fn generate_default() -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
Self::generate(Default::default())
|
||||
Self::generate_with_aux_rand(Default::default(), &mut bitcoin::key::rand::thread_rng())
|
||||
}
|
||||
|
||||
/// Generate a key with the default options and a random entropy
|
||||
///
|
||||
/// Uses a provided random number generator (rng).
|
||||
fn generate_default_with_aux_rand(
|
||||
rng: &mut (impl CryptoRng + RngCore),
|
||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
Self::generate_with_aux_rand(Default::default(), rng)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -910,7 +935,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
|
||||
}
|
||||
|
||||
/// Errors thrown while working with [`keys`](crate::keys)
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum KeyError {
|
||||
/// The key cannot exist in the given script context
|
||||
InvalidScriptContext,
|
||||
|
||||
@@ -15,33 +15,36 @@ extern crate std;
|
||||
#[doc(hidden)]
|
||||
#[macro_use]
|
||||
pub extern crate alloc;
|
||||
|
||||
pub extern crate bdk_chain as chain;
|
||||
#[cfg(feature = "file_store")]
|
||||
pub extern crate bdk_file_store as file_store;
|
||||
#[cfg(feature = "keys-bip39")]
|
||||
pub extern crate bip39;
|
||||
pub extern crate bitcoin;
|
||||
pub extern crate miniscript;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(feature = "keys-bip39")]
|
||||
extern crate bip39;
|
||||
pub extern crate serde;
|
||||
pub extern crate serde_json;
|
||||
|
||||
pub mod descriptor;
|
||||
pub mod keys;
|
||||
pub mod psbt;
|
||||
pub(crate) mod types;
|
||||
pub mod wallet;
|
||||
mod types;
|
||||
mod wallet;
|
||||
|
||||
pub(crate) use bdk_chain::collections;
|
||||
#[cfg(feature = "rusqlite")]
|
||||
pub use bdk_chain::rusqlite;
|
||||
#[cfg(feature = "rusqlite")]
|
||||
pub use bdk_chain::rusqlite_impl;
|
||||
pub use descriptor::template;
|
||||
pub use descriptor::HdKeyPaths;
|
||||
pub use signer;
|
||||
pub use signer::SignOptions;
|
||||
pub use tx_builder::*;
|
||||
pub use types::*;
|
||||
pub use wallet::signer;
|
||||
pub use wallet::signer::SignOptions;
|
||||
pub use wallet::tx_builder::TxBuilder;
|
||||
pub use wallet::Wallet;
|
||||
pub use wallet::*;
|
||||
|
||||
/// Get the version of BDK at runtime
|
||||
/// Get the version of [`bdk_wallet`](crate) at runtime.
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION", "unknown")
|
||||
}
|
||||
|
||||
pub use bdk_chain as chain;
|
||||
pub(crate) use bdk_chain::collections;
|
||||
|
||||
@@ -13,8 +13,8 @@ use alloc::boxed::Box;
|
||||
use core::convert::AsRef;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||
use bitcoin::psbt;
|
||||
use bitcoin::transaction::{OutPoint, Sequence, TxOut};
|
||||
use bitcoin::{psbt, Weight};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -72,7 +72,7 @@ pub struct WeightedUtxo {
|
||||
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
||||
///
|
||||
/// [weight units]: https://en.bitcoin.it/wiki/Weight_units
|
||||
pub satisfaction_weight: usize,
|
||||
pub satisfaction_weight: Weight,
|
||||
/// The UTXO
|
||||
pub utxo: Utxo,
|
||||
}
|
||||
|
||||
209
crates/wallet/src/wallet/changeset.rs
Normal file
209
crates/wallet/src/wallet/changeset.rs
Normal file
@@ -0,0 +1,209 @@
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph, keychain_txout, local_chain, tx_graph, ConfirmationBlockTime, Merge,
|
||||
};
|
||||
use miniscript::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
type IndexedTxGraphChangeSet =
|
||||
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>;
|
||||
|
||||
/// A changeset for [`Wallet`](crate::Wallet).
|
||||
#[derive(Default, Debug, Clone, PartialEq, serde::Deserialize, serde::Serialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct ChangeSet {
|
||||
/// Descriptor for recipient addresses.
|
||||
pub descriptor: Option<Descriptor<DescriptorPublicKey>>,
|
||||
/// Descriptor for change addresses.
|
||||
pub change_descriptor: Option<Descriptor<DescriptorPublicKey>>,
|
||||
/// Stores the network type of the transaction data.
|
||||
pub network: Option<bitcoin::Network>,
|
||||
/// Changes to the [`LocalChain`](local_chain::LocalChain).
|
||||
pub local_chain: local_chain::ChangeSet,
|
||||
/// Changes to [`TxGraph`](tx_graph::TxGraph).
|
||||
pub tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>,
|
||||
/// Changes to [`KeychainTxOutIndex`](keychain_txout::KeychainTxOutIndex).
|
||||
pub indexer: keychain_txout::ChangeSet,
|
||||
}
|
||||
|
||||
impl Merge for ChangeSet {
|
||||
/// Merge another [`ChangeSet`] into itself.
|
||||
fn merge(&mut self, other: Self) {
|
||||
if other.descriptor.is_some() {
|
||||
debug_assert!(
|
||||
self.descriptor.is_none() || self.descriptor == other.descriptor,
|
||||
"descriptor must never change"
|
||||
);
|
||||
self.descriptor = other.descriptor;
|
||||
}
|
||||
if other.change_descriptor.is_some() {
|
||||
debug_assert!(
|
||||
self.change_descriptor.is_none()
|
||||
|| self.change_descriptor == other.change_descriptor,
|
||||
"change descriptor must never change"
|
||||
);
|
||||
self.change_descriptor = other.change_descriptor;
|
||||
}
|
||||
if other.network.is_some() {
|
||||
debug_assert!(
|
||||
self.network.is_none() || self.network == other.network,
|
||||
"network must never change"
|
||||
);
|
||||
self.network = other.network;
|
||||
}
|
||||
|
||||
Merge::merge(&mut self.local_chain, other.local_chain);
|
||||
Merge::merge(&mut self.tx_graph, other.tx_graph);
|
||||
Merge::merge(&mut self.indexer, other.indexer);
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.descriptor.is_none()
|
||||
&& self.change_descriptor.is_none()
|
||||
&& self.network.is_none()
|
||||
&& self.local_chain.is_empty()
|
||||
&& self.tx_graph.is_empty()
|
||||
&& self.indexer.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rusqlite")]
|
||||
impl ChangeSet {
|
||||
/// Schema name for wallet.
|
||||
pub const WALLET_SCHEMA_NAME: &'static str = "bdk_wallet";
|
||||
/// Name of table to store wallet descriptors and network.
|
||||
pub const WALLET_TABLE_NAME: &'static str = "bdk_wallet";
|
||||
|
||||
/// Initialize sqlite tables for wallet schema & table.
|
||||
fn init_wallet_sqlite_tables(
|
||||
db_tx: &chain::rusqlite::Transaction,
|
||||
) -> chain::rusqlite::Result<()> {
|
||||
let schema_v0: &[&str] = &[&format!(
|
||||
"CREATE TABLE {} ( \
|
||||
id INTEGER PRIMARY KEY NOT NULL CHECK (id = 0), \
|
||||
descriptor TEXT, \
|
||||
change_descriptor TEXT, \
|
||||
network TEXT \
|
||||
) STRICT;",
|
||||
Self::WALLET_TABLE_NAME,
|
||||
)];
|
||||
crate::rusqlite_impl::migrate_schema(db_tx, Self::WALLET_SCHEMA_NAME, &[schema_v0])
|
||||
}
|
||||
|
||||
/// Recover a [`ChangeSet`] from sqlite database.
|
||||
pub fn from_sqlite(db_tx: &chain::rusqlite::Transaction) -> chain::rusqlite::Result<Self> {
|
||||
Self::init_wallet_sqlite_tables(db_tx)?;
|
||||
use chain::rusqlite::OptionalExtension;
|
||||
use chain::Impl;
|
||||
|
||||
let mut changeset = Self::default();
|
||||
|
||||
let mut wallet_statement = db_tx.prepare(&format!(
|
||||
"SELECT descriptor, change_descriptor, network FROM {}",
|
||||
Self::WALLET_TABLE_NAME,
|
||||
))?;
|
||||
let row = wallet_statement
|
||||
.query_row([], |row| {
|
||||
Ok((
|
||||
row.get::<_, Impl<Descriptor<DescriptorPublicKey>>>("descriptor")?,
|
||||
row.get::<_, Impl<Descriptor<DescriptorPublicKey>>>("change_descriptor")?,
|
||||
row.get::<_, Impl<bitcoin::Network>>("network")?,
|
||||
))
|
||||
})
|
||||
.optional()?;
|
||||
if let Some((Impl(desc), Impl(change_desc), Impl(network))) = row {
|
||||
changeset.descriptor = Some(desc);
|
||||
changeset.change_descriptor = Some(change_desc);
|
||||
changeset.network = Some(network);
|
||||
}
|
||||
|
||||
changeset.local_chain = local_chain::ChangeSet::from_sqlite(db_tx)?;
|
||||
changeset.tx_graph = tx_graph::ChangeSet::<_>::from_sqlite(db_tx)?;
|
||||
changeset.indexer = keychain_txout::ChangeSet::from_sqlite(db_tx)?;
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Persist [`ChangeSet`] to sqlite database.
|
||||
pub fn persist_to_sqlite(
|
||||
&self,
|
||||
db_tx: &chain::rusqlite::Transaction,
|
||||
) -> chain::rusqlite::Result<()> {
|
||||
Self::init_wallet_sqlite_tables(db_tx)?;
|
||||
use chain::rusqlite::named_params;
|
||||
use chain::Impl;
|
||||
|
||||
let mut descriptor_statement = db_tx.prepare_cached(&format!(
|
||||
"INSERT INTO {}(id, descriptor) VALUES(:id, :descriptor) ON CONFLICT(id) DO UPDATE SET descriptor=:descriptor",
|
||||
Self::WALLET_TABLE_NAME,
|
||||
))?;
|
||||
if let Some(descriptor) = &self.descriptor {
|
||||
descriptor_statement.execute(named_params! {
|
||||
":id": 0,
|
||||
":descriptor": Impl(descriptor.clone()),
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut change_descriptor_statement = db_tx.prepare_cached(&format!(
|
||||
"INSERT INTO {}(id, change_descriptor) VALUES(:id, :change_descriptor) ON CONFLICT(id) DO UPDATE SET change_descriptor=:change_descriptor",
|
||||
Self::WALLET_TABLE_NAME,
|
||||
))?;
|
||||
if let Some(change_descriptor) = &self.change_descriptor {
|
||||
change_descriptor_statement.execute(named_params! {
|
||||
":id": 0,
|
||||
":change_descriptor": Impl(change_descriptor.clone()),
|
||||
})?;
|
||||
}
|
||||
|
||||
let mut network_statement = db_tx.prepare_cached(&format!(
|
||||
"INSERT INTO {}(id, network) VALUES(:id, :network) ON CONFLICT(id) DO UPDATE SET network=:network",
|
||||
Self::WALLET_TABLE_NAME,
|
||||
))?;
|
||||
if let Some(network) = self.network {
|
||||
network_statement.execute(named_params! {
|
||||
":id": 0,
|
||||
":network": Impl(network),
|
||||
})?;
|
||||
}
|
||||
|
||||
self.local_chain.persist_to_sqlite(db_tx)?;
|
||||
self.tx_graph.persist_to_sqlite(db_tx)?;
|
||||
self.indexer.persist_to_sqlite(db_tx)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<local_chain::ChangeSet> for ChangeSet {
|
||||
fn from(chain: local_chain::ChangeSet) -> Self {
|
||||
Self {
|
||||
local_chain: chain,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IndexedTxGraphChangeSet> for ChangeSet {
|
||||
fn from(indexed_tx_graph: IndexedTxGraphChangeSet) -> Self {
|
||||
Self {
|
||||
tx_graph: indexed_tx_graph.tx_graph,
|
||||
indexer: indexed_tx_graph.indexer,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tx_graph::ChangeSet<ConfirmationBlockTime>> for ChangeSet {
|
||||
fn from(tx_graph: tx_graph::ChangeSet<ConfirmationBlockTime>) -> Self {
|
||||
Self {
|
||||
tx_graph,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<keychain_txout::ChangeSet> for ChangeSet {
|
||||
fn from(indexer: keychain_txout::ChangeSet) -> Self {
|
||||
Self {
|
||||
indexer,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,10 +26,10 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::wallet::{self, ChangeSet, coin_selection::*, coin_selection};
|
||||
//! # use bdk_wallet::wallet::error::CreateTxError;
|
||||
//! # use bdk_wallet::{self, ChangeSet, coin_selection::*, coin_selection};
|
||||
//! # use bdk_wallet::error::CreateTxError;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # use bdk_wallet::wallet::coin_selection::decide_change;
|
||||
//! # use bdk_wallet::coin_selection::decide_change;
|
||||
//! # use anyhow::Error;
|
||||
//! #[derive(Debug)]
|
||||
//! struct AlwaysSpendEverything;
|
||||
@@ -52,11 +52,10 @@
|
||||
//! (&mut selected_amount, &mut additional_weight),
|
||||
//! |(selected_amount, additional_weight), weighted_utxo| {
|
||||
//! **selected_amount += weighted_utxo.utxo.txout().value.to_sat();
|
||||
//! **additional_weight += Weight::from_wu(
|
||||
//! (TxIn::default().segwit_weight().to_wu()
|
||||
//! + weighted_utxo.satisfaction_weight as u64)
|
||||
//! as u64,
|
||||
//! );
|
||||
//! **additional_weight += TxIn::default()
|
||||
//! .segwit_weight()
|
||||
//! .checked_add(weighted_utxo.satisfaction_weight)
|
||||
//! .expect("`Weight` addition should not cause an integer overflow");
|
||||
//! Some(weighted_utxo.utxo)
|
||||
//! },
|
||||
//! )
|
||||
@@ -114,8 +113,9 @@ use bitcoin::{Script, Weight};
|
||||
|
||||
use core::convert::TryInto;
|
||||
use core::fmt::{self, Formatter};
|
||||
use rand::seq::SliceRandom;
|
||||
use rand_core::RngCore;
|
||||
|
||||
use super::utils::shuffle_slice;
|
||||
/// Default coin selection algorithm used by [`TxBuilder`](super::tx_builder::TxBuilder) if not
|
||||
/// overridden
|
||||
pub type DefaultCoinSelectionAlgorithm = BranchAndBoundCoinSelection;
|
||||
@@ -343,10 +343,10 @@ fn select_sorted_utxos(
|
||||
|(selected_amount, fee_amount), (must_use, weighted_utxo)| {
|
||||
if must_use || **selected_amount < target_amount + **fee_amount {
|
||||
**fee_amount += (fee_rate
|
||||
* Weight::from_wu(
|
||||
TxIn::default().segwit_weight().to_wu()
|
||||
+ weighted_utxo.satisfaction_weight as u64,
|
||||
))
|
||||
* (TxIn::default()
|
||||
.segwit_weight()
|
||||
.checked_add(weighted_utxo.satisfaction_weight)
|
||||
.expect("`Weight` addition should not cause an integer overflow")))
|
||||
.to_sat();
|
||||
**selected_amount += weighted_utxo.utxo.txout().value.to_sat();
|
||||
Some(weighted_utxo.utxo)
|
||||
@@ -389,9 +389,10 @@ struct OutputGroup {
|
||||
impl OutputGroup {
|
||||
fn new(weighted_utxo: WeightedUtxo, fee_rate: FeeRate) -> Self {
|
||||
let fee = (fee_rate
|
||||
* Weight::from_wu(
|
||||
TxIn::default().segwit_weight().to_wu() + weighted_utxo.satisfaction_weight as u64,
|
||||
))
|
||||
* (TxIn::default()
|
||||
.segwit_weight()
|
||||
.checked_add(weighted_utxo.satisfaction_weight)
|
||||
.expect("`Weight` addition should not cause an integer overflow")))
|
||||
.to_sat();
|
||||
let effective_value = weighted_utxo.utxo.txout().value.to_sat() as i64 - fee as i64;
|
||||
OutputGroup {
|
||||
@@ -516,27 +517,16 @@ impl CoinSelectionAlgorithm for BranchAndBoundCoinSelection {
|
||||
));
|
||||
}
|
||||
|
||||
Ok(self
|
||||
.bnb(
|
||||
required_utxos.clone(),
|
||||
optional_utxos.clone(),
|
||||
curr_value,
|
||||
curr_available_value,
|
||||
target_amount,
|
||||
cost_of_change,
|
||||
drain_script,
|
||||
fee_rate,
|
||||
)
|
||||
.unwrap_or_else(|_| {
|
||||
self.single_random_draw(
|
||||
required_utxos,
|
||||
optional_utxos,
|
||||
curr_value,
|
||||
target_amount,
|
||||
drain_script,
|
||||
fee_rate,
|
||||
)
|
||||
}))
|
||||
self.bnb(
|
||||
required_utxos.clone(),
|
||||
optional_utxos.clone(),
|
||||
curr_value,
|
||||
curr_available_value,
|
||||
target_amount,
|
||||
cost_of_change,
|
||||
drain_script,
|
||||
fee_rate,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -663,40 +653,6 @@ impl BranchAndBoundCoinSelection {
|
||||
))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn single_random_draw(
|
||||
&self,
|
||||
required_utxos: Vec<OutputGroup>,
|
||||
mut optional_utxos: Vec<OutputGroup>,
|
||||
curr_value: i64,
|
||||
target_amount: i64,
|
||||
drain_script: &Script,
|
||||
fee_rate: FeeRate,
|
||||
) -> CoinSelectionResult {
|
||||
optional_utxos.shuffle(&mut rand::thread_rng());
|
||||
let selected_utxos = optional_utxos.into_iter().fold(
|
||||
(curr_value, vec![]),
|
||||
|(mut amount, mut utxos), utxo| {
|
||||
if amount >= target_amount {
|
||||
(amount, utxos)
|
||||
} else {
|
||||
amount += utxo.effective_value;
|
||||
utxos.push(utxo);
|
||||
(amount, utxos)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// remaining_amount can't be negative as that would mean the
|
||||
// selection wasn't successful
|
||||
// target_amount = amount_needed + (fee_amount - vin_fees)
|
||||
let remaining_amount = (selected_utxos.0 - target_amount) as u64;
|
||||
|
||||
let excess = decide_change(remaining_amount, fee_rate, drain_script);
|
||||
|
||||
BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
|
||||
}
|
||||
|
||||
fn calculate_cs_result(
|
||||
mut selected_utxos: Vec<OutputGroup>,
|
||||
mut required_utxos: Vec<OutputGroup>,
|
||||
@@ -717,6 +673,58 @@ impl BranchAndBoundCoinSelection {
|
||||
}
|
||||
}
|
||||
|
||||
// Pull UTXOs at random until we have enough to meet the target
|
||||
pub(crate) fn single_random_draw(
|
||||
required_utxos: Vec<WeightedUtxo>,
|
||||
optional_utxos: Vec<WeightedUtxo>,
|
||||
target_amount: u64,
|
||||
drain_script: &Script,
|
||||
fee_rate: FeeRate,
|
||||
rng: &mut impl RngCore,
|
||||
) -> CoinSelectionResult {
|
||||
let target_amount = target_amount
|
||||
.try_into()
|
||||
.expect("Bitcoin amount to fit into i64");
|
||||
|
||||
let required_utxos: Vec<OutputGroup> = required_utxos
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
.collect();
|
||||
|
||||
let mut optional_utxos: Vec<OutputGroup> = optional_utxos
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
.collect();
|
||||
|
||||
let curr_value = required_utxos
|
||||
.iter()
|
||||
.fold(0, |acc, x| acc + x.effective_value);
|
||||
|
||||
shuffle_slice(&mut optional_utxos, rng);
|
||||
|
||||
let selected_utxos =
|
||||
optional_utxos
|
||||
.into_iter()
|
||||
.fold((curr_value, vec![]), |(mut amount, mut utxos), utxo| {
|
||||
if amount >= target_amount {
|
||||
(amount, utxos)
|
||||
} else {
|
||||
amount += utxo.effective_value;
|
||||
utxos.push(utxo);
|
||||
(amount, utxos)
|
||||
}
|
||||
});
|
||||
|
||||
// remaining_amount can't be negative as that would mean the
|
||||
// selection wasn't successful
|
||||
// target_amount = amount_needed + (fee_amount - vin_fees)
|
||||
let remaining_amount = (selected_utxos.0 - target_amount) as u64;
|
||||
|
||||
let excess = decide_change(remaining_amount, fee_rate, drain_script);
|
||||
|
||||
BranchAndBoundCoinSelection::calculate_cs_result(selected_utxos.1, required_utxos, excess)
|
||||
}
|
||||
|
||||
/// Remove duplicate UTXOs.
|
||||
///
|
||||
/// If a UTXO appears in both `required` and `optional`, the appearance in `required` is kept.
|
||||
@@ -740,6 +748,7 @@ where
|
||||
mod test {
|
||||
use assert_matches::assert_matches;
|
||||
use core::str::FromStr;
|
||||
use rand::rngs::StdRng;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::{Amount, ScriptBuf, TxIn, TxOut};
|
||||
@@ -748,8 +757,7 @@ mod test {
|
||||
use crate::types::*;
|
||||
use crate::wallet::coin_selection::filter_duplicates;
|
||||
|
||||
use rand::rngs::StdRng;
|
||||
use rand::seq::SliceRandom;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::{Rng, RngCore, SeedableRng};
|
||||
|
||||
// signature len (1WU) + signature and sighash (72WU)
|
||||
@@ -766,7 +774,7 @@ mod test {
|
||||
))
|
||||
.unwrap();
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint,
|
||||
txout: TxOut {
|
||||
@@ -826,7 +834,7 @@ mod test {
|
||||
let mut res = Vec::new();
|
||||
for i in 0..utxos_number {
|
||||
res.push(WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::from_str(&format!(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
|
||||
@@ -857,7 +865,7 @@ mod test {
|
||||
fn generate_same_value_utxos(utxos_value: u64, utxos_number: usize) -> Vec<WeightedUtxo> {
|
||||
(0..utxos_number)
|
||||
.map(|i| WeightedUtxo {
|
||||
satisfaction_weight: P2WPKH_SATISFACTION_SIZE,
|
||||
satisfaction_weight: Weight::from_wu_usize(P2WPKH_SATISFACTION_SIZE),
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::from_str(&format!(
|
||||
"ebd9813ecebc57ff8f30797de7c205e3c7498ca950ea4341ee51a685ff2fa30a:{}",
|
||||
@@ -1090,13 +1098,12 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "SRD fn was moved out of BnB"]
|
||||
fn test_bnb_coin_selection_success() {
|
||||
// In this case bnb won't find a suitable match and single random draw will
|
||||
// select three outputs
|
||||
let utxos = generate_same_value_utxos(100_000, 20);
|
||||
|
||||
let drain_script = ScriptBuf::default();
|
||||
|
||||
let target_amount = 250_000 + FEE_AMOUNT;
|
||||
|
||||
let result = BranchAndBoundCoinSelection::default()
|
||||
@@ -1136,6 +1143,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "no exact match for bnb, previously fell back to SRD"]
|
||||
fn test_bnb_coin_selection_optional_are_enough() {
|
||||
let utxos = get_test_utxos();
|
||||
let drain_script = ScriptBuf::default();
|
||||
@@ -1156,6 +1164,26 @@ mod test {
|
||||
assert_eq!(result.fee_amount, 136);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_random_draw_function_success() {
|
||||
let seed = [0; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
let mut utxos = generate_random_utxos(&mut rng, 300);
|
||||
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||
let drain_script = ScriptBuf::default();
|
||||
let result = single_random_draw(
|
||||
vec![],
|
||||
utxos,
|
||||
target_amount,
|
||||
&drain_script,
|
||||
fee_rate,
|
||||
&mut rng,
|
||||
);
|
||||
assert!(result.selected_amount() > target_amount);
|
||||
assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_bnb_coin_selection_required_not_enough() {
|
||||
@@ -1410,34 +1438,6 @@ mod test {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_random_draw_function_success() {
|
||||
let seed = [0; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
let mut utxos = generate_random_utxos(&mut rng, 300);
|
||||
let target_amount = sum_random_utxos(&mut rng, &mut utxos) + FEE_AMOUNT;
|
||||
|
||||
let fee_rate = FeeRate::from_sat_per_vb_unchecked(1);
|
||||
let utxos: Vec<OutputGroup> = utxos
|
||||
.into_iter()
|
||||
.map(|u| OutputGroup::new(u, fee_rate))
|
||||
.collect();
|
||||
|
||||
let drain_script = ScriptBuf::default();
|
||||
|
||||
let result = BranchAndBoundCoinSelection::default().single_random_draw(
|
||||
vec![],
|
||||
utxos,
|
||||
0,
|
||||
target_amount as i64,
|
||||
&drain_script,
|
||||
fee_rate,
|
||||
);
|
||||
|
||||
assert!(result.selected_amount() > target_amount);
|
||||
assert_eq!(result.fee_amount, (result.selected.len() * 68) as u64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bnb_exclude_negative_effective_value() {
|
||||
let utxos = get_test_utxos();
|
||||
@@ -1512,7 +1512,7 @@ mod test {
|
||||
fn test_filter_duplicates() {
|
||||
fn utxo(txid: &str, value: u64) -> WeightedUtxo {
|
||||
WeightedUtxo {
|
||||
satisfaction_weight: 0,
|
||||
satisfaction_weight: Weight::ZERO,
|
||||
utxo: Utxo::Local(LocalOutput {
|
||||
outpoint: OutPoint::new(bitcoin::hashes::Hash::hash(txid.as_bytes()), 0),
|
||||
txout: TxOut {
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::wallet::export::*;
|
||||
//! # use bdk_wallet::export::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! let import = r#"{
|
||||
//! "descriptor": "wpkh([c258d2e4\/84h\/1h\/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe\/0\/*)",
|
||||
@@ -29,24 +29,26 @@
|
||||
//! }"#;
|
||||
//!
|
||||
//! let import = FullyNodedExport::from_str(import)?;
|
||||
//! let wallet = Wallet::new(
|
||||
//! &import.descriptor(),
|
||||
//! &import.change_descriptor().expect("change descriptor"),
|
||||
//! Network::Testnet,
|
||||
//! )?;
|
||||
//! let wallet = Wallet::create(
|
||||
//! import.descriptor(),
|
||||
//! import.change_descriptor().expect("change descriptor"),
|
||||
//! )
|
||||
//! .network(Network::Testnet)
|
||||
//! .create_wallet_no_persist()?;
|
||||
//! # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
//!
|
||||
//! ### Export a `Wallet`
|
||||
//! ```
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::wallet::export::*;
|
||||
//! # use bdk_wallet::export::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! let wallet = Wallet::new(
|
||||
//! let wallet = Wallet::create(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)",
|
||||
//! Network::Testnet,
|
||||
//! )?;
|
||||
//! )
|
||||
//! .network(Network::Testnet)
|
||||
//! .create_wallet_no_persist()?;
|
||||
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
|
||||
//!
|
||||
//! println!("Exported: {}", export.to_string());
|
||||
@@ -116,7 +118,7 @@ impl FullyNodedExport {
|
||||
include_blockheight: bool,
|
||||
) -> Result<Self, &'static str> {
|
||||
let descriptor = wallet
|
||||
.get_descriptor_for_keychain(KeychainKind::External)
|
||||
.public_descriptor(KeychainKind::External)
|
||||
.to_string_with_secret(
|
||||
&wallet
|
||||
.get_signers(KeychainKind::External)
|
||||
@@ -128,7 +130,7 @@ impl FullyNodedExport {
|
||||
let blockheight = if include_blockheight {
|
||||
wallet.transactions().next().map_or(0, |canonical_tx| {
|
||||
match canonical_tx.chain_position {
|
||||
bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height,
|
||||
bdk_chain::ChainPosition::Confirmed(a) => a.block_id.height,
|
||||
bdk_chain::ChainPosition::Unconfirmed(_) => 0,
|
||||
}
|
||||
})
|
||||
@@ -144,7 +146,7 @@ impl FullyNodedExport {
|
||||
|
||||
let change_descriptor = {
|
||||
let descriptor = wallet
|
||||
.get_descriptor_for_keychain(KeychainKind::Internal)
|
||||
.public_descriptor(KeychainKind::Internal)
|
||||
.to_string_with_secret(
|
||||
&wallet
|
||||
.get_signers(KeychainKind::Internal)
|
||||
@@ -214,35 +216,50 @@ mod test {
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::std::string::ToString;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bdk_chain::{BlockId, ConfirmationBlockTime};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::{transaction, BlockHash, Network, Transaction};
|
||||
|
||||
use super::*;
|
||||
use crate::wallet::Wallet;
|
||||
use crate::Wallet;
|
||||
|
||||
fn get_test_wallet(descriptor: &str, change_descriptor: &str, network: Network) -> Wallet {
|
||||
let mut wallet = Wallet::new(descriptor, change_descriptor, network).unwrap();
|
||||
use crate::wallet::Update;
|
||||
use bdk_chain::TxGraph;
|
||||
let mut wallet = Wallet::create(descriptor.to_string(), change_descriptor.to_string())
|
||||
.network(network)
|
||||
.create_wallet_no_persist()
|
||||
.expect("must create wallet");
|
||||
let transaction = Transaction {
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
version: transaction::Version::non_standard(0),
|
||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||
};
|
||||
let txid = transaction.compute_txid();
|
||||
let block_id = BlockId {
|
||||
height: 5000,
|
||||
hash: BlockHash::all_zeros(),
|
||||
};
|
||||
wallet.insert_checkpoint(block_id).unwrap();
|
||||
wallet
|
||||
.insert_checkpoint(BlockId {
|
||||
height: 5001,
|
||||
hash: BlockHash::all_zeros(),
|
||||
})
|
||||
.unwrap();
|
||||
wallet.insert_tx(transaction);
|
||||
let anchor = ConfirmationBlockTime {
|
||||
confirmation_time: 0,
|
||||
block_id,
|
||||
};
|
||||
let mut graph = TxGraph::default();
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
wallet
|
||||
.insert_tx(
|
||||
transaction,
|
||||
ConfirmationTime::Confirmed {
|
||||
height: 5000,
|
||||
time: 0,
|
||||
},
|
||||
)
|
||||
.apply_update(Update {
|
||||
graph,
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
wallet
|
||||
}
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
//! ```no_run
|
||||
//! # use bdk_wallet::bitcoin::Network;
|
||||
//! # use bdk_wallet::signer::SignerOrdering;
|
||||
//! # use bdk_wallet::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk_wallet::wallet::AddressIndex::New;
|
||||
//! # use bdk_wallet::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use bdk_wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk_wallet::AddressIndex::New;
|
||||
//! # use bdk_wallet::{CreateParams, KeychainKind, SignOptions};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
@@ -30,11 +30,7 @@
|
||||
//! let first_device = devices.remove(0)?;
|
||||
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
|
||||
//!
|
||||
//! # let mut wallet = Wallet::new(
|
||||
//! # "",
|
||||
//! # None,
|
||||
//! # Network::Testnet,
|
||||
//! # )?;
|
||||
//! # let mut wallet = CreateParams::new("", "", Network::Testnet)?.create_wallet_no_persist()?;
|
||||
//! #
|
||||
//! // Adding the hardware signer to the BDK wallet
|
||||
//! wallet.add_signer(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
213
crates/wallet/src/wallet/params.rs
Normal file
213
crates/wallet/src/wallet/params.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use alloc::boxed::Box;
|
||||
use bdk_chain::{keychain_txout::DEFAULT_LOOKAHEAD, PersistAsyncWith, PersistWith};
|
||||
use bitcoin::{BlockHash, Network};
|
||||
use miniscript::descriptor::KeyMap;
|
||||
|
||||
use crate::{
|
||||
descriptor::{DescriptorError, ExtendedDescriptor, IntoWalletDescriptor},
|
||||
utils::SecpCtx,
|
||||
KeychainKind, Wallet,
|
||||
};
|
||||
|
||||
use super::{ChangeSet, LoadError, PersistedWallet};
|
||||
|
||||
/// This atrocity is to avoid having type parameters on [`CreateParams`] and [`LoadParams`].
|
||||
///
|
||||
/// The better option would be to do `Box<dyn IntoWalletDescriptor>`, but we cannot due to Rust's
|
||||
/// [object safety rules](https://doc.rust-lang.org/reference/items/traits.html#object-safety).
|
||||
type DescriptorToExtract = Box<
|
||||
dyn FnOnce(&SecpCtx, Network) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError>
|
||||
+ 'static,
|
||||
>;
|
||||
|
||||
fn make_descriptor_to_extract<D>(descriptor: D) -> DescriptorToExtract
|
||||
where
|
||||
D: IntoWalletDescriptor + 'static,
|
||||
{
|
||||
Box::new(|secp, network| descriptor.into_wallet_descriptor(secp, network))
|
||||
}
|
||||
|
||||
/// Parameters for [`Wallet::create`] or [`PersistedWallet::create`].
|
||||
#[must_use]
|
||||
pub struct CreateParams {
|
||||
pub(crate) descriptor: DescriptorToExtract,
|
||||
pub(crate) descriptor_keymap: KeyMap,
|
||||
pub(crate) change_descriptor: DescriptorToExtract,
|
||||
pub(crate) change_descriptor_keymap: KeyMap,
|
||||
pub(crate) network: Network,
|
||||
pub(crate) genesis_hash: Option<BlockHash>,
|
||||
pub(crate) lookahead: u32,
|
||||
}
|
||||
|
||||
impl CreateParams {
|
||||
/// Construct parameters with provided `descriptor`, `change_descriptor` and `network`.
|
||||
///
|
||||
/// Default values: `genesis_hash` = `None`, `lookahead` = [`DEFAULT_LOOKAHEAD`]
|
||||
pub fn new<D: IntoWalletDescriptor + 'static>(descriptor: D, change_descriptor: D) -> Self {
|
||||
Self {
|
||||
descriptor: make_descriptor_to_extract(descriptor),
|
||||
descriptor_keymap: KeyMap::default(),
|
||||
change_descriptor: make_descriptor_to_extract(change_descriptor),
|
||||
change_descriptor_keymap: KeyMap::default(),
|
||||
network: Network::Bitcoin,
|
||||
genesis_hash: None,
|
||||
lookahead: DEFAULT_LOOKAHEAD,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend the given `keychain`'s `keymap`.
|
||||
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
|
||||
match keychain {
|
||||
KeychainKind::External => &mut self.descriptor_keymap,
|
||||
KeychainKind::Internal => &mut self.change_descriptor_keymap,
|
||||
}
|
||||
.extend(keymap);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set `network`.
|
||||
pub fn network(mut self, network: Network) -> Self {
|
||||
self.network = network;
|
||||
self
|
||||
}
|
||||
|
||||
/// Use a custom `genesis_hash`.
|
||||
pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
|
||||
self.genesis_hash = Some(genesis_hash);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use custom lookahead value.
|
||||
pub fn lookahead(mut self, lookahead: u32) -> Self {
|
||||
self.lookahead = lookahead;
|
||||
self
|
||||
}
|
||||
|
||||
/// Create [`PersistedWallet`] with the given `Db`.
|
||||
pub fn create_wallet<Db>(
|
||||
self,
|
||||
db: &mut Db,
|
||||
) -> Result<PersistedWallet, <Wallet as PersistWith<Db>>::CreateError>
|
||||
where
|
||||
Wallet: PersistWith<Db, CreateParams = Self>,
|
||||
{
|
||||
PersistedWallet::create(db, self)
|
||||
}
|
||||
|
||||
/// Create [`PersistedWallet`] with the given async `Db`.
|
||||
pub async fn create_wallet_async<Db>(
|
||||
self,
|
||||
db: &mut Db,
|
||||
) -> Result<PersistedWallet, <Wallet as PersistAsyncWith<Db>>::CreateError>
|
||||
where
|
||||
Wallet: PersistAsyncWith<Db, CreateParams = Self>,
|
||||
{
|
||||
PersistedWallet::create_async(db, self).await
|
||||
}
|
||||
|
||||
/// Create [`Wallet`] without persistence.
|
||||
pub fn create_wallet_no_persist(self) -> Result<Wallet, DescriptorError> {
|
||||
Wallet::create_with_params(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters for [`Wallet::load`] or [`PersistedWallet::load`].
|
||||
#[must_use]
|
||||
pub struct LoadParams {
|
||||
pub(crate) descriptor_keymap: KeyMap,
|
||||
pub(crate) change_descriptor_keymap: KeyMap,
|
||||
pub(crate) lookahead: u32,
|
||||
pub(crate) check_network: Option<Network>,
|
||||
pub(crate) check_genesis_hash: Option<BlockHash>,
|
||||
pub(crate) check_descriptor: Option<DescriptorToExtract>,
|
||||
pub(crate) check_change_descriptor: Option<DescriptorToExtract>,
|
||||
}
|
||||
|
||||
impl LoadParams {
|
||||
/// Construct parameters with default values.
|
||||
///
|
||||
/// Default values: `lookahead` = [`DEFAULT_LOOKAHEAD`]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
descriptor_keymap: KeyMap::default(),
|
||||
change_descriptor_keymap: KeyMap::default(),
|
||||
lookahead: DEFAULT_LOOKAHEAD,
|
||||
check_network: None,
|
||||
check_genesis_hash: None,
|
||||
check_descriptor: None,
|
||||
check_change_descriptor: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend the given `keychain`'s `keymap`.
|
||||
pub fn keymap(mut self, keychain: KeychainKind, keymap: KeyMap) -> Self {
|
||||
match keychain {
|
||||
KeychainKind::External => &mut self.descriptor_keymap,
|
||||
KeychainKind::Internal => &mut self.change_descriptor_keymap,
|
||||
}
|
||||
.extend(keymap);
|
||||
self
|
||||
}
|
||||
|
||||
/// Checks that `descriptor` of `keychain` matches this, and extracts private keys (if
|
||||
/// available).
|
||||
pub fn descriptors<D>(mut self, descriptor: D, change_descriptor: D) -> Self
|
||||
where
|
||||
D: IntoWalletDescriptor + 'static,
|
||||
{
|
||||
self.check_descriptor = Some(make_descriptor_to_extract(descriptor));
|
||||
self.check_change_descriptor = Some(make_descriptor_to_extract(change_descriptor));
|
||||
self
|
||||
}
|
||||
|
||||
/// Check for `network`.
|
||||
pub fn network(mut self, network: Network) -> Self {
|
||||
self.check_network = Some(network);
|
||||
self
|
||||
}
|
||||
|
||||
/// Check for a `genesis_hash`.
|
||||
pub fn genesis_hash(mut self, genesis_hash: BlockHash) -> Self {
|
||||
self.check_genesis_hash = Some(genesis_hash);
|
||||
self
|
||||
}
|
||||
|
||||
/// Use custom lookahead value.
|
||||
pub fn lookahead(mut self, lookahead: u32) -> Self {
|
||||
self.lookahead = lookahead;
|
||||
self
|
||||
}
|
||||
|
||||
/// Load [`PersistedWallet`] with the given `Db`.
|
||||
pub fn load_wallet<Db>(
|
||||
self,
|
||||
db: &mut Db,
|
||||
) -> Result<Option<PersistedWallet>, <Wallet as PersistWith<Db>>::LoadError>
|
||||
where
|
||||
Wallet: PersistWith<Db, LoadParams = Self>,
|
||||
{
|
||||
PersistedWallet::load(db, self)
|
||||
}
|
||||
|
||||
/// Load [`PersistedWallet`] with the given async `Db`.
|
||||
pub async fn load_wallet_async<Db>(
|
||||
self,
|
||||
db: &mut Db,
|
||||
) -> Result<Option<PersistedWallet>, <Wallet as PersistAsyncWith<Db>>::LoadError>
|
||||
where
|
||||
Wallet: PersistAsyncWith<Db, LoadParams = Self>,
|
||||
{
|
||||
PersistedWallet::load_async(db, self).await
|
||||
}
|
||||
|
||||
/// Load [`Wallet`] without persistence.
|
||||
pub fn load_wallet_no_persist(self, changeset: ChangeSet) -> Result<Option<Wallet>, LoadError> {
|
||||
Wallet::load_with_params(changeset, self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LoadParams {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
171
crates/wallet/src/wallet/persisted.rs
Normal file
171
crates/wallet/src/wallet/persisted.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use core::fmt;
|
||||
|
||||
use crate::{descriptor::DescriptorError, Wallet};
|
||||
|
||||
/// Represents a persisted wallet.
|
||||
pub type PersistedWallet = bdk_chain::Persisted<Wallet>;
|
||||
|
||||
#[cfg(feature = "rusqlite")]
|
||||
impl<'c> chain::PersistWith<bdk_chain::rusqlite::Transaction<'c>> for Wallet {
|
||||
type CreateParams = crate::CreateParams;
|
||||
type LoadParams = crate::LoadParams;
|
||||
|
||||
type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
|
||||
type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
|
||||
type PersistError = bdk_chain::rusqlite::Error;
|
||||
|
||||
fn create(
|
||||
db: &mut bdk_chain::rusqlite::Transaction<'c>,
|
||||
params: Self::CreateParams,
|
||||
) -> Result<Self, Self::CreateError> {
|
||||
let mut wallet =
|
||||
Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
changeset
|
||||
.persist_to_sqlite(db)
|
||||
.map_err(CreateWithPersistError::Persist)?;
|
||||
}
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
fn load(
|
||||
conn: &mut bdk_chain::rusqlite::Transaction<'c>,
|
||||
params: Self::LoadParams,
|
||||
) -> Result<Option<Self>, Self::LoadError> {
|
||||
let changeset =
|
||||
crate::ChangeSet::from_sqlite(conn).map_err(LoadWithPersistError::Persist)?;
|
||||
if chain::Merge::is_empty(&changeset) {
|
||||
return Ok(None);
|
||||
}
|
||||
Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
|
||||
}
|
||||
|
||||
fn persist(
|
||||
db: &mut bdk_chain::rusqlite::Transaction<'c>,
|
||||
changeset: &<Self as chain::Staged>::ChangeSet,
|
||||
) -> Result<(), Self::PersistError> {
|
||||
changeset.persist_to_sqlite(db)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rusqlite")]
|
||||
impl chain::PersistWith<bdk_chain::rusqlite::Connection> for Wallet {
|
||||
type CreateParams = crate::CreateParams;
|
||||
type LoadParams = crate::LoadParams;
|
||||
|
||||
type CreateError = CreateWithPersistError<bdk_chain::rusqlite::Error>;
|
||||
type LoadError = LoadWithPersistError<bdk_chain::rusqlite::Error>;
|
||||
type PersistError = bdk_chain::rusqlite::Error;
|
||||
|
||||
fn create(
|
||||
db: &mut bdk_chain::rusqlite::Connection,
|
||||
params: Self::CreateParams,
|
||||
) -> Result<Self, Self::CreateError> {
|
||||
let mut db_tx = db.transaction().map_err(CreateWithPersistError::Persist)?;
|
||||
let wallet = chain::PersistWith::create(&mut db_tx, params)?;
|
||||
db_tx.commit().map_err(CreateWithPersistError::Persist)?;
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
fn load(
|
||||
db: &mut bdk_chain::rusqlite::Connection,
|
||||
params: Self::LoadParams,
|
||||
) -> Result<Option<Self>, Self::LoadError> {
|
||||
let mut db_tx = db.transaction().map_err(LoadWithPersistError::Persist)?;
|
||||
let wallet_opt = chain::PersistWith::load(&mut db_tx, params)?;
|
||||
db_tx.commit().map_err(LoadWithPersistError::Persist)?;
|
||||
Ok(wallet_opt)
|
||||
}
|
||||
|
||||
fn persist(
|
||||
db: &mut bdk_chain::rusqlite::Connection,
|
||||
changeset: &<Self as chain::Staged>::ChangeSet,
|
||||
) -> Result<(), Self::PersistError> {
|
||||
let db_tx = db.transaction()?;
|
||||
changeset.persist_to_sqlite(&db_tx)?;
|
||||
db_tx.commit()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "file_store")]
|
||||
impl chain::PersistWith<bdk_file_store::Store<crate::ChangeSet>> for Wallet {
|
||||
type CreateParams = crate::CreateParams;
|
||||
type LoadParams = crate::LoadParams;
|
||||
type CreateError = CreateWithPersistError<std::io::Error>;
|
||||
type LoadError =
|
||||
LoadWithPersistError<bdk_file_store::AggregateChangesetsError<crate::ChangeSet>>;
|
||||
type PersistError = std::io::Error;
|
||||
|
||||
fn create(
|
||||
db: &mut bdk_file_store::Store<crate::ChangeSet>,
|
||||
params: Self::CreateParams,
|
||||
) -> Result<Self, Self::CreateError> {
|
||||
let mut wallet =
|
||||
Self::create_with_params(params).map_err(CreateWithPersistError::Descriptor)?;
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
db.append_changeset(&changeset)
|
||||
.map_err(CreateWithPersistError::Persist)?;
|
||||
}
|
||||
Ok(wallet)
|
||||
}
|
||||
|
||||
fn load(
|
||||
db: &mut bdk_file_store::Store<crate::ChangeSet>,
|
||||
params: Self::LoadParams,
|
||||
) -> Result<Option<Self>, Self::LoadError> {
|
||||
let changeset = db
|
||||
.aggregate_changesets()
|
||||
.map_err(LoadWithPersistError::Persist)?
|
||||
.unwrap_or_default();
|
||||
Self::load_with_params(changeset, params).map_err(LoadWithPersistError::InvalidChangeSet)
|
||||
}
|
||||
|
||||
fn persist(
|
||||
db: &mut bdk_file_store::Store<crate::ChangeSet>,
|
||||
changeset: &<Self as chain::Staged>::ChangeSet,
|
||||
) -> Result<(), Self::PersistError> {
|
||||
db.append_changeset(changeset)
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for [`PersistedWallet::load`].
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum LoadWithPersistError<E> {
|
||||
/// Error from persistence.
|
||||
Persist(E),
|
||||
/// Occurs when the loaded changeset cannot construct [`Wallet`].
|
||||
InvalidChangeSet(crate::LoadError),
|
||||
}
|
||||
|
||||
impl<E: fmt::Display> fmt::Display for LoadWithPersistError<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Persist(err) => fmt::Display::fmt(err, f),
|
||||
Self::InvalidChangeSet(err) => fmt::Display::fmt(&err, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<E: fmt::Debug + fmt::Display> std::error::Error for LoadWithPersistError<E> {}
|
||||
|
||||
/// Error type for [`PersistedWallet::create`].
|
||||
#[derive(Debug)]
|
||||
pub enum CreateWithPersistError<E> {
|
||||
/// Error from persistence.
|
||||
Persist(E),
|
||||
/// Occurs when the loaded changeset cannot construct [`Wallet`].
|
||||
Descriptor(DescriptorError),
|
||||
}
|
||||
|
||||
impl<E: fmt::Display> fmt::Display for CreateWithPersistError<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Persist(err) => fmt::Display::fmt(err, f),
|
||||
Self::Descriptor(err) => fmt::Display::fmt(&err, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<E: fmt::Debug + fmt::Display> std::error::Error for CreateWithPersistError<E> {}
|
||||
@@ -69,7 +69,9 @@
|
||||
//!
|
||||
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/0/*)";
|
||||
//! let change_descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/1/*)";
|
||||
//! let mut wallet = Wallet::new(descriptor, change_descriptor, Network::Testnet)?;
|
||||
//! let mut wallet = Wallet::create(descriptor, change_descriptor)
|
||||
//! .network(Network::Testnet)
|
||||
//! .create_wallet_no_persist()?;
|
||||
//! wallet.add_signer(
|
||||
//! KeychainKind::External,
|
||||
//! SignerOrdering(200),
|
||||
@@ -91,7 +93,7 @@ use bitcoin::bip32::{ChildNumber, DerivationPath, Fingerprint, Xpriv};
|
||||
use bitcoin::hashes::hash160;
|
||||
use bitcoin::secp256k1::Message;
|
||||
use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
|
||||
use bitcoin::{ecdsa, psbt, sighash, taproot, transaction};
|
||||
use bitcoin::{ecdsa, psbt, sighash, taproot};
|
||||
use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
|
||||
use bitcoin::{PrivateKey, Psbt, PublicKey};
|
||||
|
||||
@@ -99,7 +101,7 @@ use miniscript::descriptor::{
|
||||
Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
|
||||
InnerXKey, KeyMap, SinglePriv, SinglePubKey,
|
||||
};
|
||||
use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
|
||||
use miniscript::{SigType, ToPublicKey};
|
||||
|
||||
use super::utils::SecpCtx;
|
||||
use crate::descriptor::{DescriptorMeta, XKeyUtils};
|
||||
@@ -159,12 +161,10 @@ pub enum SignerError {
|
||||
NonStandardSighash,
|
||||
/// Invalid SIGHASH for the signing context in use
|
||||
InvalidSighash,
|
||||
/// Error while computing the hash to sign a P2WPKH input.
|
||||
SighashP2wpkh(sighash::P2wpkhError),
|
||||
/// Error while computing the hash to sign a Taproot input.
|
||||
SighashTaproot(sighash::TaprootError),
|
||||
/// Error while computing the hash, out of bounds access on the transaction inputs.
|
||||
TxInputsIndexError(transaction::InputsIndexError),
|
||||
/// PSBT sign error.
|
||||
Psbt(psbt::SignError),
|
||||
/// Miniscript PSBT error
|
||||
MiniscriptPsbt(MiniscriptPsbtError),
|
||||
/// To be used only by external libraries implementing [`InputSigner`] or
|
||||
@@ -173,24 +173,6 @@ pub enum SignerError {
|
||||
External(String),
|
||||
}
|
||||
|
||||
impl From<transaction::InputsIndexError> for SignerError {
|
||||
fn from(v: transaction::InputsIndexError) -> Self {
|
||||
Self::TxInputsIndexError(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sighash::P2wpkhError> for SignerError {
|
||||
fn from(e: sighash::P2wpkhError) -> Self {
|
||||
Self::SighashP2wpkh(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sighash::TaprootError> for SignerError {
|
||||
fn from(e: sighash::TaprootError) -> Self {
|
||||
Self::SighashTaproot(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for SignerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -205,9 +187,8 @@ impl fmt::Display for SignerError {
|
||||
Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"),
|
||||
Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
|
||||
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
|
||||
Self::SighashP2wpkh(err) => write!(f, "Error while computing the hash to sign a P2WPKH input: {}", err),
|
||||
Self::SighashTaproot(err) => write!(f, "Error while computing the hash to sign a Taproot input: {}", err),
|
||||
Self::TxInputsIndexError(err) => write!(f, "Error while computing the hash, out of bounds access on the transaction inputs: {}", err),
|
||||
Self::Psbt(err) => write!(f, "Error computing the sighash: {}", err),
|
||||
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
|
||||
Self::External(err) => write!(f, "{}", err),
|
||||
}
|
||||
@@ -472,93 +453,88 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
}
|
||||
|
||||
let pubkey = PublicKey::from_private_key(secp, self);
|
||||
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
|
||||
|
||||
if let SignerContext::Tap { is_internal_key } = self.ctx {
|
||||
if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key {
|
||||
if is_internal_key
|
||||
&& psbt.inputs[input_index].tap_key_sig.is_none()
|
||||
&& sign_options.sign_with_tap_internal_key
|
||||
&& x_only_pubkey == psbt_internal_key
|
||||
match self.ctx {
|
||||
SignerContext::Tap { is_internal_key } => {
|
||||
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
|
||||
|
||||
if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key {
|
||||
if is_internal_key
|
||||
&& psbt.inputs[input_index].tap_key_sig.is_none()
|
||||
&& sign_options.sign_with_tap_internal_key
|
||||
&& x_only_pubkey == psbt_internal_key
|
||||
{
|
||||
let (sighash, sighash_type) = compute_tap_sighash(psbt, input_index, None)?;
|
||||
sign_psbt_schnorr(
|
||||
&self.inner,
|
||||
x_only_pubkey,
|
||||
None,
|
||||
&mut psbt.inputs[input_index],
|
||||
sighash,
|
||||
sighash_type,
|
||||
secp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((leaf_hashes, _)) =
|
||||
psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey)
|
||||
{
|
||||
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
|
||||
sign_psbt_schnorr(
|
||||
&self.inner,
|
||||
x_only_pubkey,
|
||||
None,
|
||||
&mut psbt.inputs[input_index],
|
||||
hash,
|
||||
hash_ty,
|
||||
secp,
|
||||
);
|
||||
let leaf_hashes = leaf_hashes
|
||||
.iter()
|
||||
.filter(|lh| {
|
||||
// Removing the leaves we shouldn't sign for
|
||||
let should_sign = match &sign_options.tap_leaves_options {
|
||||
TapLeavesOptions::All => true,
|
||||
TapLeavesOptions::Include(v) => v.contains(lh),
|
||||
TapLeavesOptions::Exclude(v) => !v.contains(lh),
|
||||
TapLeavesOptions::None => false,
|
||||
};
|
||||
// Filtering out the leaves without our key
|
||||
should_sign
|
||||
&& !psbt.inputs[input_index]
|
||||
.tap_script_sigs
|
||||
.contains_key(&(x_only_pubkey, **lh))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for lh in leaf_hashes {
|
||||
let (sighash, sighash_type) =
|
||||
compute_tap_sighash(psbt, input_index, Some(lh))?;
|
||||
sign_psbt_schnorr(
|
||||
&self.inner,
|
||||
x_only_pubkey,
|
||||
Some(lh),
|
||||
&mut psbt.inputs[input_index],
|
||||
sighash,
|
||||
sighash_type,
|
||||
secp,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((leaf_hashes, _)) =
|
||||
psbt.inputs[input_index].tap_key_origins.get(&x_only_pubkey)
|
||||
{
|
||||
let leaf_hashes = leaf_hashes
|
||||
.iter()
|
||||
.filter(|lh| {
|
||||
// Removing the leaves we shouldn't sign for
|
||||
let should_sign = match &sign_options.tap_leaves_options {
|
||||
TapLeavesOptions::All => true,
|
||||
TapLeavesOptions::Include(v) => v.contains(lh),
|
||||
TapLeavesOptions::Exclude(v) => !v.contains(lh),
|
||||
TapLeavesOptions::None => false,
|
||||
};
|
||||
// Filtering out the leaves without our key
|
||||
should_sign
|
||||
&& !psbt.inputs[input_index]
|
||||
.tap_script_sigs
|
||||
.contains_key(&(x_only_pubkey, **lh))
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for lh in leaf_hashes {
|
||||
let (hash, hash_ty) = Tap::sighash(psbt, input_index, Some(lh))?;
|
||||
sign_psbt_schnorr(
|
||||
&self.inner,
|
||||
x_only_pubkey,
|
||||
Some(lh),
|
||||
&mut psbt.inputs[input_index],
|
||||
hash,
|
||||
hash_ty,
|
||||
secp,
|
||||
);
|
||||
SignerContext::Segwitv0 | SignerContext::Legacy => {
|
||||
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
let mut sighasher = sighash::SighashCache::new(psbt.unsigned_tx.clone());
|
||||
let (msg, sighash_type) = psbt
|
||||
.sighash_ecdsa(input_index, &mut sighasher)
|
||||
.map_err(SignerError::Psbt)?;
|
||||
|
||||
sign_psbt_ecdsa(
|
||||
&self.inner,
|
||||
pubkey,
|
||||
&mut psbt.inputs[input_index],
|
||||
&msg,
|
||||
sighash_type,
|
||||
secp,
|
||||
sign_options.allow_grinding,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if psbt.inputs[input_index].partial_sigs.contains_key(&pubkey) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (hash, hash_ty) = match self.ctx {
|
||||
SignerContext::Segwitv0 => {
|
||||
let (h, t) = Segwitv0::sighash(psbt, input_index, ())?;
|
||||
let h = h.to_raw_hash();
|
||||
(h, t)
|
||||
}
|
||||
SignerContext::Legacy => {
|
||||
let (h, t) = Legacy::sighash(psbt, input_index, ())?;
|
||||
let h = h.to_raw_hash();
|
||||
(h, t)
|
||||
}
|
||||
_ => return Ok(()), // handled above
|
||||
};
|
||||
sign_psbt_ecdsa(
|
||||
&self.inner,
|
||||
pubkey,
|
||||
&mut psbt.inputs[input_index],
|
||||
hash,
|
||||
hash_ty,
|
||||
secp,
|
||||
sign_options.allow_grinding,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -567,12 +543,11 @@ fn sign_psbt_ecdsa(
|
||||
secret_key: &secp256k1::SecretKey,
|
||||
pubkey: PublicKey,
|
||||
psbt_input: &mut psbt::Input,
|
||||
hash: impl bitcoin::hashes::Hash<Bytes = [u8; 32]>,
|
||||
msg: &Message,
|
||||
sighash_type: EcdsaSighashType,
|
||||
secp: &SecpCtx,
|
||||
allow_grinding: bool,
|
||||
) {
|
||||
let msg = &Message::from_digest(hash.to_byte_array());
|
||||
let signature = if allow_grinding {
|
||||
secp.sign_ecdsa_low_r(msg, secret_key)
|
||||
} else {
|
||||
@@ -594,7 +569,7 @@ fn sign_psbt_schnorr(
|
||||
pubkey: XOnlyPublicKey,
|
||||
leaf_hash: Option<taproot::TapLeafHash>,
|
||||
psbt_input: &mut psbt::Input,
|
||||
hash: TapSighash,
|
||||
sighash: TapSighash,
|
||||
sighash_type: TapSighashType,
|
||||
secp: &SecpCtx,
|
||||
) {
|
||||
@@ -606,8 +581,8 @@ fn sign_psbt_schnorr(
|
||||
Some(_) => keypair, // no tweak for script spend
|
||||
};
|
||||
|
||||
let msg = &Message::from(hash);
|
||||
let signature = secp.sign_schnorr(msg, &keypair);
|
||||
let msg = &Message::from(sighash);
|
||||
let signature = secp.sign_schnorr_no_aux_rand(msg, &keypair);
|
||||
secp.verify_schnorr(&signature, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
|
||||
.expect("invalid or corrupted schnorr signature");
|
||||
|
||||
@@ -801,21 +776,6 @@ pub struct SignOptions {
|
||||
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
|
||||
pub allow_all_sighashes: bool,
|
||||
|
||||
/// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
|
||||
///
|
||||
/// Defaults to `true` which will remove partial signatures during finalization.
|
||||
pub remove_partial_sigs: bool,
|
||||
|
||||
/// Whether to remove taproot specific fields from the PSBT on finalization.
|
||||
///
|
||||
/// For inputs this includes the taproot internal key, merkle root, and individual
|
||||
/// scripts and signatures. For both inputs and outputs it includes key origin info.
|
||||
///
|
||||
/// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
|
||||
///
|
||||
/// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
|
||||
pub remove_taproot_extras: bool,
|
||||
|
||||
/// Whether to try finalizing the PSBT after the inputs are signed.
|
||||
///
|
||||
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
||||
@@ -860,8 +820,6 @@ impl Default for SignOptions {
|
||||
trust_witness_utxo: false,
|
||||
assume_height: None,
|
||||
allow_all_sighashes: false,
|
||||
remove_partial_sigs: true,
|
||||
remove_taproot_extras: true,
|
||||
try_finalize: true,
|
||||
tap_leaves_options: TapLeavesOptions::default(),
|
||||
sign_with_tap_internal_key: true,
|
||||
@@ -870,198 +828,53 @@ impl Default for SignOptions {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait ComputeSighash {
|
||||
type Extra;
|
||||
type Sighash;
|
||||
type SighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &Psbt,
|
||||
input_index: usize,
|
||||
extra: Self::Extra,
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
|
||||
}
|
||||
|
||||
impl ComputeSighash for Legacy {
|
||||
type Extra = ();
|
||||
type Sighash = sighash::LegacySighash;
|
||||
type SighashType = EcdsaSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &Psbt,
|
||||
input_index: usize,
|
||||
_extra: (),
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
|
||||
let psbt_input = &psbt.inputs[input_index];
|
||||
let tx_input = &psbt.unsigned_tx.input[input_index];
|
||||
|
||||
let sighash = psbt_input
|
||||
.sighash_type
|
||||
.unwrap_or_else(|| EcdsaSighashType::All.into())
|
||||
.ecdsa_hash_ty()
|
||||
.map_err(|_| SignerError::InvalidSighash)?;
|
||||
let script = match psbt_input.redeem_script {
|
||||
Some(ref redeem_script) => redeem_script.clone(),
|
||||
None => {
|
||||
let non_witness_utxo = psbt_input
|
||||
.non_witness_utxo
|
||||
.as_ref()
|
||||
.ok_or(SignerError::MissingNonWitnessUtxo)?;
|
||||
let prev_out = non_witness_utxo
|
||||
.output
|
||||
.get(tx_input.previous_output.vout as usize)
|
||||
.ok_or(SignerError::InvalidNonWitnessUtxo)?;
|
||||
|
||||
prev_out.script_pubkey.clone()
|
||||
}
|
||||
};
|
||||
|
||||
Ok((
|
||||
sighash::SighashCache::new(&psbt.unsigned_tx).legacy_signature_hash(
|
||||
input_index,
|
||||
&script,
|
||||
sighash.to_u32(),
|
||||
)?,
|
||||
sighash,
|
||||
))
|
||||
/// Computes the taproot sighash.
|
||||
fn compute_tap_sighash(
|
||||
psbt: &Psbt,
|
||||
input_index: usize,
|
||||
extra: Option<taproot::TapLeafHash>,
|
||||
) -> Result<(sighash::TapSighash, TapSighashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
}
|
||||
|
||||
impl ComputeSighash for Segwitv0 {
|
||||
type Extra = ();
|
||||
type Sighash = sighash::SegwitV0Sighash;
|
||||
type SighashType = EcdsaSighashType;
|
||||
let psbt_input = &psbt.inputs[input_index];
|
||||
|
||||
fn sighash(
|
||||
psbt: &Psbt,
|
||||
input_index: usize,
|
||||
_extra: (),
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
let sighash_type = psbt_input
|
||||
.sighash_type
|
||||
.unwrap_or_else(|| TapSighashType::Default.into())
|
||||
.taproot_hash_ty()
|
||||
.map_err(|_| SignerError::InvalidSighash)?;
|
||||
let witness_utxos = (0..psbt.inputs.len())
|
||||
.map(|i| psbt.get_utxo_for(i))
|
||||
.collect::<Vec<_>>();
|
||||
let mut all_witness_utxos = vec![];
|
||||
|
||||
let psbt_input = &psbt.inputs[input_index];
|
||||
let tx_input = &psbt.unsigned_tx.input[input_index];
|
||||
let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||
let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0;
|
||||
let prevouts = if is_anyone_can_pay {
|
||||
sighash::Prevouts::One(
|
||||
input_index,
|
||||
witness_utxos[input_index]
|
||||
.as_ref()
|
||||
.ok_or(SignerError::MissingWitnessUtxo)?,
|
||||
)
|
||||
} else if witness_utxos.iter().all(Option::is_some) {
|
||||
all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref()));
|
||||
sighash::Prevouts::All(&all_witness_utxos)
|
||||
} else {
|
||||
return Err(SignerError::MissingWitnessUtxo);
|
||||
};
|
||||
|
||||
let sighash_type = psbt_input
|
||||
.sighash_type
|
||||
.unwrap_or_else(|| EcdsaSighashType::All.into())
|
||||
.ecdsa_hash_ty()
|
||||
.map_err(|_| SignerError::InvalidSighash)?;
|
||||
// Assume no OP_CODESEPARATOR
|
||||
let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF));
|
||||
|
||||
// Always try first with the non-witness utxo
|
||||
let utxo = if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
// Check the provided prev-tx
|
||||
if prev_tx.compute_txid() != tx_input.previous_output.txid {
|
||||
return Err(SignerError::InvalidNonWitnessUtxo);
|
||||
}
|
||||
|
||||
// The output should be present, if it's missing the `non_witness_utxo` is invalid
|
||||
prev_tx
|
||||
.output
|
||||
.get(tx_input.previous_output.vout as usize)
|
||||
.ok_or(SignerError::InvalidNonWitnessUtxo)?
|
||||
} else if let Some(witness_utxo) = &psbt_input.witness_utxo {
|
||||
// Fallback to the witness_utxo. If we aren't allowed to use it, signing should fail
|
||||
// before we get to this point
|
||||
witness_utxo
|
||||
} else {
|
||||
// Nothing has been provided
|
||||
return Err(SignerError::MissingNonWitnessUtxo);
|
||||
};
|
||||
let value = utxo.value;
|
||||
|
||||
let mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||
|
||||
let sighash = match psbt_input.witness_script {
|
||||
Some(ref witness_script) => {
|
||||
sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)?
|
||||
}
|
||||
None => {
|
||||
if utxo.script_pubkey.is_p2wpkh() {
|
||||
sighasher.p2wpkh_signature_hash(
|
||||
input_index,
|
||||
&utxo.script_pubkey,
|
||||
value,
|
||||
sighash_type,
|
||||
)?
|
||||
} else if psbt_input
|
||||
.redeem_script
|
||||
.as_ref()
|
||||
.map(|s| s.is_p2wpkh())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
let script_pubkey = psbt_input.redeem_script.as_ref().unwrap();
|
||||
sighasher.p2wpkh_signature_hash(
|
||||
input_index,
|
||||
script_pubkey,
|
||||
value,
|
||||
sighash_type,
|
||||
)?
|
||||
} else {
|
||||
return Err(SignerError::MissingWitnessScript);
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok((sighash, sighash_type))
|
||||
}
|
||||
}
|
||||
|
||||
impl ComputeSighash for Tap {
|
||||
type Extra = Option<taproot::TapLeafHash>;
|
||||
type Sighash = TapSighash;
|
||||
type SighashType = TapSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &Psbt,
|
||||
input_index: usize,
|
||||
extra: Self::Extra,
|
||||
) -> Result<(Self::Sighash, TapSighashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
|
||||
let psbt_input = &psbt.inputs[input_index];
|
||||
|
||||
let sighash_type = psbt_input
|
||||
.sighash_type
|
||||
.unwrap_or_else(|| TapSighashType::Default.into())
|
||||
.taproot_hash_ty()
|
||||
.map_err(|_| SignerError::InvalidSighash)?;
|
||||
let witness_utxos = (0..psbt.inputs.len())
|
||||
.map(|i| psbt.get_utxo_for(i))
|
||||
.collect::<Vec<_>>();
|
||||
let mut all_witness_utxos = vec![];
|
||||
|
||||
let mut cache = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||
let is_anyone_can_pay = psbt::PsbtSighashType::from(sighash_type).to_u32() & 0x80 != 0;
|
||||
let prevouts = if is_anyone_can_pay {
|
||||
sighash::Prevouts::One(
|
||||
input_index,
|
||||
witness_utxos[input_index]
|
||||
.as_ref()
|
||||
.ok_or(SignerError::MissingWitnessUtxo)?,
|
||||
)
|
||||
} else if witness_utxos.iter().all(Option::is_some) {
|
||||
all_witness_utxos.extend(witness_utxos.iter().filter_map(|x| x.as_ref()));
|
||||
sighash::Prevouts::All(&all_witness_utxos)
|
||||
} else {
|
||||
return Err(SignerError::MissingWitnessUtxo);
|
||||
};
|
||||
|
||||
// Assume no OP_CODESEPARATOR
|
||||
let extra = extra.map(|leaf_hash| (leaf_hash, 0xFFFFFFFF));
|
||||
|
||||
Ok((
|
||||
cache.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)?,
|
||||
sighash_type,
|
||||
))
|
||||
}
|
||||
Ok((
|
||||
cache
|
||||
.taproot_signature_hash(input_index, &prevouts, None, extra, sighash_type)
|
||||
.map_err(SignerError::SighashTaproot)?,
|
||||
sighash_type,
|
||||
))
|
||||
}
|
||||
|
||||
impl PartialOrd for SignersContainerKey {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk_wallet::*;
|
||||
//! # use bdk_wallet::wallet::ChangeSet;
|
||||
//! # use bdk_wallet::wallet::error::CreateTxError;
|
||||
//! # use bdk_wallet::ChangeSet;
|
||||
//! # use bdk_wallet::error::CreateTxError;
|
||||
//! # use anyhow::Error;
|
||||
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
//! # let mut wallet = doctest_wallet!();
|
||||
@@ -42,11 +42,18 @@ use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
||||
use core::cell::RefCell;
|
||||
use core::fmt;
|
||||
|
||||
use alloc::sync::Arc;
|
||||
|
||||
use bitcoin::psbt::{self, Psbt};
|
||||
use bitcoin::script::PushBytes;
|
||||
use bitcoin::{absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
use bitcoin::{
|
||||
absolute, Amount, FeeRate, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid,
|
||||
Weight,
|
||||
};
|
||||
use rand_core::RngCore;
|
||||
|
||||
use super::coin_selection::CoinSelectionAlgorithm;
|
||||
use super::utils::shuffle_slice;
|
||||
use super::{CreateTxError, Wallet};
|
||||
use crate::collections::{BTreeMap, HashSet};
|
||||
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
@@ -62,11 +69,11 @@ use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::tx_builder::*;
|
||||
/// # use bdk_wallet::tx_builder::*;
|
||||
/// # use bitcoin::*;
|
||||
/// # use core::str::FromStr;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use bdk_wallet::ChangeSet;
|
||||
/// # use bdk_wallet::error::CreateTxError;
|
||||
/// # use anyhow::Error;
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
@@ -292,10 +299,8 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
for utxo in utxos {
|
||||
let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
|
||||
|
||||
let satisfaction_weight =
|
||||
descriptor.max_weight_to_satisfy().unwrap().to_wu() as usize;
|
||||
let descriptor = wallet.public_descriptor(utxo.keychain);
|
||||
let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap();
|
||||
self.params.utxos.push(WeightedUtxo {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Local(utxo),
|
||||
@@ -364,7 +369,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
satisfaction_weight: Weight,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
self.add_foreign_utxo_with_sequence(
|
||||
outpoint,
|
||||
@@ -379,7 +384,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
satisfaction_weight: Weight,
|
||||
sequence: Sequence,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
if psbt_input.witness_utxo.is_none() {
|
||||
@@ -570,7 +575,7 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
///
|
||||
/// This will be used to:
|
||||
/// 1. Set the nLockTime for preventing fee sniping.
|
||||
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
|
||||
/// **Note**: This will be ignored if you manually specify a nlocktime using [`TxBuilder::nlocktime`].
|
||||
/// 2. Decide whether coinbase outputs are mature or not. If the coinbase outputs are not
|
||||
/// mature at `current_height`, we ignore them in the coin selection.
|
||||
/// If you want to create a transaction that spends immature coinbase inputs, manually
|
||||
@@ -636,8 +641,8 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk_wallet::*;
|
||||
/// # use bdk_wallet::wallet::ChangeSet;
|
||||
/// # use bdk_wallet::wallet::error::CreateTxError;
|
||||
/// # use bdk_wallet::ChangeSet;
|
||||
/// # use bdk_wallet::error::CreateTxError;
|
||||
/// # use anyhow::Error;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
@@ -669,16 +674,33 @@ impl<'a, Cs> TxBuilder<'a, Cs> {
|
||||
impl<'a, Cs: CoinSelectionAlgorithm> TxBuilder<'a, Cs> {
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Uses the thread-local random number generator (rng).
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
///
|
||||
/// **WARNING**: To avoid change address reuse you must persist the changes resulting from one
|
||||
/// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`].
|
||||
#[cfg(feature = "std")]
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError> {
|
||||
self.finish_with_aux_rand(&mut bitcoin::key::rand::thread_rng())
|
||||
}
|
||||
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Uses a provided random number generator (rng).
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
///
|
||||
/// **WARNING**: To avoid change address reuse you must persist the changes resulting from one
|
||||
/// or more calls to this method before closing the wallet. See [`Wallet::reveal_next_address`].
|
||||
pub fn finish_with_aux_rand(self, rng: &mut impl RngCore) -> Result<Psbt, CreateTxError> {
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
.create_tx(self.coin_selection, self.params, rng)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -744,35 +766,60 @@ impl fmt::Display for AddForeignUtxoError {
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AddForeignUtxoError {}
|
||||
|
||||
type TxSort<T> = dyn Fn(&T, &T) -> core::cmp::Ordering;
|
||||
|
||||
/// Ordering of the transaction's inputs and outputs
|
||||
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
#[derive(Clone, Default)]
|
||||
pub enum TxOrdering {
|
||||
/// Randomized (default)
|
||||
#[default]
|
||||
Shuffle,
|
||||
/// Unchanged
|
||||
Untouched,
|
||||
/// BIP69 / Lexicographic
|
||||
Bip69Lexicographic,
|
||||
/// Provide custom comparison functions for sorting
|
||||
Custom {
|
||||
/// Transaction inputs sort function
|
||||
input_sort: Arc<TxSort<TxIn>>,
|
||||
/// Transaction outputs sort function
|
||||
output_sort: Arc<TxSort<TxOut>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for TxOrdering {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
|
||||
match self {
|
||||
TxOrdering::Shuffle => write!(f, "Shuffle"),
|
||||
TxOrdering::Untouched => write!(f, "Untouched"),
|
||||
TxOrdering::Custom { .. } => write!(f, "Custom"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TxOrdering {
|
||||
/// Sort transaction inputs and outputs by [`TxOrdering`] variant
|
||||
/// Sort transaction inputs and outputs by [`TxOrdering`] variant.
|
||||
///
|
||||
/// Uses the thread-local random number generator (rng).
|
||||
#[cfg(feature = "std")]
|
||||
pub fn sort_tx(&self, tx: &mut Transaction) {
|
||||
self.sort_tx_with_aux_rand(tx, &mut bitcoin::key::rand::thread_rng())
|
||||
}
|
||||
|
||||
/// Sort transaction inputs and outputs by [`TxOrdering`] variant.
|
||||
///
|
||||
/// Uses a provided random number generator (rng).
|
||||
pub fn sort_tx_with_aux_rand(&self, tx: &mut Transaction, rng: &mut impl RngCore) {
|
||||
match self {
|
||||
TxOrdering::Untouched => {}
|
||||
TxOrdering::Shuffle => {
|
||||
use rand::seq::SliceRandom;
|
||||
let mut rng = rand::thread_rng();
|
||||
tx.input.shuffle(&mut rng);
|
||||
tx.output.shuffle(&mut rng);
|
||||
shuffle_slice(&mut tx.input, rng);
|
||||
shuffle_slice(&mut tx.output, rng);
|
||||
}
|
||||
TxOrdering::Bip69Lexicographic => {
|
||||
tx.input.sort_unstable_by_key(|txin| {
|
||||
(txin.previous_output.txid, txin.previous_output.vout)
|
||||
});
|
||||
tx.output
|
||||
.sort_unstable_by_key(|txout| (txout.value, txout.script_pubkey.clone()));
|
||||
TxOrdering::Custom {
|
||||
input_sort,
|
||||
output_sort,
|
||||
} => {
|
||||
tx.input.sort_unstable_by(|a, b| input_sort(a, b));
|
||||
tx.output.sort_unstable_by(|a, b| output_sort(a, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -851,12 +898,6 @@ mod test {
|
||||
use bitcoin::TxOut;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_output_ordering_default_shuffle() {
|
||||
assert_eq!(TxOrdering::default(), TxOrdering::Shuffle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_ordering_untouched() {
|
||||
let original_tx = ordering_test_tx!();
|
||||
@@ -889,13 +930,28 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_ordering_bip69() {
|
||||
fn test_output_ordering_custom_but_bip69() {
|
||||
use core::str::FromStr;
|
||||
|
||||
let original_tx = ordering_test_tx!();
|
||||
let mut tx = original_tx;
|
||||
|
||||
TxOrdering::Bip69Lexicographic.sort_tx(&mut tx);
|
||||
let bip69_txin_cmp = |tx_a: &TxIn, tx_b: &TxIn| {
|
||||
let project_outpoint = |t: &TxIn| (t.previous_output.txid, t.previous_output.vout);
|
||||
project_outpoint(tx_a).cmp(&project_outpoint(tx_b))
|
||||
};
|
||||
|
||||
let bip69_txout_cmp = |tx_a: &TxOut, tx_b: &TxOut| {
|
||||
let project_utxo = |t: &TxOut| (t.value, t.script_pubkey.clone());
|
||||
project_utxo(tx_a).cmp(&project_utxo(tx_b))
|
||||
};
|
||||
|
||||
let custom_bip69_ordering = TxOrdering::Custom {
|
||||
input_sort: Arc::new(bip69_txin_cmp),
|
||||
output_sort: Arc::new(bip69_txout_cmp),
|
||||
};
|
||||
|
||||
custom_bip69_ordering.sort_tx(&mut tx);
|
||||
|
||||
assert_eq!(
|
||||
tx.input[0].previous_output,
|
||||
@@ -927,6 +983,63 @@ mod test {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_ordering_custom_with_sha256() {
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
|
||||
let original_tx = ordering_test_tx!();
|
||||
let mut tx_1 = original_tx.clone();
|
||||
let mut tx_2 = original_tx.clone();
|
||||
let shared_secret = "secret_tweak";
|
||||
|
||||
let hash_txin_with_shared_secret_seed = Arc::new(|tx_a: &TxIn, tx_b: &TxIn| {
|
||||
let secret_digest_from_txin = |txin: &TxIn| {
|
||||
sha256::Hash::hash(
|
||||
&[
|
||||
&txin.previous_output.txid.to_raw_hash()[..],
|
||||
&txin.previous_output.vout.to_be_bytes(),
|
||||
shared_secret.as_bytes(),
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
};
|
||||
secret_digest_from_txin(tx_a).cmp(&secret_digest_from_txin(tx_b))
|
||||
});
|
||||
|
||||
let hash_txout_with_shared_secret_seed = Arc::new(|tx_a: &TxOut, tx_b: &TxOut| {
|
||||
let secret_digest_from_txout = |txin: &TxOut| {
|
||||
sha256::Hash::hash(
|
||||
&[
|
||||
&txin.value.to_sat().to_be_bytes(),
|
||||
&txin.script_pubkey.clone().into_bytes()[..],
|
||||
shared_secret.as_bytes(),
|
||||
]
|
||||
.concat(),
|
||||
)
|
||||
};
|
||||
secret_digest_from_txout(tx_a).cmp(&secret_digest_from_txout(tx_b))
|
||||
});
|
||||
|
||||
let custom_ordering_from_salted_sha256_1 = TxOrdering::Custom {
|
||||
input_sort: hash_txin_with_shared_secret_seed.clone(),
|
||||
output_sort: hash_txout_with_shared_secret_seed.clone(),
|
||||
};
|
||||
|
||||
let custom_ordering_from_salted_sha256_2 = TxOrdering::Custom {
|
||||
input_sort: hash_txin_with_shared_secret_seed,
|
||||
output_sort: hash_txout_with_shared_secret_seed,
|
||||
};
|
||||
|
||||
custom_ordering_from_salted_sha256_1.sort_tx(&mut tx_1);
|
||||
custom_ordering_from_salted_sha256_2.sort_tx(&mut tx_2);
|
||||
|
||||
// Check the ordering is consistent between calls
|
||||
assert_eq!(tx_1, tx_2);
|
||||
// Check transaction order has changed
|
||||
assert_ne!(tx_1, original_tx);
|
||||
assert_ne!(tx_2, original_tx);
|
||||
}
|
||||
|
||||
fn get_test_utxos() -> Vec<LocalOutput> {
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ use bitcoin::{absolute, relative, Script, Sequence};
|
||||
|
||||
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
||||
|
||||
use rand_core::RngCore;
|
||||
|
||||
/// Trait to check if a value is below the dust limit.
|
||||
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
|
||||
/// keep it compatible with network dust rate
|
||||
@@ -110,6 +112,19 @@ impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
|
||||
}
|
||||
}
|
||||
|
||||
// The Knuth shuffling algorithm based on the original [Fisher-Yates method](https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle)
|
||||
pub(crate) fn shuffle_slice<T>(list: &mut [T], rng: &mut impl RngCore) {
|
||||
if list.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut current_index = list.len() - 1;
|
||||
while current_index > 0 {
|
||||
let random_index = rng.next_u32() as usize % (current_index + 1);
|
||||
list.swap(current_index, random_index);
|
||||
current_index -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type SecpCtx = Secp256k1<All>;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -118,9 +133,11 @@ mod test {
|
||||
// otherwise it's time-based
|
||||
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
||||
|
||||
use super::{check_nsequence_rbf, IsDust};
|
||||
use super::{check_nsequence_rbf, shuffle_slice, IsDust};
|
||||
use crate::bitcoin::{Address, Network, Sequence};
|
||||
use alloc::vec::Vec;
|
||||
use core::str::FromStr;
|
||||
use rand::{rngs::StdRng, thread_rng, SeedableRng};
|
||||
|
||||
#[test]
|
||||
fn test_is_dust() {
|
||||
@@ -182,4 +199,46 @@ mod test {
|
||||
);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_shuffle_slice_empty_vec() {
|
||||
let mut test: Vec<u8> = vec![];
|
||||
shuffle_slice(&mut test, &mut thread_rng());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_shuffle_slice_single_vec() {
|
||||
let mut test: Vec<u8> = vec![0];
|
||||
shuffle_slice(&mut test, &mut thread_rng());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shuffle_slice_duple_vec() {
|
||||
let seed = [0; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
let mut test: Vec<u8> = vec![0, 1];
|
||||
shuffle_slice(&mut test, &mut rng);
|
||||
assert_eq!(test, &[0, 1]);
|
||||
let seed = [6; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
let mut test: Vec<u8> = vec![0, 1];
|
||||
shuffle_slice(&mut test, &mut rng);
|
||||
assert_eq!(test, &[1, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shuffle_slice_multi_vec() {
|
||||
let seed = [0; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
let mut test: Vec<u8> = vec![0, 1, 2, 4, 5];
|
||||
shuffle_slice(&mut test, &mut rng);
|
||||
assert_eq!(test, &[2, 1, 0, 4, 5]);
|
||||
let seed = [25; 32];
|
||||
let mut rng: StdRng = SeedableRng::from_seed(seed);
|
||||
let mut test: Vec<u8> = vec![0, 1, 2, 4, 5];
|
||||
shuffle_slice(&mut test, &mut rng);
|
||||
assert_eq!(test, &[0, 4, 1, 2, 5]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bdk_wallet::{KeychainKind, LocalOutput, Wallet};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bdk_chain::{BlockId, ConfirmationBlockTime, ConfirmationTime, TxGraph};
|
||||
use bdk_wallet::{CreateParams, KeychainKind, LocalOutput, Update, Wallet};
|
||||
use bitcoin::{
|
||||
transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction, TxIn, TxOut,
|
||||
Txid,
|
||||
hashes::Hash, transaction, Address, Amount, BlockHash, FeeRate, Network, OutPoint, Transaction,
|
||||
TxIn, TxOut, Txid,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -16,7 +13,11 @@ use std::str::FromStr;
|
||||
/// to a foreign address and one returning 50_000 back to the wallet. The remaining 1000
|
||||
/// sats are the transaction fee.
|
||||
pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet, bitcoin::Txid) {
|
||||
let mut wallet = Wallet::new(descriptor, change, Network::Regtest).unwrap();
|
||||
let mut wallet = Wallet::create(descriptor.to_string(), change.to_string())
|
||||
.network(Network::Regtest)
|
||||
.create_wallet_no_persist()
|
||||
.expect("descriptors must be valid");
|
||||
|
||||
let receive_address = wallet.peek_address(KeychainKind::External, 0).address;
|
||||
let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
|
||||
.expect("address")
|
||||
@@ -65,6 +66,12 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
|
||||
],
|
||||
};
|
||||
|
||||
wallet
|
||||
.insert_checkpoint(BlockId {
|
||||
height: 42,
|
||||
hash: BlockHash::all_zeros(),
|
||||
})
|
||||
.unwrap();
|
||||
wallet
|
||||
.insert_checkpoint(BlockId {
|
||||
height: 1_000,
|
||||
@@ -77,24 +84,26 @@ pub fn get_funded_wallet_with_change(descriptor: &str, change: &str) -> (Wallet,
|
||||
hash: BlockHash::all_zeros(),
|
||||
})
|
||||
.unwrap();
|
||||
wallet
|
||||
.insert_tx(
|
||||
tx0,
|
||||
ConfirmationTime::Confirmed {
|
||||
height: 1_000,
|
||||
time: 100,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
wallet
|
||||
.insert_tx(
|
||||
tx1.clone(),
|
||||
ConfirmationTime::Confirmed {
|
||||
height: 2_000,
|
||||
time: 200,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
wallet.insert_tx(tx0.clone());
|
||||
insert_anchor_from_conf(
|
||||
&mut wallet,
|
||||
tx0.compute_txid(),
|
||||
ConfirmationTime::Confirmed {
|
||||
height: 1_000,
|
||||
time: 100,
|
||||
},
|
||||
);
|
||||
|
||||
wallet.insert_tx(tx1.clone());
|
||||
insert_anchor_from_conf(
|
||||
&mut wallet,
|
||||
tx1.compute_txid(),
|
||||
ConfirmationTime::Confirmed {
|
||||
height: 2_000,
|
||||
time: 200,
|
||||
},
|
||||
);
|
||||
|
||||
(wallet, tx1.compute_txid())
|
||||
}
|
||||
@@ -192,3 +201,30 @@ pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
|
||||
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
|
||||
FeeRate::from_sat_per_kwu(sat_kwu)
|
||||
}
|
||||
|
||||
/// Simulates confirming a tx with `txid` at the specified `position` by inserting an anchor
|
||||
/// at the lowest height in local chain that is greater or equal to `position`'s height,
|
||||
/// assuming the confirmation time matches `ConfirmationTime::Confirmed`.
|
||||
pub fn insert_anchor_from_conf(wallet: &mut Wallet, txid: Txid, position: ConfirmationTime) {
|
||||
if let ConfirmationTime::Confirmed { height, time } = position {
|
||||
// anchor tx to checkpoint with lowest height that is >= position's height
|
||||
let anchor = wallet
|
||||
.local_chain()
|
||||
.range(height..)
|
||||
.last()
|
||||
.map(|anchor_cp| ConfirmationBlockTime {
|
||||
block_id: anchor_cp.block_id(),
|
||||
confirmation_time: time,
|
||||
})
|
||||
.expect("confirmation height cannot be greater than tip");
|
||||
|
||||
let mut graph = TxGraph::default();
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
wallet
|
||||
.apply_update(Update {
|
||||
graph,
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -13,9 +13,10 @@ use bdk_bitcoind_rpc::{
|
||||
};
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Block, Transaction},
|
||||
indexed_tx_graph, keychain,
|
||||
indexed_tx_graph,
|
||||
indexer::keychain_txout,
|
||||
local_chain::{self, LocalChain},
|
||||
Append, ConfirmationTimeHeightAnchor, IndexedTxGraph,
|
||||
ConfirmationBlockTime, IndexedTxGraph, Merge,
|
||||
};
|
||||
use example_cli::{
|
||||
anyhow,
|
||||
@@ -37,7 +38,7 @@ const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
|
||||
|
||||
type ChangeSet = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain::ChangeSet<Keychain>>,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
|
||||
);
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -190,7 +191,7 @@ fn main() -> anyhow::Result<()> {
|
||||
.apply_update(emission.checkpoint)
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
|
||||
db_stage.append((chain_changeset, graph_changeset));
|
||||
db_stage.merge((chain_changeset, graph_changeset));
|
||||
|
||||
// commit staged db changes in intervals
|
||||
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
|
||||
@@ -234,7 +235,7 @@ fn main() -> anyhow::Result<()> {
|
||||
);
|
||||
{
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db_stage.append((local_chain::ChangeSet::default(), graph_changeset));
|
||||
db_stage.merge((local_chain::ChangeSet::default(), graph_changeset));
|
||||
if let Some(changeset) = db_stage.take() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
@@ -320,7 +321,7 @@ fn main() -> anyhow::Result<()> {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
db_stage.append(changeset);
|
||||
db_stage.merge(changeset);
|
||||
|
||||
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
|
||||
let db = &mut *db.lock().unwrap();
|
||||
|
||||
@@ -14,13 +14,13 @@ use bdk_chain::{
|
||||
transaction, Address, Amount, Network, Sequence, Transaction, TxIn, TxOut,
|
||||
},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain::{self, KeychainTxOutIndex},
|
||||
indexer::keychain_txout::{self, KeychainTxOutIndex},
|
||||
local_chain,
|
||||
miniscript::{
|
||||
descriptor::{DescriptorSecretKey, KeyMap},
|
||||
Descriptor, DescriptorPublicKey,
|
||||
},
|
||||
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut,
|
||||
Anchor, ChainOracle, DescriptorExt, FullTxOut, Merge,
|
||||
};
|
||||
pub use bdk_file_store;
|
||||
pub use clap;
|
||||
@@ -30,7 +30,7 @@ use clap::{Parser, Subcommand};
|
||||
pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
|
||||
pub type KeychainChangeSet<A> = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>>,
|
||||
indexed_tx_graph::ChangeSet<A, keychain_txout::ChangeSet>,
|
||||
);
|
||||
|
||||
#[derive(Parser)]
|
||||
@@ -191,7 +191,7 @@ impl core::fmt::Display for Keychain {
|
||||
}
|
||||
|
||||
pub struct CreateTxChange {
|
||||
pub index_changeset: keychain::ChangeSet<Keychain>,
|
||||
pub index_changeset: keychain_txout::ChangeSet,
|
||||
pub change_keychain: Keychain,
|
||||
pub index: u32,
|
||||
}
|
||||
@@ -207,7 +207,7 @@ pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let mut changeset = keychain::ChangeSet::default();
|
||||
let mut changeset = keychain_txout::ChangeSet::default();
|
||||
|
||||
let assets = bdk_tmp_plan::Assets {
|
||||
keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(),
|
||||
@@ -252,7 +252,7 @@ where
|
||||
let internal_keychain = if graph
|
||||
.index
|
||||
.keychains()
|
||||
.any(|(k, _)| *k == Keychain::Internal)
|
||||
.any(|(k, _)| k == Keychain::Internal)
|
||||
{
|
||||
Keychain::Internal
|
||||
} else {
|
||||
@@ -261,15 +261,15 @@ where
|
||||
|
||||
let ((change_index, change_script), change_changeset) = graph
|
||||
.index
|
||||
.next_unused_spk(&internal_keychain)
|
||||
.next_unused_spk(internal_keychain)
|
||||
.expect("Must exist");
|
||||
changeset.append(change_changeset);
|
||||
changeset.merge(change_changeset);
|
||||
|
||||
let change_plan = bdk_tmp_plan::plan_satisfaction(
|
||||
&graph
|
||||
.index
|
||||
.keychains()
|
||||
.find(|(k, _)| *k == &internal_keychain)
|
||||
.find(|(k, _)| *k == internal_keychain)
|
||||
.expect("must exist")
|
||||
.1
|
||||
.at_derivation_index(change_index)
|
||||
@@ -288,7 +288,7 @@ where
|
||||
min_drain_value: graph
|
||||
.index
|
||||
.keychains()
|
||||
.find(|(k, _)| *k == &internal_keychain)
|
||||
.find(|(k, _)| *k == internal_keychain)
|
||||
.expect("must exist")
|
||||
.1
|
||||
.dust_value(),
|
||||
@@ -433,7 +433,7 @@ pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDeri
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.find(|(keychain, _)| *keychain == &k)
|
||||
.find(|(keychain, _)| *keychain == k)
|
||||
.expect("keychain must exist")
|
||||
.1
|
||||
.at_derivation_index(i)
|
||||
@@ -456,7 +456,7 @@ pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainO
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
C: Default
|
||||
+ Append
|
||||
+ Merge
|
||||
+ DeserializeOwned
|
||||
+ Serialize
|
||||
+ From<KeychainChangeSet<A>>
|
||||
@@ -479,7 +479,7 @@ where
|
||||
};
|
||||
|
||||
let ((spk_i, spk), index_changeset) =
|
||||
spk_chooser(index, &Keychain::External).expect("Must exist");
|
||||
spk_chooser(index, Keychain::External).expect("Must exist");
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.append_changeset(&C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
@@ -501,8 +501,8 @@ where
|
||||
true => Keychain::Internal,
|
||||
false => Keychain::External,
|
||||
};
|
||||
for (spk_i, spk) in index.revealed_keychain_spks(&target_keychain) {
|
||||
let address = Address::from_script(spk, network)
|
||||
for (spk_i, spk) in index.revealed_keychain_spks(target_keychain) {
|
||||
let address = Address::from_script(spk.as_script(), network)
|
||||
.expect("should always be able to derive address");
|
||||
println!(
|
||||
"{:?} {} used:{}",
|
||||
@@ -675,7 +675,7 @@ where
|
||||
/// The initial state returned by [`init`].
|
||||
pub struct Init<CS: clap::Subcommand, S: clap::Args, C>
|
||||
where
|
||||
C: Default + Append + Serialize + DeserializeOwned + Debug + Send + Sync + 'static,
|
||||
C: Default + Merge + Serialize + DeserializeOwned + Debug + Send + Sync + 'static,
|
||||
{
|
||||
/// Arguments parsed by the cli.
|
||||
pub args: Args<CS, S>,
|
||||
@@ -697,7 +697,7 @@ pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
|
||||
) -> anyhow::Result<Init<CS, S, C>>
|
||||
where
|
||||
C: Default
|
||||
+ Append
|
||||
+ Merge
|
||||
+ Serialize
|
||||
+ DeserializeOwned
|
||||
+ Debug
|
||||
|
||||
@@ -7,10 +7,10 @@ use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Address, Network, Txid},
|
||||
collections::BTreeSet,
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
indexer::keychain_txout,
|
||||
local_chain::{self, LocalChain},
|
||||
spk_client::{FullScanRequest, SyncRequest},
|
||||
Append, ConfirmationHeightAnchor,
|
||||
ConfirmationBlockTime, Merge,
|
||||
};
|
||||
use bdk_electrum::{
|
||||
electrum_client::{self, Client, ElectrumApi},
|
||||
@@ -100,7 +100,7 @@ pub struct ScanOptions {
|
||||
|
||||
type ChangeSet = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationHeightAnchor, keychain::ChangeSet<Keychain>>,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
|
||||
);
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
@@ -166,7 +166,7 @@ fn main() -> anyhow::Result<()> {
|
||||
Keychain::External,
|
||||
graph
|
||||
.index
|
||||
.unbounded_spk_iter(&Keychain::External)
|
||||
.unbounded_spk_iter(Keychain::External)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
@@ -174,7 +174,7 @@ fn main() -> anyhow::Result<()> {
|
||||
Keychain::Internal,
|
||||
graph
|
||||
.index
|
||||
.unbounded_spk_iter(&Keychain::Internal)
|
||||
.unbounded_spk_iter(Keychain::Internal)
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
@@ -193,8 +193,7 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
let res = client
|
||||
.full_scan::<_>(request, stop_gap, scan_options.batch_size, false)
|
||||
.context("scanning the blockchain")?
|
||||
.with_confirmation_height_anchor();
|
||||
.context("scanning the blockchain")?;
|
||||
(
|
||||
res.chain_update,
|
||||
res.graph_update,
|
||||
@@ -277,7 +276,7 @@ fn main() -> anyhow::Result<()> {
|
||||
if unconfirmed {
|
||||
let unconfirmed_txids = graph
|
||||
.graph()
|
||||
.list_chain_txs(&*chain, chain_tip.block_id())
|
||||
.list_canonical_txs(&*chain, chain_tip.block_id())
|
||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||
.collect::<Vec<Txid>>();
|
||||
@@ -317,8 +316,7 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
let res = client
|
||||
.sync(request, scan_options.batch_size, false)
|
||||
.context("scanning the blockchain")?
|
||||
.with_confirmation_height_anchor();
|
||||
.context("scanning the blockchain")?;
|
||||
|
||||
// drop lock on graph and chain
|
||||
drop((graph, chain));
|
||||
@@ -340,12 +338,12 @@ fn main() -> anyhow::Result<()> {
|
||||
let chain_changeset = chain.apply_update(chain_update)?;
|
||||
|
||||
let mut indexed_tx_graph_changeset =
|
||||
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
|
||||
indexed_tx_graph::ChangeSet::<ConfirmationBlockTime, _>::default();
|
||||
if let Some(keychain_update) = keychain_update {
|
||||
let keychain_changeset = graph.index.reveal_to_target_multi(&keychain_update);
|
||||
indexed_tx_graph_changeset.append(keychain_changeset.into());
|
||||
indexed_tx_graph_changeset.merge(keychain_changeset.into());
|
||||
}
|
||||
indexed_tx_graph_changeset.append(graph.apply_update(graph_update));
|
||||
indexed_tx_graph_changeset.merge(graph.apply_update(graph_update));
|
||||
|
||||
(chain_changeset, indexed_tx_graph_changeset)
|
||||
};
|
||||
|
||||
@@ -7,10 +7,10 @@ use std::{
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Address, Network, Txid},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
indexer::keychain_txout,
|
||||
local_chain::{self, LocalChain},
|
||||
spk_client::{FullScanRequest, SyncRequest},
|
||||
Append, ConfirmationTimeHeightAnchor,
|
||||
ConfirmationBlockTime, Merge,
|
||||
};
|
||||
|
||||
use bdk_esplora::{esplora_client, EsploraExt};
|
||||
@@ -22,11 +22,11 @@ use example_cli::{
|
||||
};
|
||||
|
||||
const DB_MAGIC: &[u8] = b"bdk_example_esplora";
|
||||
const DB_PATH: &str = ".bdk_esplora_example.db";
|
||||
const DB_PATH: &str = "bdk_example_esplora.db";
|
||||
|
||||
type ChangeSet = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain::ChangeSet<Keychain>>,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationBlockTime, keychain_txout::ChangeSet>,
|
||||
);
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
@@ -84,7 +84,7 @@ impl EsploraArgs {
|
||||
Network::Bitcoin => "https://blockstream.info/api",
|
||||
Network::Testnet => "https://blockstream.info/testnet/api",
|
||||
Network::Regtest => "http://localhost:3002",
|
||||
Network::Signet => "https://mempool.space/signet/api",
|
||||
Network::Signet => "http://signet.bitcoindevkit.net",
|
||||
_ => panic!("unsupported network"),
|
||||
});
|
||||
|
||||
@@ -96,7 +96,7 @@ impl EsploraArgs {
|
||||
#[derive(Parser, Debug, Clone, PartialEq)]
|
||||
pub struct ScanOptions {
|
||||
/// Max number of concurrent esplora server requests.
|
||||
#[clap(long, default_value = "1")]
|
||||
#[clap(long, default_value = "5")]
|
||||
pub parallel_requests: usize,
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ fn main() -> anyhow::Result<()> {
|
||||
.index
|
||||
.reveal_to_target_multi(&update.last_active_indices);
|
||||
let mut indexed_tx_graph_changeset = graph.apply_update(update.graph_update);
|
||||
indexed_tx_graph_changeset.append(index_changeset.into());
|
||||
indexed_tx_graph_changeset.merge(index_changeset.into());
|
||||
indexed_tx_graph_changeset
|
||||
})
|
||||
}
|
||||
@@ -307,7 +307,7 @@ fn main() -> anyhow::Result<()> {
|
||||
// `EsploraExt::update_tx_graph_without_keychain`.
|
||||
let unconfirmed_txids = graph
|
||||
.graph()
|
||||
.list_chain_txs(&*chain, local_tip.block_id())
|
||||
.list_canonical_txs(&*chain, local_tip.block_id())
|
||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||
.collect::<Vec<Txid>>();
|
||||
|
||||
@@ -4,7 +4,6 @@ version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk_wallet = { path = "../../crates/wallet", features = ["file_store"] }
|
||||
bdk_electrum = { path = "../../crates/electrum" }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -1,53 +1,54 @@
|
||||
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
|
||||
const STOP_GAP: usize = 50;
|
||||
const BATCH_SIZE: usize = 5;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use bdk_wallet::file_store::Store;
|
||||
use bdk_wallet::Wallet;
|
||||
use std::io::Write;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bdk_electrum::electrum_client;
|
||||
use bdk_electrum::BdkElectrumClient;
|
||||
use bdk_file_store::Store;
|
||||
use bdk_wallet::bitcoin::Network;
|
||||
use bdk_wallet::bitcoin::{Address, Amount};
|
||||
use bdk_wallet::chain::collections::HashSet;
|
||||
use bdk_wallet::{bitcoin::Network, Wallet};
|
||||
use bdk_wallet::{KeychainKind, SignOptions};
|
||||
|
||||
const DB_MAGIC: &str = "bdk_wallet_electrum_example";
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
|
||||
const STOP_GAP: usize = 50;
|
||||
const BATCH_SIZE: usize = 5;
|
||||
|
||||
const NETWORK: Network = Network::Testnet;
|
||||
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
const ELECTRUM_URL: &str = "ssl://electrum.blockstream.info:60002";
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let db_path = std::env::temp_dir().join("bdk-electrum-example");
|
||||
let mut db =
|
||||
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
let changeset = db
|
||||
.aggregate_changesets()
|
||||
.map_err(|e| anyhow!("load changes error: {}", e))?;
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
external_descriptor,
|
||||
internal_descriptor,
|
||||
changeset,
|
||||
Network::Testnet,
|
||||
)?;
|
||||
let db_path = "bdk-electrum-example.db";
|
||||
|
||||
let mut db = Store::<bdk_wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
|
||||
let wallet_opt = Wallet::load()
|
||||
.descriptors(EXTERNAL_DESC, INTERNAL_DESC)
|
||||
.network(NETWORK)
|
||||
.load_wallet(&mut db)?;
|
||||
let mut wallet = match wallet_opt {
|
||||
Some(wallet) => wallet,
|
||||
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
|
||||
.network(NETWORK)
|
||||
.create_wallet(&mut db)?,
|
||||
};
|
||||
|
||||
let address = wallet.next_unused_address(KeychainKind::External);
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
wallet.persist(&mut db)?;
|
||||
println!("Generated Address: {}", address);
|
||||
|
||||
let balance = wallet.balance();
|
||||
println!("Wallet balance before syncing: {} sats", balance.total());
|
||||
|
||||
print!("Syncing...");
|
||||
let client = BdkElectrumClient::new(electrum_client::Client::new(
|
||||
"ssl://electrum.blockstream.info:60002",
|
||||
)?);
|
||||
let client = BdkElectrumClient::new(electrum_client::Client::new(ELECTRUM_URL)?);
|
||||
|
||||
// Populate the electrum client's transaction cache so it doesn't redownload transaction we
|
||||
// already have.
|
||||
client.populate_tx_cache(&wallet);
|
||||
client.populate_tx_cache(wallet.tx_graph());
|
||||
|
||||
let request = wallet
|
||||
.start_full_scan()
|
||||
@@ -63,9 +64,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
})
|
||||
.inspect_spks_for_all_keychains(|_, _, _| std::io::stdout().flush().expect("must flush"));
|
||||
|
||||
let mut update = client
|
||||
.full_scan(request, STOP_GAP, BATCH_SIZE, false)?
|
||||
.with_confirmation_time_height_anchor(&client)?;
|
||||
let mut update = client.full_scan(request, STOP_GAP, BATCH_SIZE, false)?;
|
||||
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
@@ -73,9 +72,7 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
println!();
|
||||
|
||||
wallet.apply_update(update)?;
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
wallet.persist(&mut db)?;
|
||||
|
||||
let balance = wallet.balance();
|
||||
println!("Wallet balance after syncing: {} sats", balance.total());
|
||||
|
||||
@@ -6,8 +6,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk_wallet = { path = "../../crates/wallet", features = ["rusqlite"] }
|
||||
bdk_esplora = { path = "../../crates/esplora", features = ["async-https"] }
|
||||
bdk_sqlite = { path = "../../crates/sqlite" }
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -1,76 +1,58 @@
|
||||
use std::{collections::BTreeSet, io::Write, str::FromStr};
|
||||
use std::{collections::BTreeSet, io::Write};
|
||||
|
||||
use anyhow::Ok;
|
||||
use bdk_esplora::{esplora_client, EsploraAsyncExt};
|
||||
use bdk_wallet::{
|
||||
bitcoin::{Address, Amount, Network, Script},
|
||||
bitcoin::{Amount, Network},
|
||||
rusqlite::Connection,
|
||||
KeychainKind, SignOptions, Wallet,
|
||||
};
|
||||
|
||||
use bdk_sqlite::{rusqlite::Connection, Store};
|
||||
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
|
||||
const STOP_GAP: usize = 50;
|
||||
const STOP_GAP: usize = 5;
|
||||
const PARALLEL_REQUESTS: usize = 5;
|
||||
|
||||
const DB_PATH: &str = "bdk-example-esplora-async.sqlite";
|
||||
const NETWORK: Network = Network::Signet;
|
||||
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), anyhow::Error> {
|
||||
let db_path = "bdk-esplora-async-example.sqlite";
|
||||
let conn = Connection::open(db_path)?;
|
||||
let mut db = Store::new(conn)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
let changeset = db.read()?;
|
||||
let mut conn = Connection::open(DB_PATH)?;
|
||||
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
external_descriptor,
|
||||
internal_descriptor,
|
||||
changeset,
|
||||
Network::Signet,
|
||||
)?;
|
||||
let wallet_opt = Wallet::load()
|
||||
.descriptors(EXTERNAL_DESC, INTERNAL_DESC)
|
||||
.network(NETWORK)
|
||||
.load_wallet(&mut conn)?;
|
||||
let mut wallet = match wallet_opt {
|
||||
Some(wallet) => wallet,
|
||||
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
|
||||
.network(NETWORK)
|
||||
.create_wallet(&mut conn)?,
|
||||
};
|
||||
|
||||
let address = wallet.next_unused_address(KeychainKind::External);
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
db.write(&changeset)?;
|
||||
}
|
||||
println!("Generated Address: {}", address);
|
||||
wallet.persist(&mut conn)?;
|
||||
println!("Next unused address: ({}) {}", address.index, address);
|
||||
|
||||
let balance = wallet.balance();
|
||||
println!("Wallet balance before syncing: {} sats", balance.total());
|
||||
|
||||
print!("Syncing...");
|
||||
let client = esplora_client::Builder::new("http://signet.bitcoindevkit.net").build_async()?;
|
||||
let client = esplora_client::Builder::new(ESPLORA_URL).build_async()?;
|
||||
|
||||
fn generate_inspect(kind: KeychainKind) -> impl FnMut(u32, &Script) + Send + Sync + 'static {
|
||||
let mut once = Some(());
|
||||
let mut stdout = std::io::stdout();
|
||||
move |spk_i, _| {
|
||||
match once.take() {
|
||||
Some(_) => print!("\nScanning keychain [{:?}]", kind),
|
||||
None => print!(" {:<3}", spk_i),
|
||||
};
|
||||
stdout.flush().expect("must flush");
|
||||
}
|
||||
}
|
||||
let request = wallet
|
||||
.start_full_scan()
|
||||
.inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::<KeychainKind>::new();
|
||||
move |keychain, spk_i, _| {
|
||||
match once.insert(keychain) {
|
||||
true => print!("\nScanning keychain [{:?}]", keychain),
|
||||
false => print!(" {:<3}", spk_i),
|
||||
}
|
||||
std::io::stdout().flush().expect("must flush")
|
||||
let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::<KeychainKind>::new();
|
||||
move |keychain, spk_i, _| {
|
||||
if once.insert(keychain) {
|
||||
print!("\nScanning keychain [{:?}] ", keychain);
|
||||
}
|
||||
})
|
||||
.inspect_spks_for_keychain(
|
||||
KeychainKind::External,
|
||||
generate_inspect(KeychainKind::External),
|
||||
)
|
||||
.inspect_spks_for_keychain(
|
||||
KeychainKind::Internal,
|
||||
generate_inspect(KeychainKind::Internal),
|
||||
);
|
||||
print!(" {:<3}", spk_i);
|
||||
std::io::stdout().flush().expect("must flush")
|
||||
}
|
||||
});
|
||||
|
||||
let mut update = client
|
||||
.full_scan(request, STOP_GAP, PARALLEL_REQUESTS)
|
||||
@@ -79,9 +61,7 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
let _ = update.graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
wallet.apply_update(update)?;
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
db.write(&changeset)?;
|
||||
}
|
||||
wallet.persist(&mut conn)?;
|
||||
println!();
|
||||
|
||||
let balance = wallet.balance();
|
||||
@@ -95,12 +75,9 @@ async fn main() -> Result<(), anyhow::Error> {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
|
||||
.require_network(Network::Signet)?;
|
||||
|
||||
let mut tx_builder = wallet.build_tx();
|
||||
tx_builder
|
||||
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
|
||||
.add_recipient(address.script_pubkey(), SEND_AMOUNT)
|
||||
.enable_rbf();
|
||||
|
||||
let mut psbt = tx_builder.finish()?;
|
||||
|
||||
@@ -7,7 +7,6 @@ publish = false
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk_wallet = { path = "../../crates/wallet", features = ["file_store"] }
|
||||
bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
anyhow = "1"
|
||||
|
||||
@@ -1,52 +1,57 @@
|
||||
const DB_MAGIC: &str = "bdk_wallet_esplora_example";
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(1000);
|
||||
const STOP_GAP: usize = 5;
|
||||
const PARALLEL_REQUESTS: usize = 1;
|
||||
|
||||
use std::{collections::BTreeSet, io::Write, str::FromStr};
|
||||
use std::{collections::BTreeSet, io::Write};
|
||||
|
||||
use bdk_esplora::{esplora_client, EsploraExt};
|
||||
use bdk_file_store::Store;
|
||||
use bdk_wallet::{
|
||||
bitcoin::{Address, Amount, Network},
|
||||
bitcoin::{Amount, Network},
|
||||
file_store::Store,
|
||||
KeychainKind, SignOptions, Wallet,
|
||||
};
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let db_path = std::env::temp_dir().join("bdk-esplora-example");
|
||||
let mut db =
|
||||
Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), db_path)?;
|
||||
let external_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
let internal_descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
let changeset = db.aggregate_changesets()?;
|
||||
const DB_MAGIC: &str = "bdk_wallet_esplora_example";
|
||||
const DB_PATH: &str = "bdk-example-esplora-blocking.db";
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(5000);
|
||||
const STOP_GAP: usize = 5;
|
||||
const PARALLEL_REQUESTS: usize = 5;
|
||||
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
external_descriptor,
|
||||
internal_descriptor,
|
||||
changeset,
|
||||
Network::Testnet,
|
||||
)?;
|
||||
const NETWORK: Network = Network::Signet;
|
||||
const EXTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/0/*)";
|
||||
const INTERNAL_DESC: &str = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/1'/0'/1/*)";
|
||||
const ESPLORA_URL: &str = "http://signet.bitcoindevkit.net";
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let mut db = Store::<bdk_wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), DB_PATH)?;
|
||||
|
||||
let wallet_opt = Wallet::load()
|
||||
.descriptors(EXTERNAL_DESC, INTERNAL_DESC)
|
||||
.network(NETWORK)
|
||||
.load_wallet(&mut db)?;
|
||||
let mut wallet = match wallet_opt {
|
||||
Some(wallet) => wallet,
|
||||
None => Wallet::create(EXTERNAL_DESC, INTERNAL_DESC)
|
||||
.network(NETWORK)
|
||||
.create_wallet(&mut db)?,
|
||||
};
|
||||
|
||||
let address = wallet.next_unused_address(KeychainKind::External);
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
println!("Generated Address: {}", address);
|
||||
wallet.persist(&mut db)?;
|
||||
println!(
|
||||
"Next unused address: ({}) {}",
|
||||
address.index, address.address
|
||||
);
|
||||
|
||||
let balance = wallet.balance();
|
||||
println!("Wallet balance before syncing: {} sats", balance.total());
|
||||
|
||||
print!("Syncing...");
|
||||
let client =
|
||||
esplora_client::Builder::new("https://blockstream.info/testnet/api").build_blocking();
|
||||
let client = esplora_client::Builder::new(ESPLORA_URL).build_blocking();
|
||||
|
||||
let request = wallet.start_full_scan().inspect_spks_for_all_keychains({
|
||||
let mut once = BTreeSet::<KeychainKind>::new();
|
||||
move |keychain, spk_i, _| {
|
||||
match once.insert(keychain) {
|
||||
true => print!("\nScanning keychain [{:?}]", keychain),
|
||||
false => print!(" {:<3}", spk_i),
|
||||
};
|
||||
if once.insert(keychain) {
|
||||
print!("\nScanning keychain [{:?}] ", keychain);
|
||||
}
|
||||
print!(" {:<3}", spk_i);
|
||||
std::io::stdout().flush().expect("must flush")
|
||||
}
|
||||
});
|
||||
@@ -72,12 +77,9 @@ fn main() -> Result<(), anyhow::Error> {
|
||||
std::process::exit(0);
|
||||
}
|
||||
|
||||
let faucet_address = Address::from_str("mkHS9ne12qx9pS9VojpwU5xtRd4T7X7ZUt")?
|
||||
.require_network(Network::Testnet)?;
|
||||
|
||||
let mut tx_builder = wallet.build_tx();
|
||||
tx_builder
|
||||
.add_recipient(faucet_address.script_pubkey(), SEND_AMOUNT)
|
||||
.add_recipient(address.script_pubkey(), SEND_AMOUNT)
|
||||
.enable_rbf();
|
||||
|
||||
let mut psbt = tx_builder.finish()?;
|
||||
|
||||
@@ -6,8 +6,7 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_wallet = { path = "../../crates/wallet" }
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
bdk_wallet = { path = "../../crates/wallet", features = ["file_store"] }
|
||||
bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
|
||||
|
||||
anyhow = "1"
|
||||
|
||||
@@ -2,10 +2,10 @@ use bdk_bitcoind_rpc::{
|
||||
bitcoincore_rpc::{Auth, Client, RpcApi},
|
||||
Emitter,
|
||||
};
|
||||
use bdk_file_store::Store;
|
||||
use bdk_wallet::{
|
||||
bitcoin::{Block, Network, Transaction},
|
||||
wallet::Wallet,
|
||||
file_store::Store,
|
||||
Wallet,
|
||||
};
|
||||
use clap::{self, Parser};
|
||||
use std::{path::PathBuf, sync::mpsc::sync_channel, thread::spawn, time::Instant};
|
||||
@@ -86,18 +86,18 @@ fn main() -> anyhow::Result<()> {
|
||||
);
|
||||
|
||||
let start_load_wallet = Instant::now();
|
||||
let mut db = Store::<bdk_wallet::wallet::ChangeSet>::open_or_create_new(
|
||||
DB_MAGIC.as_bytes(),
|
||||
args.db_path,
|
||||
)?;
|
||||
let changeset = db.aggregate_changesets()?;
|
||||
|
||||
let mut wallet = Wallet::new_or_load(
|
||||
&args.descriptor,
|
||||
&args.change_descriptor,
|
||||
changeset,
|
||||
args.network,
|
||||
)?;
|
||||
let mut db =
|
||||
Store::<bdk_wallet::ChangeSet>::open_or_create_new(DB_MAGIC.as_bytes(), args.db_path)?;
|
||||
let wallet_opt = Wallet::load()
|
||||
.descriptors(args.descriptor.clone(), args.change_descriptor.clone())
|
||||
.network(args.network)
|
||||
.load_wallet(&mut db)?;
|
||||
let mut wallet = match wallet_opt {
|
||||
Some(wallet) => wallet,
|
||||
None => Wallet::create(args.descriptor, args.change_descriptor)
|
||||
.network(args.network)
|
||||
.create_wallet(&mut db)?,
|
||||
};
|
||||
println!(
|
||||
"Loaded wallet in {}s",
|
||||
start_load_wallet.elapsed().as_secs_f32()
|
||||
@@ -146,9 +146,7 @@ fn main() -> anyhow::Result<()> {
|
||||
let connected_to = block_emission.connected_to();
|
||||
let start_apply_block = Instant::now();
|
||||
wallet.apply_block_connected_to(&block_emission.block, height, connected_to)?;
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
wallet.persist(&mut db)?;
|
||||
let elapsed = start_apply_block.elapsed().as_secs_f32();
|
||||
println!(
|
||||
"Applied block {} at height {} in {}s",
|
||||
@@ -158,9 +156,7 @@ fn main() -> anyhow::Result<()> {
|
||||
Emission::Mempool(mempool_emission) => {
|
||||
let start_apply_mempool = Instant::now();
|
||||
wallet.apply_unconfirmed_txs(mempool_emission.iter().map(|(tx, time)| (tx, *time)));
|
||||
if let Some(changeset) = wallet.take_staged() {
|
||||
db.append_changeset(&changeset)?;
|
||||
}
|
||||
wallet.persist(&mut db)?;
|
||||
println!(
|
||||
"Applied unconfirmed transactions in {}s",
|
||||
start_apply_mempool.elapsed().as_secs_f32()
|
||||
|
||||
@@ -18,11 +18,11 @@ use bdk_chain::{bitcoin, collections::*, miniscript};
|
||||
use bitcoin::{
|
||||
absolute,
|
||||
bip32::{DerivationPath, Fingerprint, KeySource},
|
||||
blockdata::transaction::Sequence,
|
||||
ecdsa,
|
||||
hashes::{hash160, ripemd160, sha256},
|
||||
secp256k1::Secp256k1,
|
||||
taproot::{self, LeafVersion, TapLeafHash},
|
||||
transaction::Sequence,
|
||||
ScriptBuf, TxIn, Witness, WitnessVersion,
|
||||
};
|
||||
use miniscript::{
|
||||
|
||||
Reference in New Issue
Block a user