bitcoin: add electrum backend
This includes changes from darosior's comment in https://github.com/wizardsardine/liana/pull/1222#issuecomment-2324894986.
This commit is contained in:
parent
c4c2424f24
commit
c7ee862020
222
Cargo.lock
generated
222
Cargo.lock
generated
@ -62,12 +62,32 @@ version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||
|
||||
[[package]]
|
||||
name = "bdk_chain"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c601c4dc7e6c3efa538a0afbb43b964cefab9a9b5e8f352fa0ca38145448a5e7"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"miniscript",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bdk_coin_select"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c084bf76f0f67546fc814ffa82044144be1bb4618183a15016c162f8b087ad4"
|
||||
|
||||
[[package]]
|
||||
name = "bdk_electrum"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28906275aeb1f71dc32045670f06c8a26fb17cc62151a99f7425d258f4bda589"
|
||||
dependencies = [
|
||||
"bdk_chain",
|
||||
"electrum-client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bech32"
|
||||
version = "0.10.0-beta"
|
||||
@ -139,6 +159,12 @@ version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.83"
|
||||
@ -172,7 +198,24 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "electrum-client"
|
||||
version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89008f106be6f303695522f2f4c1f28b40c3e8367ed8b3bb227f1f882cb52cc2"
|
||||
dependencies = [
|
||||
"bitcoin",
|
||||
"byteorder",
|
||||
"libc",
|
||||
"log",
|
||||
"rustls",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"webpki-roots",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -268,6 +311,7 @@ version = "6.0.0"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bdk_coin_select",
|
||||
"bdk_electrum",
|
||||
"bip39",
|
||||
"dirs",
|
||||
"fern",
|
||||
@ -438,6 +482,21 @@ dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.30.0"
|
||||
@ -458,12 +517,44 @@ version = "0.1.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
|
||||
|
||||
[[package]]
|
||||
name = "sct"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "secp256k1"
|
||||
version = "0.28.0"
|
||||
@ -521,6 +612,12 @@ version = "1.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.46"
|
||||
@ -591,6 +688,12 @@ dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "vcpkg"
|
||||
version = "0.2.15"
|
||||
@ -609,13 +712,50 @@ version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -624,13 +764,29 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.52.6",
|
||||
"windows_aarch64_msvc 0.52.6",
|
||||
"windows_i686_gnu 0.52.6",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc 0.52.6",
|
||||
"windows_x86_64_gnu 0.52.6",
|
||||
"windows_x86_64_gnullvm 0.52.6",
|
||||
"windows_x86_64_msvc 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -639,42 +795,90 @@ version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.32"
|
||||
|
||||
@ -31,6 +31,10 @@ miniscript = { version = "11.0", features = ["serde", "compiler", "base64"] }
|
||||
# Coin selection algorithms for spend transaction creation.
|
||||
bdk_coin_select = "0.3"
|
||||
|
||||
# For Electrum backend. This is the latest version with the same bitcoin version as
|
||||
# the miniscript dependency.
|
||||
bdk_electrum = { version = "0.14" }
|
||||
|
||||
# Don't reinvent the wheel
|
||||
dirs = "5.0"
|
||||
|
||||
|
||||
411
src/bitcoin/electrum/client.rs
Normal file
411
src/bitcoin/electrum/client.rs
Normal file
@ -0,0 +1,411 @@
|
||||
use std::{collections::HashSet, convert::TryInto};
|
||||
|
||||
use bdk_electrum::{
|
||||
bdk_chain::{
|
||||
bitcoin,
|
||||
local_chain::{CheckPoint, LocalChain},
|
||||
spk_client::{FullScanRequest, FullScanResult, SyncRequest, SyncResult},
|
||||
BlockId, ChainPosition, ConfirmationHeightAnchor, TxGraph,
|
||||
},
|
||||
electrum_client::{self, Config, ElectrumApi},
|
||||
ElectrumExt,
|
||||
};
|
||||
|
||||
use super::utils::{
|
||||
block_id_from_tip, height_i32_from_usize, height_usize_from_i32, outpoints_from_tx,
|
||||
};
|
||||
use crate::{
|
||||
bitcoin::{electrum::utils::tip_from_block_id, BlockChainTip, MempoolEntry, MempoolEntryFees},
|
||||
config,
|
||||
};
|
||||
|
||||
// Default batch size to use when making requests to the Electrum server.
|
||||
const DEFAULT_BATCH_SIZE: usize = 200;
|
||||
|
||||
// If Electrum takes more than 3 minutes to answer one of our queries, fail.
|
||||
const RPC_SOCKET_TIMEOUT: u8 = 180;
|
||||
|
||||
// Number of retries while communicating with the Electrum server.
|
||||
// A retry happens with exponential back-off (base 2) so this makes us give up after (1+2+4+8+16+32=) 63 seconds.
|
||||
const RETRY_LIMIT: u8 = 6;
|
||||
|
||||
/// An error in the Electrum client.
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Server(electrum_client::Error),
|
||||
TipChanged(BlockId, BlockId),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::Server(e) => write!(f, "Electrum error: '{}'.", e),
|
||||
Error::TipChanged(expected, actual) => write!(
|
||||
f,
|
||||
"Electrum error: Expected tip '{}' but actual tip was {}.",
|
||||
tip_from_block_id(*expected),
|
||||
tip_from_block_id(*actual),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Client(electrum_client::Client);
|
||||
|
||||
impl Client {
|
||||
/// Create a new client and perform sanity checks.
|
||||
pub fn new(electrum_config: &config::ElectrumConfig) -> Result<Self, Error> {
|
||||
let config = Config::builder()
|
||||
.retry(RETRY_LIMIT)
|
||||
.timeout(Some(RPC_SOCKET_TIMEOUT))
|
||||
.build();
|
||||
let client =
|
||||
bdk_electrum::electrum_client::Client::from_config(&electrum_config.addr, config)
|
||||
.map_err(Error::Server)?;
|
||||
Ok(Self(client))
|
||||
}
|
||||
|
||||
pub fn chain_tip(&self) -> Result<BlockChainTip, Error> {
|
||||
self.0
|
||||
.block_headers_subscribe()
|
||||
.map_err(Error::Server)
|
||||
.map(|notif| BlockChainTip {
|
||||
height: height_i32_from_usize(notif.height),
|
||||
hash: notif.header.block_hash(),
|
||||
})
|
||||
}
|
||||
|
||||
fn genesis_block_header(&self) -> Result<bitcoin::block::Header, Error> {
|
||||
self.0.block_header(0).map_err(Error::Server)
|
||||
}
|
||||
|
||||
pub fn genesis_block_timestamp(&self) -> Result<u32, Error> {
|
||||
self.genesis_block_header().map(|header| header.time)
|
||||
}
|
||||
|
||||
pub fn genesis_block(&self) -> Result<BlockChainTip, Error> {
|
||||
self.genesis_block_header().map(|header| BlockChainTip {
|
||||
hash: header.block_hash(),
|
||||
height: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn broadcast_tx(&self, tx: &bitcoin::Transaction) -> Result<bitcoin::Txid, Error> {
|
||||
self.0.transaction_broadcast(tx).map_err(Error::Server)
|
||||
}
|
||||
|
||||
pub fn tip_time(&self) -> Result<u32, Error> {
|
||||
let tip_height = self.chain_tip()?.height;
|
||||
self.0
|
||||
.block_header(height_usize_from_i32(tip_height))
|
||||
.map_err(Error::Server)
|
||||
.map(|bh| bh.time)
|
||||
}
|
||||
|
||||
fn sync_with_confirmation_height_anchor(
|
||||
&self,
|
||||
request: SyncRequest,
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<SyncResult<ConfirmationHeightAnchor>, Error> {
|
||||
Ok(self
|
||||
.0
|
||||
.sync(request, DEFAULT_BATCH_SIZE, fetch_prev_txouts)
|
||||
.map_err(Error::Server)?
|
||||
.with_confirmation_height_anchor())
|
||||
}
|
||||
|
||||
/// Perform the given `SyncRequest` with `ConfirmationTimeHeightAnchor`.
|
||||
pub fn sync_with_confirmation_time_height_anchor(
|
||||
&self,
|
||||
request: SyncRequest,
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<SyncResult, Error> {
|
||||
self.0
|
||||
.sync(request, DEFAULT_BATCH_SIZE, fetch_prev_txouts)
|
||||
.map_err(Error::Server)?
|
||||
.with_confirmation_time_height_anchor(&self.0)
|
||||
.map_err(Error::Server)
|
||||
}
|
||||
|
||||
/// Perform the given `FullScanRequest` with `ConfirmationTimeHeightAnchor`.
|
||||
pub fn full_scan_with_confirmation_time_height_anchor<K: Ord + Clone>(
|
||||
&self,
|
||||
request: FullScanRequest<K>,
|
||||
stop_gap: usize,
|
||||
fetch_prev_txouts: bool,
|
||||
) -> Result<FullScanResult<K>, Error> {
|
||||
self.0
|
||||
.full_scan(request, stop_gap, DEFAULT_BATCH_SIZE, fetch_prev_txouts)
|
||||
.map_err(Error::Server)?
|
||||
.with_confirmation_time_height_anchor(&self.0)
|
||||
.map_err(Error::Server)
|
||||
}
|
||||
|
||||
/// Get mempool entries.
|
||||
///
|
||||
/// If `expected_tip` is specified, the function will return `Error::TipChanged` if the chain tip
|
||||
/// changes while the entries are being found. Otherwise, the function will restart in case the
|
||||
/// chain tip changes before completion.
|
||||
fn mempool_entries(
|
||||
&self,
|
||||
txids: HashSet<bitcoin::Txid>,
|
||||
expected_tip: Option<CheckPoint>,
|
||||
) -> Result<Vec<MempoolEntry>, Error> {
|
||||
log::debug!("Getting mempool entries for txids '{:?}'.", txids);
|
||||
let mut graph = TxGraph::default();
|
||||
let mut local_chain = LocalChain::from_genesis_hash(self.genesis_block()?.hash).0;
|
||||
let tip_block = if let Some(ref expected_tip) = expected_tip {
|
||||
expected_tip.block_id()
|
||||
} else {
|
||||
block_id_from_tip(self.chain_tip()?)
|
||||
};
|
||||
if tip_block.height > 0 {
|
||||
let _ = local_chain
|
||||
.insert_block(tip_block)
|
||||
.expect("only contains genesis block");
|
||||
}
|
||||
// First, get the tx itself and check it's unconfirmed.
|
||||
let request = SyncRequest::from_chain_tip(local_chain.tip()).chain_txids(txids.clone());
|
||||
// We'll get prev txouts for this tx when we find its ancestors below.
|
||||
let sync_result = self.sync_with_confirmation_height_anchor(request, false)?;
|
||||
let _ = local_chain.apply_update(sync_result.chain_update);
|
||||
// Store local tip after first sync. This will be our reference tip.
|
||||
let local_tip = local_chain.tip();
|
||||
if let Some(ref expected_tip) = expected_tip {
|
||||
if expected_tip != &local_chain.tip() {
|
||||
return Err(Error::TipChanged(
|
||||
expected_tip.block_id(),
|
||||
local_chain.tip().block_id(),
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut desc_ops = Vec::new();
|
||||
let mut txs = Vec::new();
|
||||
for txid in &txids {
|
||||
if let Some(ChainPosition::Unconfirmed(_)) = sync_result
|
||||
.graph_update
|
||||
.get_chain_position(&local_chain, local_chain.tip().block_id(), *txid)
|
||||
{
|
||||
let tx = sync_result
|
||||
.graph_update
|
||||
.get_tx(*txid)
|
||||
.expect("we must have tx in graph after sync");
|
||||
desc_ops.extend(outpoints_from_tx(&tx));
|
||||
txs.push(tx);
|
||||
}
|
||||
}
|
||||
let _ = graph.apply_update(sync_result.graph_update);
|
||||
// Now iterate over increasing depths of descendants.
|
||||
// As they are descendants, we can assume they are all unconfirmed.
|
||||
while !desc_ops.is_empty() {
|
||||
log::debug!("Syncing descendant outpoints: {:?}", desc_ops);
|
||||
let request = SyncRequest::from_chain_tip(local_chain.tip())
|
||||
.cache_graph_txs(&graph)
|
||||
.chain_outpoints(desc_ops.clone());
|
||||
// Fetch prev txouts to ensure we have all required txs in the graph to calculate fees.
|
||||
// An unconfirmed descendant may have a confirmed parent that we wouldn't have in our graph.
|
||||
let sync_result = self.sync_with_confirmation_height_anchor(request, true)?;
|
||||
let _ = local_chain.apply_update(sync_result.chain_update);
|
||||
if let Some(ref expected_tip) = expected_tip {
|
||||
if expected_tip != &local_chain.tip() {
|
||||
return Err(Error::TipChanged(
|
||||
expected_tip.block_id(),
|
||||
local_chain.tip().block_id(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if local_chain.tip() != local_tip {
|
||||
log::debug!("Chain tip changed while getting mempool entry. Restarting.");
|
||||
return self.mempool_entries(txids, expected_tip.clone());
|
||||
}
|
||||
let _ = graph.apply_update(sync_result.graph_update);
|
||||
// Get any txids spending the outpoints we've just synced against.
|
||||
let desc_txids: HashSet<_> = graph
|
||||
.filter_chain_txouts(
|
||||
&local_chain,
|
||||
local_chain.tip().block_id(),
|
||||
desc_ops.iter().map(|op| ((), *op)),
|
||||
)
|
||||
.filter_map(|(_, txout)| txout.spent_by.map(|(_, spend_txid)| spend_txid))
|
||||
.collect();
|
||||
desc_ops = desc_txids
|
||||
.iter()
|
||||
.flat_map(|txid| {
|
||||
let desc_tx = graph
|
||||
.get_tx(*txid)
|
||||
.expect("we must have tx in graph after sync");
|
||||
outpoints_from_tx(&desc_tx)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
// For each unconfirmed transaction, starting with `txid`, get its direct ancestors, which may be confirmed or unconfirmed.
|
||||
// Continue until there are no more unconfirmed ancestors.
|
||||
// Confirmed transactions will be filtered out from `anc_txids` later on.
|
||||
let mut anc_txids: HashSet<_> = txs
|
||||
.iter()
|
||||
.flat_map(|tx| tx.input.iter().map(|txin| txin.previous_output.txid))
|
||||
.collect();
|
||||
while !anc_txids.is_empty() {
|
||||
log::debug!("Syncing ancestor txids: {:?}", anc_txids);
|
||||
let request = SyncRequest::from_chain_tip(local_chain.tip())
|
||||
.cache_graph_txs(&graph)
|
||||
.chain_txids(anc_txids.clone());
|
||||
// We expect to have prev txouts for all unconfirmed ancestors in our graph so no need to fetch them here.
|
||||
// Note we keep iterating through ancestors until we find one that is confirmed and only need to calculate
|
||||
// fees for unconfirmed transactions.
|
||||
let sync_result = self.sync_with_confirmation_height_anchor(request, false)?;
|
||||
let _ = local_chain.apply_update(sync_result.chain_update);
|
||||
if let Some(expected_tip) = &expected_tip {
|
||||
if expected_tip != &local_chain.tip() {
|
||||
return Err(Error::TipChanged(
|
||||
expected_tip.block_id(),
|
||||
local_chain.tip().block_id(),
|
||||
));
|
||||
}
|
||||
}
|
||||
if local_chain.tip() != local_tip {
|
||||
log::debug!("Chain tip changed while getting mempool entry. Restarting.");
|
||||
return self.mempool_entries(txids, expected_tip);
|
||||
}
|
||||
let _ = graph.apply_update(sync_result.graph_update);
|
||||
|
||||
// Add ancestors of any unconfirmed txs.
|
||||
anc_txids = anc_txids
|
||||
.iter()
|
||||
.filter_map(|anc_txid| {
|
||||
if let Some(ChainPosition::Unconfirmed(_)) = graph.get_chain_position(
|
||||
&local_chain,
|
||||
local_chain.tip().block_id(),
|
||||
*anc_txid,
|
||||
) {
|
||||
let anc_tx = graph.get_tx(*anc_txid).expect("we must have it");
|
||||
Some(
|
||||
anc_tx
|
||||
.input
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|txin| txin.previous_output.txid)
|
||||
.collect::<HashSet<_>>(),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
.collect();
|
||||
}
|
||||
let mut entries = Vec::new();
|
||||
for tx in txs {
|
||||
// Now iterate over ancestors and descendants in the graph.
|
||||
let base_fee = graph
|
||||
.calculate_fee(&tx)
|
||||
.expect("all required txs are in graph");
|
||||
let base_size = tx.vsize();
|
||||
// Ancestor & descendant fees include those of `txid`.
|
||||
let mut desc_fees = base_fee;
|
||||
let mut anc_fees = base_fee;
|
||||
// Ancestor size includes that of `txid`.
|
||||
let mut anc_size = base_size;
|
||||
for desc_txid in graph.walk_descendants(tx.txid(), |_, desc_txid| Some(desc_txid)) {
|
||||
log::debug!("Getting fee for desc txid '{}'.", desc_txid);
|
||||
let desc_tx = graph
|
||||
.get_tx(desc_txid)
|
||||
.expect("all descendant txs are in graph");
|
||||
let fee = graph
|
||||
.calculate_fee(&desc_tx)
|
||||
.expect("all required txs are in graph");
|
||||
desc_fees += fee;
|
||||
}
|
||||
for anc_tx in graph.walk_ancestors(tx, |_, anc_tx| Some(anc_tx)) {
|
||||
log::debug!("Getting fee and size for anc txid '{}'.", anc_tx.txid());
|
||||
if let Some(ChainPosition::Unconfirmed(_)) = graph.get_chain_position(
|
||||
&local_chain,
|
||||
local_chain.tip().block_id(),
|
||||
anc_tx.txid(),
|
||||
) {
|
||||
let fee = graph
|
||||
.calculate_fee(&anc_tx)
|
||||
.expect("all required txs are in graph");
|
||||
anc_fees += fee;
|
||||
anc_size += anc_tx.vsize();
|
||||
} else {
|
||||
log::debug!("Ancestor txid '{}' is not unconfirmed.", anc_tx.txid());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let fees = MempoolEntryFees {
|
||||
base: bitcoin::Amount::from_sat(base_fee),
|
||||
ancestor: bitcoin::Amount::from_sat(anc_fees),
|
||||
descendant: bitcoin::Amount::from_sat(desc_fees),
|
||||
};
|
||||
let entry = MempoolEntry {
|
||||
vsize: base_size.try_into().expect("tx size must fit into u64"),
|
||||
fees,
|
||||
ancestor_vsize: anc_size.try_into().expect("tx size must fit into u64"),
|
||||
};
|
||||
entries.push(entry)
|
||||
}
|
||||
|
||||
// It's possible that the chain tip has now changed, but it hadn't done as of the last sync,
|
||||
// so go ahead and return the results.
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
/// Get mempool entry for a single `txid`.
|
||||
///
|
||||
/// Convenience method to call `mempool_entries` for a single `txid`,
|
||||
/// returning `Option` instead of `Vec`.
|
||||
pub fn mempool_entry(&self, txid: &bitcoin::Txid) -> Result<Option<MempoolEntry>, Error> {
|
||||
// We just require the chain tip to stay the same while running `mempool_entries` so
|
||||
// don't need to pass in an expected tip.
|
||||
self.mempool_entries(HashSet::from([*txid]), None)
|
||||
.map(|entries| entries.first().cloned())
|
||||
}
|
||||
|
||||
/// Get mempool spenders of the given outpoints.
|
||||
///
|
||||
/// Will restart if chain tip changes before completion.
|
||||
pub fn mempool_spenders(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Result<Vec<MempoolEntry>, Error> {
|
||||
log::debug!("Getting mempool spenders for outpoints: {:?}.", outpoints);
|
||||
let mut local_chain = LocalChain::from_genesis_hash(self.genesis_block()?.hash).0;
|
||||
let chain_tip = self.chain_tip()?;
|
||||
if chain_tip.height > 0 {
|
||||
let _ = local_chain
|
||||
.insert_block(block_id_from_tip(chain_tip))
|
||||
.expect("only contains genesis block");
|
||||
}
|
||||
let request =
|
||||
SyncRequest::from_chain_tip(local_chain.tip()).chain_outpoints(outpoints.to_vec());
|
||||
// We don't need to fetch prev txouts as we just want the outspends.
|
||||
let sync_result = self.sync_with_confirmation_height_anchor(request, false)?;
|
||||
let _ = local_chain.apply_update(sync_result.chain_update);
|
||||
// Store tip at which first sync was completed. This will be our reference tip.
|
||||
let local_tip = local_chain.tip();
|
||||
let graph = sync_result.graph_update;
|
||||
let txids: HashSet<_> = outpoints
|
||||
.iter()
|
||||
.flat_map(|op| graph.outspends(*op))
|
||||
.copied()
|
||||
.collect();
|
||||
let entries = match self.mempool_entries(txids, Some(local_tip)) {
|
||||
Ok(entries) => entries,
|
||||
Err(Error::TipChanged(expected, actual)) => {
|
||||
log::debug!(
|
||||
"Chain tip changed from {:?} to {:?} while \
|
||||
getting mempool spenders. Restarting.",
|
||||
expected,
|
||||
actual
|
||||
);
|
||||
return self.mempool_spenders(outpoints);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
Ok(entries)
|
||||
}
|
||||
}
|
||||
250
src/bitcoin/electrum/mod.rs
Normal file
250
src/bitcoin/electrum/mod.rs
Normal file
@ -0,0 +1,250 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bdk_electrum::bdk_chain::{
|
||||
bitcoin::{self, bip32::ChildNumber, BlockHash, OutPoint},
|
||||
local_chain::LocalChain,
|
||||
spk_client::{FullScanRequest, SyncRequest},
|
||||
ChainPosition,
|
||||
};
|
||||
|
||||
pub mod client;
|
||||
mod utils;
|
||||
pub mod wallet;
|
||||
use crate::bitcoin::{Block, BlockChainTip, Coin};
|
||||
|
||||
/// An error in the Electrum interface.
|
||||
#[derive(Debug)]
|
||||
pub enum ElectrumError {
|
||||
Client(client::Error),
|
||||
GenesisHashMismatch(
|
||||
BlockHash, /*expected hash*/
|
||||
BlockHash, /*server hash*/
|
||||
BlockHash, /*wallet hash*/
|
||||
),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ElectrumError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
match self {
|
||||
ElectrumError::Client(e) => write!(f, "Electrum client error: '{}'.", e),
|
||||
ElectrumError::GenesisHashMismatch(expected, server, wallet) => {
|
||||
write!(
|
||||
f,
|
||||
"Genesis hash mismatch. The genesis hash is expected to be '{}'. \
|
||||
The server has hash '{}' and the wallet has hash '{}'.",
|
||||
expected, server, wallet,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Interface for Electrum backend.
|
||||
pub struct Electrum {
|
||||
client: client::Client,
|
||||
bdk_wallet: wallet::BdkWallet,
|
||||
/// Used for setting the `last_seen` of unconfirmed transactions in a strictly
|
||||
/// increasing manner.
|
||||
sync_count: u64,
|
||||
/// Set to `true` to force a full scan from the genesis block regardless of
|
||||
/// the wallet's local chain height.
|
||||
full_scan: bool,
|
||||
}
|
||||
|
||||
impl Electrum {
|
||||
pub fn new(
|
||||
client: client::Client,
|
||||
bdk_wallet: wallet::BdkWallet,
|
||||
) -> Result<Self, ElectrumError> {
|
||||
Ok(Self {
|
||||
client,
|
||||
bdk_wallet,
|
||||
sync_count: 0,
|
||||
full_scan: false, // by default, only perform full scan if wallet's local chain has height 0
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sanity_checks(&self, expected_hash: &bitcoin::BlockHash) -> Result<(), ElectrumError> {
|
||||
let server_hash = self
|
||||
.client
|
||||
.genesis_block()
|
||||
.map_err(ElectrumError::Client)?
|
||||
.hash;
|
||||
let wallet_hash = self.bdk_wallet.local_chain().genesis_hash();
|
||||
if server_hash != *expected_hash || wallet_hash != *expected_hash {
|
||||
return Err(ElectrumError::GenesisHashMismatch(
|
||||
*expected_hash,
|
||||
server_hash,
|
||||
wallet_hash,
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn client(&self) -> &client::Client {
|
||||
&self.client
|
||||
}
|
||||
|
||||
fn local_chain(&self) -> &LocalChain {
|
||||
self.bdk_wallet.local_chain()
|
||||
}
|
||||
|
||||
/// Get all coins stored in the wallet, taking into consideration only those unconfirmed
|
||||
/// transactions that were seen in the last wallet sync.
|
||||
pub fn wallet_coins(&self, outpoints: Option<&[OutPoint]>) -> HashMap<OutPoint, Coin> {
|
||||
self.bdk_wallet.coins(outpoints, Some(self.sync_count))
|
||||
}
|
||||
|
||||
/// Get the tip of the wallet's local chain.
|
||||
pub fn wallet_tip(&self) -> BlockChainTip {
|
||||
utils::tip_from_block_id(self.local_chain().tip().block_id())
|
||||
}
|
||||
|
||||
/// Whether `tip` exists in the wallet's `local_chain`.
|
||||
///
|
||||
/// Returns `None` if no block at that height exists in `local_chain`.
|
||||
pub fn is_in_wallet_chain(&self, tip: BlockChainTip) -> Option<bool> {
|
||||
self.bdk_wallet.is_in_chain(tip)
|
||||
}
|
||||
|
||||
/// Whether we'll perform a full scan at the next poll.
|
||||
pub fn is_rescanning(&self) -> bool {
|
||||
self.full_scan || self.local_chain().tip().height() == 0
|
||||
}
|
||||
|
||||
/// Make the poller perform a full scan on the next iteration.
|
||||
pub fn trigger_rescan(&mut self) {
|
||||
self.full_scan = true;
|
||||
}
|
||||
|
||||
/// Sync the wallet with the Electrum server. If there was any reorg since the last poll, this
|
||||
/// returns the first common ancestor between the previous and the new chain.
|
||||
pub fn sync_wallet(
|
||||
&mut self,
|
||||
receive_index: ChildNumber,
|
||||
change_index: ChildNumber,
|
||||
) -> Result<Option<BlockChainTip>, ElectrumError> {
|
||||
self.bdk_wallet.reveal_spks(receive_index, change_index);
|
||||
let local_chain_tip = self.local_chain().tip();
|
||||
log::debug!(
|
||||
"local chain tip height before sync with electrum: {}",
|
||||
local_chain_tip.block_id().height
|
||||
);
|
||||
|
||||
// We'll only need to calculate fees of mempool transactions and this will be done separately from our graph
|
||||
// so we don't need to fetch prev txouts. In any case, we'll already have these for our own transactions.
|
||||
const FETCH_PREV_TXOUTS: bool = false;
|
||||
const STOP_GAP: usize = 50;
|
||||
|
||||
let (chain_update, mut graph_update, keychain_update) = if !self.is_rescanning() {
|
||||
log::info!("Performing sync.");
|
||||
let mut request = SyncRequest::from_chain_tip(local_chain_tip.clone())
|
||||
.cache_graph_txs(self.bdk_wallet.graph());
|
||||
|
||||
let all_spks: Vec<_> = self
|
||||
.bdk_wallet
|
||||
.index()
|
||||
.inner() // we include lookahead SPKs
|
||||
.all_spks()
|
||||
.iter()
|
||||
.map(|(_, script)| script.clone())
|
||||
.collect();
|
||||
request = request.chain_spks(all_spks);
|
||||
log::debug!("num SPKs for sync: {}", request.spks.len());
|
||||
|
||||
let sync_result = self
|
||||
.client
|
||||
.sync_with_confirmation_time_height_anchor(request, FETCH_PREV_TXOUTS)
|
||||
.map_err(ElectrumError::Client)?;
|
||||
log::info!("Sync complete.");
|
||||
(sync_result.chain_update, sync_result.graph_update, None)
|
||||
} else {
|
||||
log::info!("Performing full scan.");
|
||||
// Either local_chain has height 0 or we want to trigger a full scan.
|
||||
// In both cases, the scan should be from the genesis block.
|
||||
let genesis_block = local_chain_tip.get(0).expect("must contain genesis block");
|
||||
let mut request = FullScanRequest::from_chain_tip(genesis_block)
|
||||
.cache_graph_txs(self.bdk_wallet.graph());
|
||||
|
||||
for (k, spks) in self.bdk_wallet.index().all_unbounded_spk_iters() {
|
||||
request = request.set_spks_for_keychain(k, spks);
|
||||
}
|
||||
let scan_result = self
|
||||
.client
|
||||
.full_scan_with_confirmation_time_height_anchor(
|
||||
request,
|
||||
STOP_GAP,
|
||||
FETCH_PREV_TXOUTS,
|
||||
)
|
||||
.map_err(ElectrumError::Client)?;
|
||||
// A full scan only makes sense to do once, in most cases. Don't do it again unless
|
||||
// explicitly asked to by a user.
|
||||
self.full_scan = false;
|
||||
log::info!("Full scan complete.");
|
||||
(
|
||||
scan_result.chain_update,
|
||||
scan_result.graph_update,
|
||||
Some(scan_result.last_active_indices),
|
||||
)
|
||||
};
|
||||
log::debug!(
|
||||
"chain update height after sync with electrum: {}",
|
||||
chain_update.height()
|
||||
);
|
||||
|
||||
// Increment the sync count and apply changes.
|
||||
self.sync_count = self.sync_count.checked_add(1).expect("must fit");
|
||||
if let Some(keychain_update) = keychain_update {
|
||||
self.bdk_wallet.apply_keychain_update(keychain_update);
|
||||
}
|
||||
let changeset = self.bdk_wallet.apply_connected_chain_update(chain_update);
|
||||
|
||||
let mut changes_iter = changeset.into_iter();
|
||||
let reorg_common_ancestor = loop {
|
||||
match changes_iter.next() {
|
||||
Some((height, Some(_))) => {
|
||||
// `BlockHash` being `Some(_)` means a checkpoint at this height was added to the chain.
|
||||
// Since we iterate in ascending height order, we'll see the lowest block height first.
|
||||
// If the lowest height it adds is higher than our height before syncing, we're good.
|
||||
// Else if it's adding a block at height before syncing or lower, it's a reorg.
|
||||
break if height > local_chain_tip.height() {
|
||||
None
|
||||
} else {
|
||||
log::info!("Block chain reorganization detected.");
|
||||
Some(self.bdk_wallet.find_block_at_or_before_height(height))
|
||||
};
|
||||
}
|
||||
Some((_, None)) => continue,
|
||||
None => break None,
|
||||
}
|
||||
};
|
||||
|
||||
// Unconfirmed transactions have their last seen as 0, so we override to the `sync_count`
|
||||
// so that conflicts can be properly handled. We use `sync_count` instead of current time
|
||||
// in seconds to ensure strictly increasing values between poller iterations.
|
||||
for tx in &graph_update.initial_changeset().txs {
|
||||
let txid = tx.txid();
|
||||
if let Some(ChainPosition::Unconfirmed(_)) = graph_update.get_chain_position(
|
||||
self.local_chain(),
|
||||
self.local_chain().tip().block_id(),
|
||||
txid,
|
||||
) {
|
||||
log::debug!(
|
||||
"changing last seen for txid '{}' to {}",
|
||||
txid,
|
||||
self.sync_count
|
||||
);
|
||||
let _ = graph_update.insert_seen_at(txid, self.sync_count);
|
||||
}
|
||||
}
|
||||
self.bdk_wallet.apply_graph_update(graph_update);
|
||||
Ok(reorg_common_ancestor)
|
||||
}
|
||||
|
||||
pub fn wallet_transaction(
|
||||
&self,
|
||||
txid: &bitcoin::Txid,
|
||||
) -> Option<(bitcoin::Transaction, Option<Block>)> {
|
||||
self.bdk_wallet.get_transaction(txid)
|
||||
}
|
||||
}
|
||||
55
src/bitcoin/electrum/utils.rs
Normal file
55
src/bitcoin/electrum/utils.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use std::convert::TryInto;
|
||||
|
||||
use bdk_electrum::bdk_chain::{bitcoin, BlockId, ConfirmationTimeHeightAnchor};
|
||||
|
||||
use crate::bitcoin::{BlockChainTip, BlockInfo};
|
||||
|
||||
pub fn height_u32_from_i32(height: i32) -> u32 {
|
||||
height.try_into().expect("height must fit into u32")
|
||||
}
|
||||
|
||||
pub fn height_i32_from_u32(height: u32) -> i32 {
|
||||
height.try_into().expect("height must fit into i32")
|
||||
}
|
||||
|
||||
pub fn height_i32_from_usize(height: usize) -> i32 {
|
||||
height.try_into().expect("height must fit into i32")
|
||||
}
|
||||
|
||||
pub fn height_usize_from_i32(height: i32) -> usize {
|
||||
height.try_into().expect("height must fit into usize")
|
||||
}
|
||||
|
||||
pub fn block_id_from_tip(tip: BlockChainTip) -> BlockId {
|
||||
BlockId {
|
||||
height: height_u32_from_i32(tip.height),
|
||||
hash: tip.hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tip_from_block_id(id: BlockId) -> BlockChainTip {
|
||||
BlockChainTip {
|
||||
height: height_i32_from_u32(id.height),
|
||||
hash: id.hash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn block_info_from_anchor(anchor: ConfirmationTimeHeightAnchor) -> BlockInfo {
|
||||
BlockInfo {
|
||||
height: height_i32_from_u32(anchor.confirmation_height),
|
||||
time: anchor
|
||||
.confirmation_time
|
||||
.try_into()
|
||||
.expect("u32 by consensus"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the transaction's outpoints.
|
||||
pub fn outpoints_from_tx(tx: &bitcoin::Transaction) -> Vec<bitcoin::OutPoint> {
|
||||
let txid = tx.txid();
|
||||
(0..tx.output.len())
|
||||
.map(|i| {
|
||||
bitcoin::OutPoint::new(txid, i.try_into().expect("num tx outputs must fit in u32"))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
328
src/bitcoin/electrum/wallet.rs
Normal file
328
src/bitcoin/electrum/wallet.rs
Normal file
@ -0,0 +1,328 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
convert::TryInto,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use bdk_electrum::bdk_chain::{
|
||||
bitcoin::{self, bip32, BlockHash, OutPoint, ScriptBuf, TxOut},
|
||||
keychain::KeychainTxOutIndex,
|
||||
local_chain::{ChangeSet as ChainChangeSet, CheckPoint, LocalChain},
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
tx_graph::{self, TxGraph},
|
||||
ChainOracle, ChainPosition, ConfirmationTimeHeightAnchor, IndexedTxGraph,
|
||||
};
|
||||
use miniscript::bitcoin::bip32::ChildNumber;
|
||||
|
||||
use super::utils::{
|
||||
block_id_from_tip, block_info_from_anchor, height_i32_from_u32, height_u32_from_i32,
|
||||
};
|
||||
use crate::{
|
||||
bitcoin::{Block, BlockChainTip, Coin, COINBASE_MATURITY},
|
||||
descriptors::LianaDescriptor,
|
||||
};
|
||||
|
||||
// TODO: Move and reuse `liana::database::sqlite::utils::LOOK_AHEAD_LIMIT`?
|
||||
const LOOK_AHEAD_LIMIT: u32 = 200;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum KeychainType {
|
||||
Receive,
|
||||
Change,
|
||||
}
|
||||
|
||||
pub struct BdkWallet {
|
||||
graph: IndexedTxGraph<ConfirmationTimeHeightAnchor, KeychainTxOutIndex<KeychainType>>,
|
||||
local_chain: LocalChain,
|
||||
// Store descriptors for use when getting SPKs.
|
||||
receive_desc: Descriptor<DescriptorPublicKey>,
|
||||
change_desc: Descriptor<DescriptorPublicKey>,
|
||||
}
|
||||
|
||||
impl BdkWallet {
|
||||
/// Create a new BDK wallet and initialize with the given data that was
|
||||
/// valid as of `tip`.
|
||||
///
|
||||
/// If there is no `tip`, then any provided data will be ignored.
|
||||
///
|
||||
/// `receive_index` and `change_index` are the last used derivation
|
||||
/// indices for the receive and change descriptors, respectively.
|
||||
pub fn new(
|
||||
main_descriptor: &LianaDescriptor,
|
||||
genesis_hash: BlockHash,
|
||||
tip: Option<BlockChainTip>,
|
||||
coins: &[Coin],
|
||||
txs: &[bitcoin::Transaction],
|
||||
receive_index: ChildNumber,
|
||||
change_index: ChildNumber,
|
||||
) -> Self {
|
||||
let local_chain = LocalChain::from_genesis_hash(genesis_hash).0;
|
||||
let receive_desc = main_descriptor
|
||||
.receive_descriptor()
|
||||
.as_descriptor_public_key();
|
||||
let change_desc = main_descriptor
|
||||
.change_descriptor()
|
||||
.as_descriptor_public_key();
|
||||
|
||||
let mut bdk_wallet = BdkWallet {
|
||||
graph: {
|
||||
let mut indexer = KeychainTxOutIndex::<KeychainType>::new(LOOK_AHEAD_LIMIT);
|
||||
let _ = indexer.insert_descriptor(KeychainType::Receive, receive_desc.clone());
|
||||
let _ = indexer.insert_descriptor(KeychainType::Change, change_desc.clone());
|
||||
IndexedTxGraph::new(indexer)
|
||||
},
|
||||
local_chain,
|
||||
receive_desc: receive_desc.clone(),
|
||||
change_desc: change_desc.clone(),
|
||||
};
|
||||
if let Some(tip) = tip {
|
||||
// This will be our anchor for any confirmed transactions.
|
||||
let anchor_block = block_id_from_tip(tip);
|
||||
if tip.height > 0 {
|
||||
log::debug!("inserting block into local chain: {:?}", anchor_block);
|
||||
let _ = bdk_wallet
|
||||
.local_chain
|
||||
.insert_block(anchor_block)
|
||||
.expect("local chain only contains genesis block");
|
||||
}
|
||||
// Update the last used derivation index for both change and receive addresses.
|
||||
log::debug!(
|
||||
"revealing SPKs up to receive index {receive_index} and change index {change_index}"
|
||||
);
|
||||
bdk_wallet.reveal_spks(receive_index, change_index);
|
||||
|
||||
// Update the existing coins and transactions information using a TxGraph changeset.
|
||||
log::debug!("Number of coins to load: {}.", coins.len());
|
||||
log::debug!("Number of txs to load: {}.", txs.len());
|
||||
let mut graph_cs = tx_graph::ChangeSet::default();
|
||||
for tx in txs {
|
||||
graph_cs.txs.insert(Arc::new(tx.clone()));
|
||||
}
|
||||
for coin in coins {
|
||||
// First of all insert the txout itself.
|
||||
let script_pubkey = bdk_wallet.get_spk(coin.derivation_index, coin.is_change);
|
||||
let txout = TxOut {
|
||||
script_pubkey,
|
||||
value: coin.amount,
|
||||
};
|
||||
graph_cs.txouts.insert(coin.outpoint, txout);
|
||||
// If the coin's deposit transaction is confirmed, tell BDK by inserting an anchor.
|
||||
// Otherwise, we could insert a last seen timestamp but we don't have such data stored in
|
||||
// the table.
|
||||
if let Some(block) = coin.block_info {
|
||||
graph_cs.anchors.insert((
|
||||
ConfirmationTimeHeightAnchor {
|
||||
confirmation_height: height_u32_from_i32(block.height),
|
||||
confirmation_time: block.time.into(),
|
||||
anchor_block,
|
||||
},
|
||||
coin.outpoint.txid,
|
||||
));
|
||||
}
|
||||
// If the coin's spending transaction is confirmed, do the same.
|
||||
if let Some(block) = coin.spend_block {
|
||||
let spend_txid = coin.spend_txid.expect("Must be present if confirmed.");
|
||||
graph_cs.anchors.insert((
|
||||
ConfirmationTimeHeightAnchor {
|
||||
confirmation_height: height_u32_from_i32(block.height),
|
||||
confirmation_time: block.time.into(),
|
||||
anchor_block,
|
||||
},
|
||||
spend_txid,
|
||||
));
|
||||
}
|
||||
}
|
||||
let mut graph = TxGraph::default();
|
||||
graph.apply_changeset(graph_cs);
|
||||
let _ = bdk_wallet.graph.apply_update(graph);
|
||||
}
|
||||
bdk_wallet
|
||||
}
|
||||
|
||||
/// Get a reference to the local chain.
|
||||
pub fn local_chain(&self) -> &LocalChain {
|
||||
&self.local_chain
|
||||
}
|
||||
|
||||
/// Whether `tip` exists in `local_chain`.
|
||||
///
|
||||
/// Returns `None` if no block at that height exists in `local_chain`.
|
||||
pub fn is_in_chain(&self, tip: BlockChainTip) -> Option<bool> {
|
||||
self.local_chain
|
||||
.is_block_in_chain(block_id_from_tip(tip), self.local_chain().tip().block_id())
|
||||
.expect("function is infallible")
|
||||
}
|
||||
|
||||
/// Get a reference to the graph.
|
||||
pub fn graph(&self) -> &TxGraph<ConfirmationTimeHeightAnchor> {
|
||||
self.graph.graph()
|
||||
}
|
||||
|
||||
/// Get a reference to the transaction index.
|
||||
pub fn index(&self) -> &KeychainTxOutIndex<KeychainType> {
|
||||
&self.graph.index
|
||||
}
|
||||
|
||||
/// Reveal SPKs based on derivation indices set in DB.
|
||||
pub fn reveal_spks(&mut self, receive_index: ChildNumber, change_index: ChildNumber) {
|
||||
let mut keychain_update = BTreeMap::new();
|
||||
keychain_update.insert(KeychainType::Receive, receive_index.into());
|
||||
keychain_update.insert(KeychainType::Change, change_index.into());
|
||||
self.apply_keychain_update(keychain_update)
|
||||
}
|
||||
|
||||
fn get_spk(&self, der_index: bip32::ChildNumber, is_change: bool) -> ScriptBuf {
|
||||
// Try to get it from the BDK wallet cache first, failing that derive it from the appropriate
|
||||
// descriptor.
|
||||
let chain_kind = if is_change {
|
||||
KeychainType::Change
|
||||
} else {
|
||||
KeychainType::Receive
|
||||
};
|
||||
if let Some(spk) = self.graph.index.spk_at_index(chain_kind, der_index.into()) {
|
||||
spk.to_owned()
|
||||
} else {
|
||||
let desc = if is_change {
|
||||
&self.change_desc
|
||||
} else {
|
||||
&self.receive_desc
|
||||
};
|
||||
desc.at_derivation_index(der_index.into())
|
||||
.expect("Not multipath and index isn't hardened.")
|
||||
.script_pubkey()
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the coins currently stored by the `BdkWallet` optionally filtered by `outpoints`.
|
||||
/// If `outpoints` is `None`, no filter will be applied.
|
||||
/// If `outpoints` is an empty slice, no coins will be returned.
|
||||
/// If `last_seen` is set, only those unconfirmed transactions with a matching last seen
|
||||
/// will be considered.
|
||||
pub fn coins(
|
||||
&self,
|
||||
outpoints: Option<&[bitcoin::OutPoint]>,
|
||||
last_seen: Option<u64>,
|
||||
) -> HashMap<OutPoint, Coin> {
|
||||
// Get an iterator over all the wallet txos (not only the currently unspent ones) by using
|
||||
// lower level methods.
|
||||
let tx_graph = self.graph.graph();
|
||||
let txo_index = &self.graph.index;
|
||||
let tip_id = self.local_chain.tip().block_id();
|
||||
let wallet_txos =
|
||||
tx_graph.filter_chain_txouts(&self.local_chain, tip_id, txo_index.outpoints());
|
||||
let mut wallet_coins = HashMap::new();
|
||||
// Go through all the wallet txos and create a coin for each.
|
||||
for ((k, i), full_txo) in wallet_txos {
|
||||
let outpoint = full_txo.outpoint;
|
||||
if outpoints.map(|ops| !ops.contains(&outpoint)) == Some(true) {
|
||||
continue;
|
||||
}
|
||||
let amount = full_txo.txout.value;
|
||||
let derivation_index = i.into();
|
||||
let is_change = matches!(k, KeychainType::Change);
|
||||
let block_info = match full_txo.chain_position {
|
||||
ChainPosition::Unconfirmed(ls) => {
|
||||
if let Some(last_seen) = last_seen.filter(|last_seen| *last_seen != ls) {
|
||||
log::debug!("Ignoring coin at {}, which was last seen at {} instead of {} as required.", outpoint, ls, last_seen);
|
||||
continue;
|
||||
}
|
||||
None
|
||||
}
|
||||
ChainPosition::Confirmed(anchor) => Some(block_info_from_anchor(anchor)),
|
||||
};
|
||||
|
||||
// Immature if from a coinbase transaction with less than a hundred confs.
|
||||
let is_immature = full_txo.is_on_coinbase
|
||||
&& block_info
|
||||
.and_then(|blk| {
|
||||
let tip_height: i32 = height_i32_from_u32(tip_id.height);
|
||||
tip_height
|
||||
.checked_sub(blk.height)
|
||||
.map(|confs| confs < COINBASE_MATURITY)
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
// Get spend status of this coin.
|
||||
let (mut spend_txid, mut spend_block) = (None, None);
|
||||
if let Some((spend_pos, txid)) = full_txo.spent_by {
|
||||
spend_txid = Some(txid);
|
||||
match spend_pos {
|
||||
ChainPosition::Unconfirmed(ls) => {
|
||||
if let Some(last_seen) = last_seen.filter(|last_seen| *last_seen != ls) {
|
||||
log::debug!(
|
||||
"Ignoring spend txid {} for coin at {}, \
|
||||
which was last seen at {} instead of {} as required.",
|
||||
txid,
|
||||
outpoint,
|
||||
ls,
|
||||
last_seen
|
||||
);
|
||||
spend_txid = None;
|
||||
}
|
||||
}
|
||||
ChainPosition::Confirmed(anchor) => {
|
||||
spend_block = Some(block_info_from_anchor(anchor));
|
||||
}
|
||||
};
|
||||
}
|
||||
let coin = crate::bitcoin::Coin {
|
||||
outpoint,
|
||||
amount,
|
||||
derivation_index,
|
||||
is_change,
|
||||
is_immature,
|
||||
block_info,
|
||||
spend_txid,
|
||||
spend_block,
|
||||
};
|
||||
wallet_coins.insert(coin.outpoint, coin);
|
||||
}
|
||||
wallet_coins
|
||||
}
|
||||
|
||||
pub fn get_transaction(
|
||||
&self,
|
||||
txid: &bitcoin::Txid,
|
||||
) -> Option<(bitcoin::Transaction, Option<Block>)> {
|
||||
self.graph.graph().get_tx_node(*txid).map(|tx_node| {
|
||||
let block = tx_node.anchors.iter().next().map(|info| Block {
|
||||
hash: info.anchor_block.hash, // not necessarily the confirmation block hash
|
||||
height: height_i32_from_u32(info.confirmation_height),
|
||||
time: info.confirmation_time.try_into().expect("u32 by consensus"),
|
||||
});
|
||||
let tx = tx_node.tx.as_ref().clone();
|
||||
(tx, block)
|
||||
})
|
||||
}
|
||||
|
||||
/// Find the first block in the local chain whose height is less than or equal to this.
|
||||
pub fn find_block_at_or_before_height(&self, height: u32) -> BlockChainTip {
|
||||
for cp in self.local_chain.iter_checkpoints() {
|
||||
if cp.height() <= height {
|
||||
return BlockChainTip {
|
||||
height: height_i32_from_u32(cp.height()),
|
||||
hash: cp.hash(),
|
||||
};
|
||||
}
|
||||
}
|
||||
unreachable!("There must be at least the genesis block.")
|
||||
}
|
||||
|
||||
/// Apply an update to the local chain.
|
||||
/// Panics if update does not connect to the local chain.
|
||||
pub fn apply_connected_chain_update(&mut self, chain_update: CheckPoint) -> ChainChangeSet {
|
||||
self.local_chain
|
||||
.apply_update(chain_update)
|
||||
.expect("update must connect to local chain")
|
||||
}
|
||||
|
||||
/// Apply a graph update.
|
||||
pub fn apply_graph_update(&mut self, graph_update: TxGraph<ConfirmationTimeHeightAnchor>) {
|
||||
let _ = self.graph.apply_update(graph_update);
|
||||
}
|
||||
|
||||
/// Apply a keychain update.
|
||||
pub fn apply_keychain_update(&mut self, keychain_update: BTreeMap<KeychainType, u32>) {
|
||||
let _ = self.graph.index.reveal_to_target_multi(&keychain_update);
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,7 @@
|
||||
//! Broadcast transactions, poll for new unspent coins, gather fee estimates.
|
||||
|
||||
pub mod d;
|
||||
pub mod electrum;
|
||||
pub mod poller;
|
||||
|
||||
use crate::{
|
||||
@ -405,6 +406,193 @@ impl BitcoinInterface for d::BitcoinD {
|
||||
}
|
||||
}
|
||||
|
||||
impl BitcoinInterface for electrum::Electrum {
|
||||
fn sync_wallet(
|
||||
&mut self,
|
||||
receive_index: ChildNumber,
|
||||
change_index: ChildNumber,
|
||||
) -> Result<Option<BlockChainTip>, String> {
|
||||
self.sync_wallet(receive_index, change_index)
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
fn received_coins(
|
||||
&self,
|
||||
tip: &BlockChainTip,
|
||||
_descs: &[descriptors::SinglePathLianaDesc],
|
||||
) -> Vec<UTxO> {
|
||||
// Get those wallet coins that are either unconfirmed or have a confirmation height
|
||||
// after tip. The poller will then discard any that had already been received.
|
||||
self.wallet_coins(None)
|
||||
.values()
|
||||
.filter_map(|c| {
|
||||
let height = c.block_info.map(|info| info.height);
|
||||
if height.filter(|h| *h <= tip.height).is_some() {
|
||||
None
|
||||
} else {
|
||||
Some(UTxO {
|
||||
outpoint: c.outpoint,
|
||||
block_height: height,
|
||||
amount: c.amount,
|
||||
address: UTxOAddress::DerivIndex(c.derivation_index, c.is_change),
|
||||
is_immature: c.is_immature,
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn confirmed_coins(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> (Vec<(bitcoin::OutPoint, i32, u32)>, Vec<bitcoin::OutPoint>) {
|
||||
let wallet_coins = &self.wallet_coins(Some(outpoints));
|
||||
let mut confirmed = Vec::new();
|
||||
let mut expired = Vec::new();
|
||||
for op in outpoints {
|
||||
if let Some(w_c) = wallet_coins.get(op) {
|
||||
if let Some(block) = w_c.block_info {
|
||||
if w_c.is_immature {
|
||||
log::debug!(
|
||||
"Coin at '{}' comes from an immature coinbase transaction at \
|
||||
block height {}. Not marking it as confirmed for now.",
|
||||
op,
|
||||
block.height
|
||||
);
|
||||
continue;
|
||||
}
|
||||
confirmed.push((w_c.outpoint, block.height, block.time));
|
||||
}
|
||||
} else {
|
||||
expired.push(*op);
|
||||
}
|
||||
}
|
||||
(confirmed, expired)
|
||||
}
|
||||
|
||||
fn spending_coins(
|
||||
&self,
|
||||
outpoints: &[bitcoin::OutPoint],
|
||||
) -> Vec<(bitcoin::OutPoint, bitcoin::Txid)> {
|
||||
let wallet_coins = &self.wallet_coins(Some(outpoints));
|
||||
outpoints
|
||||
.iter()
|
||||
.filter_map(|op| {
|
||||
if let Some(w_c) = wallet_coins.get(op) {
|
||||
w_c.spend_txid.map(|txid| (w_c.outpoint, txid))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn spent_coins(
|
||||
&self,
|
||||
outpoints: &[(bitcoin::OutPoint, bitcoin::Txid)],
|
||||
) -> (Vec<SpentCoin>, Vec<bitcoin::OutPoint>) {
|
||||
let ops: Vec<_> = outpoints.iter().map(|(op, _)| op).copied().collect();
|
||||
let wallet_coins = &self.wallet_coins(Some(&ops));
|
||||
let mut spent = Vec::new();
|
||||
let mut expired_spending = Vec::new();
|
||||
|
||||
for (op, spend_txid) in outpoints {
|
||||
if let Some(w_c) = wallet_coins.get(op) {
|
||||
if w_c.spend_txid != Some(*spend_txid) {
|
||||
expired_spending.push(*op);
|
||||
}
|
||||
if let Some(block) = w_c.spend_block {
|
||||
spent.push((*op, *spend_txid, block.height, block.time));
|
||||
}
|
||||
}
|
||||
}
|
||||
(spent, expired_spending)
|
||||
}
|
||||
|
||||
fn genesis_block_timestamp(&self) -> u32 {
|
||||
self.client()
|
||||
.genesis_block_timestamp()
|
||||
.expect("Genesis block timestamp must always be there")
|
||||
}
|
||||
|
||||
fn genesis_block(&self) -> BlockChainTip {
|
||||
self.client()
|
||||
.genesis_block()
|
||||
.expect("Genesis block must always be there")
|
||||
}
|
||||
|
||||
fn chain_tip(&self) -> BlockChainTip {
|
||||
// We want the wallet's local chain tip after syncing.
|
||||
self.wallet_tip()
|
||||
}
|
||||
|
||||
fn is_in_chain(&self, tip: &BlockChainTip) -> bool {
|
||||
// Return `false` if no block at same height as `tip`
|
||||
// is in wallet's local chain.
|
||||
self.is_in_wallet_chain(*tip).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// FIXME: make the Bitcoin backend interface higher level. See the comment in the poller next
|
||||
/// to the `sync_wallet()` call.
|
||||
fn common_ancestor(&self, _tip: &BlockChainTip) -> Option<BlockChainTip> {
|
||||
unreachable!("The common ancestor is returned in `sync_wallet()`. If no reorg was detected then, this method will never be called on an Electrum backend.")
|
||||
}
|
||||
|
||||
fn broadcast_tx(&self, tx: &bitcoin::Transaction) -> Result<(), String> {
|
||||
match self.client().broadcast_tx(tx) {
|
||||
Ok(_txid) => Ok(()),
|
||||
Err(e) => Err(e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn wallet_transaction(
|
||||
&self,
|
||||
txid: &bitcoin::Txid,
|
||||
) -> Option<(bitcoin::Transaction, Option<Block>)> {
|
||||
self.wallet_transaction(txid)
|
||||
}
|
||||
|
||||
fn mempool_entry(&self, txid: &bitcoin::Txid) -> Option<MempoolEntry> {
|
||||
self.client().mempool_entry(txid).ok()?
|
||||
}
|
||||
|
||||
fn mempool_spenders(&self, outpoints: &[bitcoin::OutPoint]) -> Vec<MempoolEntry> {
|
||||
self.client()
|
||||
.mempool_spenders(outpoints)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn sync_progress(&self) -> SyncProgress {
|
||||
// Always return 100% for now since the API is bitcoind-specific to mean "blocks/headers".
|
||||
// But in the future it would be nice to inform the user about the progress of the sync
|
||||
// if it takes a few dozen seconds.
|
||||
let blocks = self.chain_tip().height as u64;
|
||||
SyncProgress::new(1.0, blocks, blocks)
|
||||
}
|
||||
|
||||
fn start_rescan(
|
||||
&mut self,
|
||||
_desc: &descriptors::LianaDescriptor,
|
||||
_timestamp: u32,
|
||||
) -> Result<(), String> {
|
||||
self.trigger_rescan();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rescan_progress(&self) -> Option<f64> {
|
||||
// Until we sync we're at 0%. After the sync, we're at 100%.
|
||||
self.is_rescanning().then_some(0.0)
|
||||
}
|
||||
|
||||
fn block_before_date(&self, _timestamp: u32) -> Option<BlockChainTip> {
|
||||
Some(self.genesis_block())
|
||||
}
|
||||
|
||||
fn tip_time(&self) -> Option<u32> {
|
||||
self.client().tip_time().ok()
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: do we need to repeat the entire trait implemenation? Isn't there a nicer way?
|
||||
impl BitcoinInterface for sync::Arc<sync::Mutex<dyn BitcoinInterface + 'static>> {
|
||||
fn genesis_block_timestamp(&self) -> u32 {
|
||||
@ -528,3 +716,21 @@ pub enum UTxOAddress {
|
||||
/// Derivation index and whether it is from the change descriptor.
|
||||
DerivIndex(ChildNumber, bool),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BlockInfo {
|
||||
pub height: i32,
|
||||
pub time: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Coin {
|
||||
pub outpoint: bitcoin::OutPoint,
|
||||
pub amount: bitcoin::Amount,
|
||||
pub derivation_index: ChildNumber,
|
||||
pub is_change: bool,
|
||||
pub is_immature: bool,
|
||||
pub block_info: Option<BlockInfo>,
|
||||
pub spend_txid: Option<bitcoin::Txid>,
|
||||
pub spend_block: Option<BlockInfo>,
|
||||
}
|
||||
|
||||
@ -91,6 +91,9 @@ pub enum BitcoinBackend {
|
||||
/// Settings specific to bitcoind as the Bitcoin interface.
|
||||
#[serde(rename = "bitcoind_config")]
|
||||
Bitcoind(BitcoindConfig),
|
||||
/// Settings specific to Electrum as the Bitcoin interface.
|
||||
#[serde(rename = "electrum_config")]
|
||||
Electrum(ElectrumConfig),
|
||||
}
|
||||
|
||||
/// RPC authentication options.
|
||||
@ -123,6 +126,15 @@ pub struct BitcoindConfig {
|
||||
pub addr: SocketAddr,
|
||||
}
|
||||
|
||||
/// Everything we need to know for talking to Electrum serenely.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ElectrumConfig {
|
||||
/// The URL the Electrum's RPC is listening on.
|
||||
/// Include "ssl://" for SSL. otherwise TCP will be assumed.
|
||||
/// Can optionally prefix with "tcp://".
|
||||
pub addr: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct BitcoinConfig {
|
||||
/// The network we are operating on, one of "bitcoin", "testnet", "regtest", "signet"
|
||||
|
||||
90
src/lib.rs
90
src/lib.rs
@ -14,9 +14,13 @@ pub mod spend;
|
||||
mod testutils;
|
||||
|
||||
pub use bip39;
|
||||
use bitcoin::electrum;
|
||||
pub use miniscript;
|
||||
|
||||
pub use crate::bitcoin::d::{BitcoinD, BitcoindError, WalletError};
|
||||
pub use crate::bitcoin::{
|
||||
d::{BitcoinD, BitcoindError, WalletError},
|
||||
electrum::{Electrum, ElectrumError},
|
||||
};
|
||||
#[cfg(feature = "daemon")]
|
||||
use crate::jsonrpc::server::{rpcserver_loop, rpcserver_setup};
|
||||
use crate::{
|
||||
@ -34,7 +38,7 @@ use std::{
|
||||
thread,
|
||||
};
|
||||
|
||||
use miniscript::bitcoin::secp256k1;
|
||||
use miniscript::bitcoin::{constants::ChainHash, hashes::Hash, secp256k1, BlockHash};
|
||||
|
||||
#[cfg(not(test))]
|
||||
use std::panic;
|
||||
@ -92,10 +96,12 @@ pub enum StartupError {
|
||||
DefaultDataDirNotFound,
|
||||
DatadirCreation(path::PathBuf, io::Error),
|
||||
MissingBitcoindConfig,
|
||||
MissingElectrumConfig,
|
||||
MissingBitcoinBackendConfig,
|
||||
DbMigrateBitcoinTxs(&'static str),
|
||||
Database(SqliteDbError),
|
||||
Bitcoind(BitcoindError),
|
||||
Electrum(ElectrumError),
|
||||
#[cfg(unix)]
|
||||
Daemonization(&'static str),
|
||||
#[cfg(windows)]
|
||||
@ -118,6 +124,10 @@ impl fmt::Display for StartupError {
|
||||
f,
|
||||
"Our Bitcoin interface is bitcoind but we have no 'bitcoind_config' entry in the configuration."
|
||||
),
|
||||
Self::MissingElectrumConfig => write!(
|
||||
f,
|
||||
"Our Bitcoin interface is Electrum but we have no 'electrum_config' entry in the configuration."
|
||||
),
|
||||
Self::MissingBitcoinBackendConfig => write!(
|
||||
f,
|
||||
"No Bitcoin backend entry in the configuration."
|
||||
@ -128,6 +138,7 @@ impl fmt::Display for StartupError {
|
||||
),
|
||||
Self::Database(e) => write!(f, "Error initializing database: '{}'.", e),
|
||||
Self::Bitcoind(e) => write!(f, "Error setting up bitcoind interface: '{}'.", e),
|
||||
Self::Electrum(e) => write!(f, "Error setting up Electrum interface: '{}'.", e),
|
||||
#[cfg(unix)]
|
||||
Self::Daemonization(e) => write!(f, "Error when daemonizing: '{}'.", e),
|
||||
#[cfg(windows)]
|
||||
@ -265,10 +276,10 @@ fn setup_bitcoind(
|
||||
#[cfg(target_os = "windows")]
|
||||
let wo_path_str = wo_path_str.replace("\\\\?\\", "").replace("\\\\?", "");
|
||||
|
||||
let config::BitcoinBackend::Bitcoind(bitcoind_config) = config
|
||||
.bitcoin_backend
|
||||
.as_ref()
|
||||
.ok_or(StartupError::MissingBitcoindConfig)?;
|
||||
let bitcoind_config = match config.bitcoin_backend.as_ref() {
|
||||
Some(config::BitcoinBackend::Bitcoind(bitcoind_config)) => bitcoind_config,
|
||||
_ => Err(StartupError::MissingBitcoindConfig)?,
|
||||
};
|
||||
let bitcoind = BitcoinD::new(bitcoind_config, wo_path_str)?;
|
||||
bitcoind.node_sanity_checks(
|
||||
config.bitcoin_config.network,
|
||||
@ -292,6 +303,70 @@ fn setup_bitcoind(
|
||||
Ok(bitcoind)
|
||||
}
|
||||
|
||||
// Create an Electrum interface from a client and BDK-based wallet, and do some sanity checks.
|
||||
// If all went well, returns the interface to Electrum.
|
||||
fn setup_electrum(
|
||||
config: &Config,
|
||||
db: sync::Arc<sync::Mutex<dyn DatabaseInterface>>,
|
||||
) -> Result<Electrum, StartupError> {
|
||||
let electrum_config = match config.bitcoin_backend.as_ref() {
|
||||
Some(config::BitcoinBackend::Electrum(electrum_config)) => electrum_config,
|
||||
_ => Err(StartupError::MissingElectrumConfig)?,
|
||||
};
|
||||
// First create the client to communicate with the Electrum server.
|
||||
let client = electrum::client::Client::new(electrum_config)
|
||||
.map_err(|e| StartupError::Electrum(ElectrumError::Client(e)))?;
|
||||
// Then create the BDK-based wallet and populate it with DB data.
|
||||
let mut db_conn = db.connection();
|
||||
let tip = db_conn.chain_tip();
|
||||
let coins: Vec<_> = db_conn
|
||||
.coins(&[], &[])
|
||||
.into_values()
|
||||
.map(|c| crate::bitcoin::Coin {
|
||||
outpoint: c.outpoint,
|
||||
amount: c.amount,
|
||||
derivation_index: c.derivation_index,
|
||||
is_change: c.is_change,
|
||||
is_immature: c.is_immature,
|
||||
block_info: c.block_info.map(|info| crate::bitcoin::BlockInfo {
|
||||
height: info.height,
|
||||
time: info.time,
|
||||
}),
|
||||
spend_txid: c.spend_txid,
|
||||
spend_block: c.spend_block.map(|info| crate::bitcoin::BlockInfo {
|
||||
height: info.height,
|
||||
time: info.time,
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
let txids = db_conn.list_saved_txids();
|
||||
// This will only return those txs referenced by our coins, which may not be all of `txids`.
|
||||
let txs: Vec<_> = db_conn
|
||||
.list_wallet_transactions(&txids)
|
||||
.into_iter()
|
||||
.map(|(tx, _, _)| tx)
|
||||
.collect();
|
||||
let (receive_index, change_index) = (db_conn.receive_index(), db_conn.change_index());
|
||||
let genesis_hash = {
|
||||
let chain_hash = ChainHash::using_genesis_block(config.bitcoin_config.network);
|
||||
BlockHash::from_byte_array(*chain_hash.as_bytes())
|
||||
};
|
||||
let bdk_wallet = electrum::wallet::BdkWallet::new(
|
||||
&config.main_descriptor,
|
||||
genesis_hash,
|
||||
tip,
|
||||
&coins,
|
||||
&txs,
|
||||
receive_index,
|
||||
change_index,
|
||||
);
|
||||
let electrum = Electrum::new(client, bdk_wallet).map_err(StartupError::Electrum)?;
|
||||
electrum
|
||||
.sanity_checks(&genesis_hash)
|
||||
.map_err(StartupError::Electrum)?;
|
||||
Ok(electrum)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DaemonControl {
|
||||
config: Config,
|
||||
@ -407,6 +482,9 @@ impl DaemonHandle {
|
||||
sync::Mutex::from(bitcoind.expect("bitcoind must have been set already")),
|
||||
)
|
||||
as sync::Arc<sync::Mutex<dyn BitcoinInterface>>,
|
||||
(None, Some(config::BitcoinBackend::Electrum(..))) => {
|
||||
sync::Arc::from(sync::Mutex::from(setup_electrum(&config, db.clone())?))
|
||||
}
|
||||
(None, None) => Err(StartupError::MissingBitcoinBackendConfig)?,
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user