Compare commits
83 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2fdc5ccf8 | ||
|
|
df6675dc09 | ||
|
|
4b98565ee2 | ||
|
|
5d7be37136 | ||
|
|
761796c94a | ||
|
|
5dc4bb6360 | ||
|
|
e31cf4773b | ||
|
|
c2656995fc | ||
|
|
b185259561 | ||
|
|
80bf744a70 | ||
|
|
0e28021b3e | ||
|
|
980fa4afd6 | ||
|
|
b20d41ace8 | ||
|
|
24d07e2cac | ||
|
|
fbffdfe65e | ||
|
|
dd13e4e422 | ||
|
|
1372898ff2 | ||
|
|
f24458248a | ||
|
|
74ac97cfe4 | ||
|
|
7de4cb758d | ||
|
|
ed9784ee79 | ||
|
|
e14f66b2f1 | ||
|
|
862280a882 | ||
|
|
a3488f4ff9 | ||
|
|
0ee0a6ad7a | ||
|
|
6721929f22 | ||
|
|
372eda9455 | ||
|
|
eb38c038b2 | ||
|
|
83747b131b | ||
|
|
0e459b6bd6 | ||
|
|
805ea0af30 | ||
|
|
3dc4daceaf | ||
|
|
5a476fdfd9 | ||
|
|
ec90685324 | ||
|
|
15f753f05f | ||
|
|
ed0cdae329 | ||
|
|
7ef3ff6873 | ||
|
|
f00b9998d1 | ||
|
|
b0a1bfb0cb | ||
|
|
6e2a9b4b64 | ||
|
|
43da8e9e6b | ||
|
|
3c20ca18b6 | ||
|
|
d917f891e8 | ||
|
|
1fcddcba9b | ||
|
|
a871b084ec | ||
|
|
6fe96fddae | ||
|
|
05771a81d7 | ||
|
|
0b97659b38 | ||
|
|
4dd7e2135e | ||
|
|
9f09f50217 | ||
|
|
0dd51408a3 | ||
|
|
e7af332684 | ||
|
|
84f4f609b9 | ||
|
|
746a0e6463 | ||
|
|
54797a0be8 | ||
|
|
8d71f9597c | ||
|
|
3a1f1bffb0 | ||
|
|
64c77ee1bc | ||
|
|
4b421084e9 | ||
|
|
1899234f2e | ||
|
|
1bbae7d5a0 | ||
|
|
6cf723504f | ||
|
|
78cb066966 | ||
|
|
28b1aaa0c3 | ||
|
|
898f230081 | ||
|
|
ef9fd6bf50 | ||
|
|
fd81717f8e | ||
|
|
d8554fb550 | ||
|
|
91228df8a5 | ||
|
|
dd3c171a7a | ||
|
|
e4d2b1d194 | ||
|
|
dacd772d5e | ||
|
|
8b31e5fe41 | ||
|
|
54fd52d898 | ||
|
|
fe33e19bda | ||
|
|
dd872d6714 | ||
|
|
a331ae8059 | ||
|
|
5ecb26fd7d | ||
|
|
3f6dd0c796 | ||
|
|
ed9bb09d02 | ||
|
|
2d44350b44 | ||
|
|
84b1435df5 | ||
|
|
adb0c7444e |
56
.github/workflows/cont_integration.yml
vendored
56
.github/workflows/cont_integration.yml
vendored
@ -3,32 +3,31 @@ on: [push, pull_request]
|
||||
name: CI
|
||||
|
||||
jobs:
|
||||
|
||||
test-fmt:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_ELECTRUM_SERVER: electrum.blockstream.info:50001
|
||||
#TEST_ELECTRUM_SERVER: bitcoin.aranguren.org:50001
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- stable # STABLE
|
||||
- 1.75.0 # MSRV
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Install rustup
|
||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
- name: Set default toolchain
|
||||
run: $HOME/.cargo/bin/rustup default stable
|
||||
- name: Set profile
|
||||
run: $HOME/.cargo/bin/rustup set profile minimal
|
||||
- name: Fmt
|
||||
run: cargo fmt -- --check --verbose
|
||||
- name: Install rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ matrix.rust }}
|
||||
- name: Test
|
||||
run: cargo test --verbose --all-features
|
||||
- name: Setup iptables for the timeout test
|
||||
@ -41,3 +40,32 @@ jobs:
|
||||
- run: cargo check --verbose --no-default-features --features=minimal,debug-calls
|
||||
- run: cargo check --verbose --no-default-features --features=proxy,use-openssl
|
||||
- run: cargo check --verbose --no-default-features --features=proxy,use-rustls
|
||||
- run: cargo check --verbose --no-default-features --features=proxy,use-rustls-ring
|
||||
- run: cargo check --verbose --no-default-features --features=proxy,use-rustls,use-rustls-ring
|
||||
|
||||
fmt:
|
||||
name: Rust fmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
- name: Check fmt
|
||||
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||
|
||||
clippy_check:
|
||||
name: Rust clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@v1
|
||||
with:
|
||||
toolchain: 1.90.0
|
||||
components: clippy
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- run: cargo clippy --all-features --all-targets -- -D warnings
|
||||
|
||||
66
CHANGELOG.md
Normal file
66
CHANGELOG.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project can be found here and in each release's git tag and can be viewed with `git tag -ln100 "v*"`.
|
||||
|
||||
Contributors do not need to change this file but do need to add changelog details in their PR descriptions. The person making the next release will collect changelog details from included PRs and edit this file prior to each release.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.24.1]
|
||||
- Default to `ring` if multiple `rustls` features are set #183
|
||||
|
||||
## [0.24.0]
|
||||
- Use default `CryptoProvider` if available, otherwise install `rustls`'s `CryptoProvider` based on features #171
|
||||
- Add a new batch method for `blockchain.transaction.get_merkle` #170
|
||||
|
||||
## [0.23.1]
|
||||
- Fix batch request to Electrum servers out of order responses #160
|
||||
- Allow types that references to `ElectrumApi` to also implement it #163
|
||||
|
||||
## [0.23.0]
|
||||
|
||||
- Raise MSRV to `1.75` and bump `rustls` to `0.23.21` #159
|
||||
- Enforce min `rustls` version 0.23.19 to support MSRV with fix for RUSTSEC-2024-0399 #158
|
||||
|
||||
## [0.22.0]
|
||||
|
||||
- Updates the NoCertificateVerification implementation for the rustls::client::danger::ServerCertVerifier to use the rustls::SignatureScheme from CryptoProvider in use #150
|
||||
- Add `id_from_pos` support #155
|
||||
|
||||
## [0.21.0]
|
||||
|
||||
- Add use-rustls-ring feature #135
|
||||
- refactor: make validate_merkle_proof more efficient #134
|
||||
- chore: set rust edition to 2021, fix clippy, add ci fmt and clippy checks #139
|
||||
|
||||
## [0.20.0]
|
||||
|
||||
- Upgrade rustls to 0.23 #132
|
||||
- chore(deps): upgrade rust-bitcoin to 0.32.0 #133
|
||||
- ci: add test with MSRV 1.63.0 #128
|
||||
|
||||
## [0.19.0]
|
||||
|
||||
- Add Batch::raw and improve docs #94
|
||||
- Remove webpki and bump webpki-roots to v0.25 #117
|
||||
- Upgrade rust-bitcoin to v0.31.0 #121
|
||||
- Add utility to validate GetMerkleRes #122
|
||||
- Enforce timeout on initial socks5 proxy connection #125
|
||||
|
||||
## [0.18.0]
|
||||
|
||||
- Revert "errors if expecting headers notification but not subscribed" #115
|
||||
|
||||
[0.18.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.17.0...0.18.0
|
||||
[0.19.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.18.0...0.19.0
|
||||
[0.20.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.19.0...0.20.0
|
||||
[0.21.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.20.0...0.21.0
|
||||
[0.22.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.21.0...0.22.0
|
||||
[0.23.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.22.0...0.23.0
|
||||
[0.23.1]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.23.0...0.23.1
|
||||
[0.24.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.23.1...0.24.0
|
||||
[0.24.1]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.24.0...0.24.1
|
||||
[Unreleased]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.24.1...HEAD
|
||||
19
Cargo.toml
19
Cargo.toml
@ -1,14 +1,16 @@
|
||||
[package]
|
||||
name = "electrum-client"
|
||||
version = "0.17.0"
|
||||
version = "0.24.1"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
|
||||
license = "MIT"
|
||||
homepage = "https://github.com/MagicalBitcoin/rust-electrum-client"
|
||||
repository = "https://github.com/MagicalBitcoin/rust-electrum-client"
|
||||
homepage = "https://github.com/bitcoindevkit/rust-electrum-client"
|
||||
repository = "https://github.com/bitcoindevkit/rust-electrum-client"
|
||||
documentation = "https://docs.rs/electrum-client/"
|
||||
description = "Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers."
|
||||
keywords = ["bitcoin", "electrum"]
|
||||
readme = "README.md"
|
||||
rust-version = "1.75.0"
|
||||
edition = "2021"
|
||||
|
||||
# loosely based on https://github.com/evgeniy-scherbina/rust-electrumx-client
|
||||
|
||||
@ -18,16 +20,14 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
log = "^0.4"
|
||||
bitcoin = { version = "^0.30", features = ["serde"] }
|
||||
bitcoin-private = "0.1.0"
|
||||
bitcoin = { version = "0.32", features = ["serde"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
|
||||
# Optional dependencies
|
||||
openssl = { version = "0.10", optional = true }
|
||||
rustls = { version = "0.21", optional = true, features = ["dangerous_configuration"] }
|
||||
webpki = { version = "0.22", optional = true }
|
||||
webpki-roots = { version = "0.22", optional = true }
|
||||
rustls = { version = "0.23.21", optional = true, default-features = false }
|
||||
webpki-roots = { version = "0.25", optional = true }
|
||||
|
||||
byteorder = { version = "1.0", optional = true }
|
||||
|
||||
@ -42,5 +42,6 @@ default = ["proxy", "use-rustls"]
|
||||
minimal = []
|
||||
debug-calls = []
|
||||
proxy = ["byteorder", "winapi", "libc"]
|
||||
use-rustls = ["webpki", "webpki-roots", "rustls"]
|
||||
use-rustls = ["webpki-roots", "rustls/default"]
|
||||
use-rustls-ring = ["webpki-roots", "rustls/ring", "rustls/logging", "rustls/std", "rustls/tls12"]
|
||||
use-openssl = ["openssl"]
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
# rust-electrum-client [![Build Status]][GitHub Workflow] [![Latest Version]][crates.io]
|
||||
# rust-electrum-client
|
||||
[![Build Status]][GitHub Workflow] [![Latest Version]][crates.io] [![MSRV Badge]][Rust Blog]
|
||||
|
||||
[Build Status]: https://github.com/bitcoindevkit/rust-electrum-client/actions/workflows/cont_integration.yml/badge.svg
|
||||
[GitHub Workflow]: https://github.com/bitcoindevkit/rust-electrum-client/actions?query=workflow%3ACI
|
||||
[Latest Version]: https://img.shields.io/crates/v/electrum-client.svg
|
||||
[crates.io]: https://crates.io/crates/electrum-client
|
||||
[MSRV Badge]: https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg
|
||||
[Rust Blog]: https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html
|
||||
|
||||
Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers.
|
||||
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
|
||||
This library should compile with any combination of features with Rust 1.75.0.
|
||||
|
||||
1
clippy.toml
Normal file
1
clippy.toml
Normal file
@ -0,0 +1 @@
|
||||
msrv="1.75.0"
|
||||
34
justfile
Normal file
34
justfile
Normal file
@ -0,0 +1,34 @@
|
||||
alias b := build
|
||||
alias c := check
|
||||
alias f := fmt
|
||||
alias t := test
|
||||
alias p := pre-push
|
||||
|
||||
_default:
|
||||
@just --list
|
||||
|
||||
# Build the project
|
||||
build:
|
||||
cargo build
|
||||
|
||||
# Check code: formatting, compilation, linting, doc comments, and commit signature
|
||||
check:
|
||||
cargo +nightly fmt --all -- --check
|
||||
cargo check --all-features --all-targets
|
||||
cargo clippy --all-features --all-targets -- -D warnings
|
||||
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --no-deps
|
||||
@[ "$(git log --pretty='format:%G?' -1 HEAD)" = "N" ] && \
|
||||
echo "\n⚠️ Unsigned commit: BDK requires that commits be signed." || \
|
||||
true
|
||||
|
||||
# Format all code
|
||||
fmt:
|
||||
cargo +nightly fmt
|
||||
|
||||
# Run all tests on the workspace with all features
|
||||
test:
|
||||
cargo test --all-features -- --test-threads=1
|
||||
|
||||
# Run pre-push suite: format, check, and test
|
||||
pre-push: fmt check test
|
||||
|
||||
421
src/api.rs
421
src/api.rs
@ -2,12 +2,185 @@
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::convert::TryInto;
|
||||
use std::ops::Deref;
|
||||
|
||||
use bitcoin::consensus::encode::{deserialize, serialize};
|
||||
use bitcoin::{block, Script, Transaction, Txid};
|
||||
|
||||
use batch::Batch;
|
||||
use types::*;
|
||||
use crate::batch::Batch;
|
||||
use crate::types::*;
|
||||
|
||||
impl<E: Deref> ElectrumApi for E
|
||||
where
|
||||
E::Target: ElectrumApi,
|
||||
{
|
||||
fn raw_call(
|
||||
&self,
|
||||
method_name: &str,
|
||||
params: impl IntoIterator<Item = Param>,
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
(**self).raw_call(method_name, params)
|
||||
}
|
||||
|
||||
fn batch_call(&self, batch: &Batch) -> Result<Vec<serde_json::Value>, Error> {
|
||||
(**self).batch_call(batch)
|
||||
}
|
||||
|
||||
fn block_headers_subscribe_raw(&self) -> Result<RawHeaderNotification, Error> {
|
||||
(**self).block_headers_subscribe_raw()
|
||||
}
|
||||
|
||||
fn block_headers_pop_raw(&self) -> Result<Option<RawHeaderNotification>, Error> {
|
||||
(**self).block_headers_pop_raw()
|
||||
}
|
||||
|
||||
fn block_header_raw(&self, height: usize) -> Result<Vec<u8>, Error> {
|
||||
(**self).block_header_raw(height)
|
||||
}
|
||||
|
||||
fn block_headers(&self, start_height: usize, count: usize) -> Result<GetHeadersRes, Error> {
|
||||
(**self).block_headers(start_height, count)
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, number: usize) -> Result<f64, Error> {
|
||||
(**self).estimate_fee(number)
|
||||
}
|
||||
|
||||
fn relay_fee(&self) -> Result<f64, Error> {
|
||||
(**self).relay_fee()
|
||||
}
|
||||
|
||||
fn script_subscribe(&self, script: &Script) -> Result<Option<ScriptStatus>, Error> {
|
||||
(**self).script_subscribe(script)
|
||||
}
|
||||
|
||||
fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result<Vec<Option<ScriptStatus>>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<&'s Script>,
|
||||
{
|
||||
(**self).batch_script_subscribe(scripts)
|
||||
}
|
||||
|
||||
fn script_unsubscribe(&self, script: &Script) -> Result<bool, Error> {
|
||||
(**self).script_unsubscribe(script)
|
||||
}
|
||||
|
||||
fn script_pop(&self, script: &Script) -> Result<Option<ScriptStatus>, Error> {
|
||||
(**self).script_pop(script)
|
||||
}
|
||||
|
||||
fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes, Error> {
|
||||
(**self).script_get_balance(script)
|
||||
}
|
||||
|
||||
fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result<Vec<GetBalanceRes>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<&'s Script>,
|
||||
{
|
||||
(**self).batch_script_get_balance(scripts)
|
||||
}
|
||||
|
||||
fn script_get_history(&self, script: &Script) -> Result<Vec<GetHistoryRes>, Error> {
|
||||
(**self).script_get_history(script)
|
||||
}
|
||||
|
||||
fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result<Vec<Vec<GetHistoryRes>>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<&'s Script>,
|
||||
{
|
||||
(**self).batch_script_get_history(scripts)
|
||||
}
|
||||
|
||||
fn script_list_unspent(&self, script: &Script) -> Result<Vec<ListUnspentRes>, Error> {
|
||||
(**self).script_list_unspent(script)
|
||||
}
|
||||
|
||||
fn batch_script_list_unspent<'s, I>(
|
||||
&self,
|
||||
scripts: I,
|
||||
) -> Result<Vec<Vec<ListUnspentRes>>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<&'s Script>,
|
||||
{
|
||||
(**self).batch_script_list_unspent(scripts)
|
||||
}
|
||||
|
||||
fn transaction_get_raw(&self, txid: &Txid) -> Result<Vec<u8>, Error> {
|
||||
(**self).transaction_get_raw(txid)
|
||||
}
|
||||
|
||||
fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result<Vec<Vec<u8>>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<&'t Txid>,
|
||||
{
|
||||
(**self).batch_transaction_get_raw(txids)
|
||||
}
|
||||
|
||||
fn batch_block_header_raw<I>(&self, heights: I) -> Result<Vec<Vec<u8>>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<u32>,
|
||||
{
|
||||
(**self).batch_block_header_raw(heights)
|
||||
}
|
||||
|
||||
fn batch_estimate_fee<I>(&self, numbers: I) -> Result<Vec<f64>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<usize>,
|
||||
{
|
||||
(**self).batch_estimate_fee(numbers)
|
||||
}
|
||||
|
||||
fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result<Txid, Error> {
|
||||
(**self).transaction_broadcast_raw(raw_tx)
|
||||
}
|
||||
|
||||
fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result<GetMerkleRes, Error> {
|
||||
(**self).transaction_get_merkle(txid, height)
|
||||
}
|
||||
|
||||
fn batch_transaction_get_merkle<I>(
|
||||
&self,
|
||||
txids_and_heights: I,
|
||||
) -> Result<Vec<GetMerkleRes>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<(Txid, usize)>,
|
||||
{
|
||||
(**self).batch_transaction_get_merkle(txids_and_heights)
|
||||
}
|
||||
|
||||
fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result<Txid, Error> {
|
||||
(**self).txid_from_pos(height, tx_pos)
|
||||
}
|
||||
|
||||
fn txid_from_pos_with_merkle(
|
||||
&self,
|
||||
height: usize,
|
||||
tx_pos: usize,
|
||||
) -> Result<TxidFromPosRes, Error> {
|
||||
(**self).txid_from_pos_with_merkle(height, tx_pos)
|
||||
}
|
||||
|
||||
fn server_features(&self) -> Result<ServerFeaturesRes, Error> {
|
||||
(**self).server_features()
|
||||
}
|
||||
|
||||
fn ping(&self) -> Result<(), Error> {
|
||||
(**self).ping()
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug-calls")]
|
||||
fn calls_made(&self) -> Result<usize, Error> {
|
||||
(**self).calls_made()
|
||||
}
|
||||
}
|
||||
|
||||
/// API calls exposed by an Electrum client
|
||||
pub trait ElectrumApi {
|
||||
@ -200,6 +373,28 @@ pub trait ElectrumApi {
|
||||
/// Returns the merkle path for the transaction `txid` confirmed in the block at `height`.
|
||||
fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result<GetMerkleRes, Error>;
|
||||
|
||||
/// Batch version of [`transaction_get_merkle`](#method.transaction_get_merkle).
|
||||
///
|
||||
/// Take a list of `(txid, height)`, for transactions with `txid` confirmed in the block at `height`.
|
||||
fn batch_transaction_get_merkle<I>(
|
||||
&self,
|
||||
txids_and_heights: I,
|
||||
) -> Result<Vec<GetMerkleRes>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<(Txid, usize)>;
|
||||
|
||||
/// Returns a transaction hash, given a block `height` and a `tx_pos` in the block.
|
||||
fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result<Txid, Error>;
|
||||
|
||||
/// Returns a transaction hash and a merkle path, given a block `height` and a `tx_pos` in the
|
||||
/// block.
|
||||
fn txid_from_pos_with_merkle(
|
||||
&self,
|
||||
height: usize,
|
||||
tx_pos: usize,
|
||||
) -> Result<TxidFromPosRes, Error>;
|
||||
|
||||
/// Returns the capabilities of the server.
|
||||
fn server_features(&self) -> Result<ServerFeaturesRes, Error>;
|
||||
|
||||
@ -211,3 +406,225 @@ pub trait ElectrumApi {
|
||||
/// Returns the number of network calls made since the creation of the client.
|
||||
fn calls_made(&self) -> Result<usize, Error>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use super::ElectrumApi;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct FakeApi;
|
||||
|
||||
impl ElectrumApi for FakeApi {
|
||||
fn raw_call(
|
||||
&self,
|
||||
_: &str,
|
||||
_: impl IntoIterator<Item = super::Param>,
|
||||
) -> Result<serde_json::Value, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_call(&self, _: &crate::Batch) -> Result<Vec<serde_json::Value>, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn block_headers_subscribe_raw(
|
||||
&self,
|
||||
) -> Result<super::RawHeaderNotification, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn block_headers_pop_raw(
|
||||
&self,
|
||||
) -> Result<Option<super::RawHeaderNotification>, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn block_header_raw(&self, _: usize) -> Result<Vec<u8>, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn block_headers(&self, _: usize, _: usize) -> Result<super::GetHeadersRes, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn estimate_fee(&self, _: usize) -> Result<f64, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn relay_fee(&self) -> Result<f64, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn script_subscribe(
|
||||
&self,
|
||||
_: &bitcoin::Script,
|
||||
) -> Result<Option<super::ScriptStatus>, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_script_subscribe<'s, I>(
|
||||
&self,
|
||||
_: I,
|
||||
) -> Result<Vec<Option<super::ScriptStatus>>, super::Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: std::borrow::Borrow<&'s bitcoin::Script>,
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn script_unsubscribe(&self, _: &bitcoin::Script) -> Result<bool, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn script_pop(
|
||||
&self,
|
||||
_: &bitcoin::Script,
|
||||
) -> Result<Option<super::ScriptStatus>, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn script_get_balance(
|
||||
&self,
|
||||
_: &bitcoin::Script,
|
||||
) -> Result<super::GetBalanceRes, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_script_get_balance<'s, I>(
|
||||
&self,
|
||||
_: I,
|
||||
) -> Result<Vec<super::GetBalanceRes>, super::Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: std::borrow::Borrow<&'s bitcoin::Script>,
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn script_get_history(
|
||||
&self,
|
||||
_: &bitcoin::Script,
|
||||
) -> Result<Vec<super::GetHistoryRes>, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_script_get_history<'s, I>(
|
||||
&self,
|
||||
_: I,
|
||||
) -> Result<Vec<Vec<super::GetHistoryRes>>, super::Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: std::borrow::Borrow<&'s bitcoin::Script>,
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn script_list_unspent(
|
||||
&self,
|
||||
_: &bitcoin::Script,
|
||||
) -> Result<Vec<super::ListUnspentRes>, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_script_list_unspent<'s, I>(
|
||||
&self,
|
||||
_: I,
|
||||
) -> Result<Vec<Vec<super::ListUnspentRes>>, super::Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: std::borrow::Borrow<&'s bitcoin::Script>,
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn transaction_get_raw(&self, _: &bitcoin::Txid) -> Result<Vec<u8>, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_transaction_get_raw<'t, I>(&self, _: I) -> Result<Vec<Vec<u8>>, super::Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: std::borrow::Borrow<&'t bitcoin::Txid>,
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_block_header_raw<I>(&self, _: I) -> Result<Vec<Vec<u8>>, super::Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: std::borrow::Borrow<u32>,
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_estimate_fee<I>(&self, _: I) -> Result<Vec<f64>, super::Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: std::borrow::Borrow<usize>,
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn transaction_broadcast_raw(&self, _: &[u8]) -> Result<bitcoin::Txid, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn transaction_get_merkle(
|
||||
&self,
|
||||
_: &bitcoin::Txid,
|
||||
_: usize,
|
||||
) -> Result<super::GetMerkleRes, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn batch_transaction_get_merkle<I>(
|
||||
&self,
|
||||
_: I,
|
||||
) -> Result<Vec<crate::GetMerkleRes>, crate::Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: std::borrow::Borrow<(bitcoin::Txid, usize)>,
|
||||
{
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn txid_from_pos(&self, _: usize, _: usize) -> Result<bitcoin::Txid, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn txid_from_pos_with_merkle(
|
||||
&self,
|
||||
_: usize,
|
||||
_: usize,
|
||||
) -> Result<super::TxidFromPosRes, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn server_features(&self) -> Result<super::ServerFeaturesRes, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn ping(&self) -> Result<(), super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[cfg(feature = "debug-calls")]
|
||||
fn calls_made(&self) -> Result<usize, super::Error> {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_impl<A: ElectrumApi>() {}
|
||||
|
||||
#[test]
|
||||
fn deref() {
|
||||
is_impl::<FakeApi>();
|
||||
is_impl::<&FakeApi>();
|
||||
is_impl::<Arc<FakeApi>>();
|
||||
is_impl::<Box<FakeApi>>();
|
||||
is_impl::<Cow<FakeApi>>();
|
||||
}
|
||||
}
|
||||
|
||||
27
src/batch.rs
27
src/batch.rs
@ -4,7 +4,7 @@
|
||||
|
||||
use bitcoin::{Script, Txid};
|
||||
|
||||
use types::{Call, Param, ToElectrumScriptHash};
|
||||
use crate::types::{Call, Param, ToElectrumScriptHash};
|
||||
|
||||
/// Helper structure that caches all the requests before they are actually sent to the server.
|
||||
///
|
||||
@ -16,11 +16,17 @@ use types::{Call, Param, ToElectrumScriptHash};
|
||||
/// [`Client`](../client/struct.Client.html), like
|
||||
/// [`batch_script_get_balance`](../client/struct.Client.html#method.batch_script_get_balance) to ask the
|
||||
/// server for the balance of multiple scripts with a single request.
|
||||
#[derive(Default)]
|
||||
pub struct Batch {
|
||||
calls: Vec<Call>,
|
||||
}
|
||||
|
||||
impl Batch {
|
||||
/// Add a raw request to the batch queue
|
||||
pub fn raw(&mut self, method: String, params: Vec<Param>) {
|
||||
self.calls.push((method, params));
|
||||
}
|
||||
|
||||
/// Add one `blockchain.scripthash.listunspent` request to the batch queue
|
||||
pub fn script_list_unspent(&mut self, script: &Script) {
|
||||
let params = vec![Param::String(script.to_electrum_scripthash().to_hex())];
|
||||
@ -56,6 +62,17 @@ impl Batch {
|
||||
.push((String::from("blockchain.transaction.get"), params));
|
||||
}
|
||||
|
||||
/// Add one `blockchain.transaction.get_merkle` request to the batch queue
|
||||
pub fn transaction_get_merkle(&mut self, tx_hash_and_height: &(Txid, usize)) {
|
||||
let (tx_hash, height) = tx_hash_and_height;
|
||||
let params = vec![
|
||||
Param::String(format!("{:x}", tx_hash)),
|
||||
Param::Usize(*height),
|
||||
];
|
||||
self.calls
|
||||
.push((String::from("blockchain.transaction.get_merkle"), params));
|
||||
}
|
||||
|
||||
/// Add one `blockchain.estimatefee` request to the batch queue
|
||||
pub fn estimate_fee(&mut self, number: usize) {
|
||||
let params = vec![Param::Usize(number)];
|
||||
@ -71,7 +88,7 @@ impl Batch {
|
||||
}
|
||||
|
||||
/// Returns an iterator on the batch
|
||||
pub fn iter(&self) -> BatchIter {
|
||||
pub fn iter(&self) -> BatchIter<'_> {
|
||||
BatchIter {
|
||||
batch: self,
|
||||
index: 0,
|
||||
@ -102,9 +119,3 @@ impl<'a> std::iter::Iterator for BatchIter<'a> {
|
||||
val
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for Batch {
|
||||
fn default() -> Self {
|
||||
Batch { calls: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,12 +6,12 @@ use log::{info, warn};
|
||||
|
||||
use bitcoin::{Script, Txid};
|
||||
|
||||
use api::ElectrumApi;
|
||||
use batch::Batch;
|
||||
use config::Config;
|
||||
use raw_client::*;
|
||||
use crate::api::ElectrumApi;
|
||||
use crate::batch::Batch;
|
||||
use crate::config::Config;
|
||||
use crate::raw_client::*;
|
||||
use crate::types::*;
|
||||
use std::convert::TryFrom;
|
||||
use types::*;
|
||||
|
||||
/// Generalized Electrum client that supports multiple backends. This wraps
|
||||
/// [`RawClient`](client/struct.RawClient.html) and provides a more user-friendly
|
||||
@ -49,7 +49,7 @@ macro_rules! impl_inner_call {
|
||||
drop(read_client);
|
||||
match res {
|
||||
Ok(val) => return Ok(val),
|
||||
Err(Error::Protocol(_) | Error::AlreadySubscribed(_) | Error::NotSubscribed(_) | Error::NotSubscribedToHeaders) => {
|
||||
Err(Error::Protocol(_) | Error::AlreadySubscribed(_)) => {
|
||||
return res;
|
||||
},
|
||||
Err(e) => {
|
||||
@ -148,7 +148,6 @@ impl Client {
|
||||
/// If no prefix is specified, then `tcp://` is assumed.
|
||||
///
|
||||
/// See [Client::from_config] for more configuration options
|
||||
///
|
||||
pub fn new(url: &str) -> Result<Self, Error> {
|
||||
Self::from_config(url, Config::default())
|
||||
}
|
||||
@ -328,6 +327,36 @@ impl ElectrumApi for Client {
|
||||
impl_inner_call!(self, transaction_get_merkle, txid, height)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn batch_transaction_get_merkle<I>(
|
||||
&self,
|
||||
txids_and_heights: I,
|
||||
) -> Result<Vec<GetMerkleRes>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<(Txid, usize)>,
|
||||
{
|
||||
impl_inner_call!(
|
||||
self,
|
||||
batch_transaction_get_merkle,
|
||||
txids_and_heights.clone()
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result<Txid, Error> {
|
||||
impl_inner_call!(self, txid_from_pos, height, tx_pos)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn txid_from_pos_with_merkle(
|
||||
&self,
|
||||
height: usize,
|
||||
tx_pos: usize,
|
||||
) -> Result<TxidFromPosRes, Error> {
|
||||
impl_inner_call!(self, txid_from_pos_with_merkle, height, tx_pos)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn server_features(&self) -> Result<ServerFeaturesRes, Error> {
|
||||
impl_inner_call!(self, server_features)
|
||||
@ -353,7 +382,7 @@ mod tests {
|
||||
fn more_failed_attempts_than_retries_means_exhausted() {
|
||||
let exhausted = retries_exhausted(10, 5);
|
||||
|
||||
assert_eq!(exhausted, true)
|
||||
assert!(exhausted)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -362,21 +391,21 @@ mod tests {
|
||||
|
||||
let exhausted = retries_exhausted(failed_attempts, u8::MAX);
|
||||
|
||||
assert_eq!(exhausted, true)
|
||||
assert!(exhausted)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn less_failed_attempts_means_not_exhausted() {
|
||||
let exhausted = retries_exhausted(2, 5);
|
||||
|
||||
assert_eq!(exhausted, false)
|
||||
assert!(!exhausted)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attempts_equals_retries_means_not_exhausted_yet() {
|
||||
let exhausted = retries_exhausted(2, 2);
|
||||
|
||||
assert_eq!(exhausted, false)
|
||||
assert!(!exhausted)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -408,7 +437,7 @@ mod tests {
|
||||
sender.send(()).unwrap();
|
||||
|
||||
for _stream in listener.incoming() {
|
||||
loop {}
|
||||
std::thread::sleep(Duration::from_secs(60))
|
||||
}
|
||||
});
|
||||
|
||||
@ -419,7 +448,9 @@ mod tests {
|
||||
let now = Instant::now();
|
||||
let client = Client::from_config(
|
||||
&endpoint,
|
||||
crate::config::ConfigBuilder::new().timeout(Some(5)).build(),
|
||||
crate::config::ConfigBuilder::new()
|
||||
.timeout(Some(Duration::from_secs(5)))
|
||||
.build(),
|
||||
);
|
||||
let elapsed = now.elapsed();
|
||||
|
||||
|
||||
@ -1,5 +1,11 @@
|
||||
use std::time::Duration;
|
||||
|
||||
/// Configuration for an electrum client
|
||||
///
|
||||
/// Refer to [`Client::from_config`] and [`ClientType::from_config`].
|
||||
///
|
||||
/// [`Client::from_config`]: crate::Client::from_config
|
||||
/// [`ClientType::from_config`]: crate::ClientType::from_config
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
/// Proxy socks5 configuration, default None
|
||||
@ -49,8 +55,8 @@ impl ConfigBuilder {
|
||||
}
|
||||
|
||||
/// Sets the timeout
|
||||
pub fn timeout(mut self, timeout: Option<u8>) -> Self {
|
||||
self.config.timeout = timeout.map(|t| Duration::from_secs(t as u64));
|
||||
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
|
||||
self.config.timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
@ -97,19 +103,35 @@ impl Socks5Config {
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Get the configuration for `socks5`
|
||||
///
|
||||
/// Set this with [`ConfigBuilder::socks5`]
|
||||
pub fn socks5(&self) -> &Option<Socks5Config> {
|
||||
&self.socks5
|
||||
}
|
||||
|
||||
/// Get the configuration for `retry`
|
||||
///
|
||||
/// Set this with [`ConfigBuilder::retry`]
|
||||
pub fn retry(&self) -> u8 {
|
||||
self.retry
|
||||
}
|
||||
|
||||
/// Get the configuration for `timeout`
|
||||
///
|
||||
/// Set this with [`ConfigBuilder::timeout`]
|
||||
pub fn timeout(&self) -> Option<Duration> {
|
||||
self.timeout
|
||||
}
|
||||
|
||||
/// Get the configuration for `validate_domain`
|
||||
///
|
||||
/// Set this with [`ConfigBuilder::validate_domain`]
|
||||
pub fn validate_domain(&self) -> bool {
|
||||
self.validate_domain
|
||||
}
|
||||
|
||||
/// Convenience method for calling [`ConfigBuilder::new`]
|
||||
pub fn builder() -> ConfigBuilder {
|
||||
ConfigBuilder::new()
|
||||
}
|
||||
|
||||
22
src/lib.rs
22
src/lib.rs
@ -20,22 +20,27 @@
|
||||
//! ```
|
||||
|
||||
pub extern crate bitcoin;
|
||||
extern crate bitcoin_private;
|
||||
extern crate core;
|
||||
extern crate log;
|
||||
#[cfg(feature = "use-openssl")]
|
||||
extern crate openssl;
|
||||
#[cfg(all(
|
||||
any(feature = "default", feature = "use-rustls"),
|
||||
any(
|
||||
feature = "default",
|
||||
feature = "use-rustls",
|
||||
feature = "use-rustls-ring"
|
||||
),
|
||||
not(feature = "use-openssl")
|
||||
))]
|
||||
extern crate rustls;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(any(feature = "use-rustls", feature = "default"))]
|
||||
extern crate webpki;
|
||||
#[cfg(any(feature = "use-rustls", feature = "default"))]
|
||||
#[cfg(any(
|
||||
feature = "default",
|
||||
feature = "use-rustls",
|
||||
feature = "use-rustls-ring"
|
||||
))]
|
||||
extern crate webpki_roots;
|
||||
|
||||
#[cfg(any(feature = "default", feature = "proxy"))]
|
||||
@ -54,7 +59,8 @@ mod batch;
|
||||
|
||||
#[cfg(any(
|
||||
all(feature = "proxy", feature = "use-openssl"),
|
||||
all(feature = "proxy", feature = "use-rustls")
|
||||
all(feature = "proxy", feature = "use-rustls"),
|
||||
all(feature = "proxy", feature = "use-rustls-ring")
|
||||
))]
|
||||
pub mod client;
|
||||
|
||||
@ -63,12 +69,14 @@ mod config;
|
||||
pub mod raw_client;
|
||||
mod stream;
|
||||
mod types;
|
||||
pub mod utils;
|
||||
|
||||
pub use api::ElectrumApi;
|
||||
pub use batch::Batch;
|
||||
#[cfg(any(
|
||||
all(feature = "proxy", feature = "use-openssl"),
|
||||
all(feature = "proxy", feature = "use-rustls")
|
||||
all(feature = "proxy", feature = "use-rustls"),
|
||||
all(feature = "proxy", feature = "use-rustls-ring")
|
||||
))]
|
||||
pub use client::*;
|
||||
pub use config::{Config, ConfigBuilder, Socks5Config};
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
//! This module contains the definition of the raw client that wraps the transport method
|
||||
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, VecDeque};
|
||||
use std::collections::{BTreeMap, HashMap, VecDeque};
|
||||
use std::io::{BufRead, BufReader, Read, Write};
|
||||
use std::mem::drop;
|
||||
use std::net::{TcpStream, ToSocketAddrs};
|
||||
@ -16,28 +16,34 @@ use std::time::Duration;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::hex::{DisplayHex, FromHex};
|
||||
use bitcoin::{Script, Txid};
|
||||
use bitcoin_private::hex::exts::DisplayHex;
|
||||
|
||||
#[cfg(feature = "use-openssl")]
|
||||
use openssl::ssl::{SslConnector, SslMethod, SslStream, SslVerifyMode};
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "default", feature = "use-rustls"),
|
||||
any(
|
||||
feature = "default",
|
||||
feature = "use-rustls",
|
||||
feature = "use-rustls-ring"
|
||||
),
|
||||
not(feature = "use-openssl")
|
||||
))]
|
||||
use rustls::{
|
||||
ClientConfig, ClientConnection, OwnedTrustAnchor, RootCertStore, ServerName, StreamOwned,
|
||||
pki_types::ServerName,
|
||||
pki_types::{Der, TrustAnchor},
|
||||
ClientConfig, ClientConnection, RootCertStore, StreamOwned,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "default", feature = "proxy"))]
|
||||
use crate::socks::{Socks5Stream, TargetAddr, ToTargetAddr};
|
||||
|
||||
use stream::ClonableStream;
|
||||
use crate::stream::ClonableStream;
|
||||
|
||||
use api::ElectrumApi;
|
||||
use batch::Batch;
|
||||
use types::*;
|
||||
use crate::api::ElectrumApi;
|
||||
use crate::batch::Batch;
|
||||
use crate::types::*;
|
||||
|
||||
macro_rules! impl_batch_call {
|
||||
( $self:expr, $data:expr, $call:ident ) => {{
|
||||
@ -77,7 +83,7 @@ pub trait ToSocketAddrsDomain: ToSocketAddrs {
|
||||
|
||||
impl ToSocketAddrsDomain for &str {
|
||||
fn domain(&self) -> Option<&str> {
|
||||
self.splitn(2, ':').next()
|
||||
self.split(':').next()
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,7 +139,7 @@ where
|
||||
last_id: AtomicUsize,
|
||||
waiting_map: Mutex<HashMap<usize, Sender<ChannelMessage>>>,
|
||||
|
||||
headers: Mutex<Option<VecDeque<RawHeaderNotification>>>,
|
||||
headers: Mutex<VecDeque<RawHeaderNotification>>,
|
||||
script_notifications: Mutex<HashMap<ScriptHash, VecDeque<ScriptStatus>>>,
|
||||
|
||||
#[cfg(feature = "debug-calls")]
|
||||
@ -154,7 +160,7 @@ where
|
||||
last_id: AtomicUsize::new(0),
|
||||
waiting_map: Mutex::new(HashMap::new()),
|
||||
|
||||
headers: Mutex::new(None),
|
||||
headers: Mutex::new(VecDeque::new()),
|
||||
script_notifications: Mutex::new(HashMap::new()),
|
||||
|
||||
#[cfg(feature = "debug-calls")]
|
||||
@ -284,40 +290,81 @@ impl RawClient<ElectrumSslStream> {
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "default", feature = "use-rustls"),
|
||||
any(
|
||||
feature = "default",
|
||||
feature = "use-rustls",
|
||||
feature = "use-rustls-ring"
|
||||
),
|
||||
not(feature = "use-openssl")
|
||||
))]
|
||||
mod danger {
|
||||
use rustls;
|
||||
use rustls::client::ServerCertVerified;
|
||||
use rustls::{Certificate, Error, ServerName};
|
||||
use std::time::SystemTime;
|
||||
use crate::raw_client::ServerName;
|
||||
use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified};
|
||||
use rustls::crypto::CryptoProvider;
|
||||
use rustls::pki_types::{CertificateDer, UnixTime};
|
||||
use rustls::DigitallySignedStruct;
|
||||
|
||||
pub struct NoCertificateVerification {}
|
||||
#[derive(Debug)]
|
||||
pub struct NoCertificateVerification(CryptoProvider);
|
||||
|
||||
impl rustls::client::ServerCertVerifier for NoCertificateVerification {
|
||||
impl NoCertificateVerification {
|
||||
pub fn new(provider: CryptoProvider) -> Self {
|
||||
Self(provider)
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &Certificate,
|
||||
_intermediates: &[Certificate],
|
||||
_server_name: &ServerName,
|
||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: SystemTime,
|
||||
) -> Result<ServerCertVerified, Error> {
|
||||
_end_entity: &CertificateDer<'_>,
|
||||
_intermediates: &[CertificateDer<'_>],
|
||||
_server_name: &ServerName<'_>,
|
||||
_ocsp: &[u8],
|
||||
_now: UnixTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls12_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn verify_tls13_signature(
|
||||
&self,
|
||||
_message: &[u8],
|
||||
_cert: &CertificateDer<'_>,
|
||||
_dss: &DigitallySignedStruct,
|
||||
) -> Result<HandshakeSignatureValid, rustls::Error> {
|
||||
Ok(HandshakeSignatureValid::assertion())
|
||||
}
|
||||
|
||||
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
|
||||
self.0.signature_verification_algorithms.supported_schemes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(
|
||||
any(feature = "default", feature = "use-rustls"),
|
||||
any(
|
||||
feature = "default",
|
||||
feature = "use-rustls",
|
||||
feature = "use-rustls-ring"
|
||||
),
|
||||
not(feature = "use-openssl")
|
||||
))]
|
||||
/// Transport type used to establish a Rustls TLS encrypted/authenticated connection with the server
|
||||
pub type ElectrumSslStream = StreamOwned<ClientConnection, TcpStream>;
|
||||
#[cfg(all(
|
||||
any(feature = "default", feature = "use-rustls"),
|
||||
any(
|
||||
feature = "default",
|
||||
feature = "use-rustls",
|
||||
feature = "use-rustls-ring"
|
||||
),
|
||||
not(feature = "use-openssl")
|
||||
))]
|
||||
impl RawClient<ElectrumSslStream> {
|
||||
@ -359,26 +406,53 @@ impl RawClient<ElectrumSslStream> {
|
||||
) -> Result<Self, Error> {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
let builder = ClientConfig::builder().with_safe_defaults();
|
||||
if rustls::crypto::CryptoProvider::get_default().is_none() {
|
||||
// We install a crypto provider depending on the set feature.
|
||||
#[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))]
|
||||
rustls::crypto::CryptoProvider::install_default(
|
||||
rustls::crypto::aws_lc_rs::default_provider(),
|
||||
)
|
||||
.map_err(|_| {
|
||||
Error::CouldNotCreateConnection(rustls::Error::General(
|
||||
"Failed to install CryptoProvider".to_string(),
|
||||
))
|
||||
})?;
|
||||
|
||||
#[cfg(feature = "use-rustls-ring")]
|
||||
rustls::crypto::CryptoProvider::install_default(
|
||||
rustls::crypto::ring::default_provider(),
|
||||
)
|
||||
.map_err(|_| {
|
||||
Error::CouldNotCreateConnection(rustls::Error::General(
|
||||
"Failed to install CryptoProvider".to_string(),
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
let builder = ClientConfig::builder();
|
||||
|
||||
let config = if validate_domain {
|
||||
socket_addr.domain().ok_or(Error::MissingDomain)?;
|
||||
|
||||
let mut store = RootCertStore::empty();
|
||||
store.add_server_trust_anchors(webpki_roots::TLS_SERVER_ROOTS.0.into_iter().map(|t| {
|
||||
OwnedTrustAnchor::from_subject_spki_name_constraints(
|
||||
t.subject,
|
||||
t.spki,
|
||||
t.name_constraints,
|
||||
)
|
||||
}));
|
||||
let store = webpki_roots::TLS_SERVER_ROOTS
|
||||
.iter()
|
||||
.map(|t| TrustAnchor {
|
||||
subject: Der::from_slice(t.subject),
|
||||
subject_public_key_info: Der::from_slice(t.spki),
|
||||
name_constraints: t.name_constraints.map(Der::from_slice),
|
||||
})
|
||||
.collect::<RootCertStore>();
|
||||
|
||||
// TODO: cert pinning
|
||||
builder.with_root_certificates(store).with_no_client_auth()
|
||||
} else {
|
||||
builder
|
||||
.dangerous()
|
||||
.with_custom_certificate_verifier(std::sync::Arc::new(
|
||||
danger::NoCertificateVerification {},
|
||||
#[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))]
|
||||
danger::NoCertificateVerification::new(rustls::crypto::aws_lc_rs::default_provider()),
|
||||
#[cfg(feature = "use-rustls-ring")]
|
||||
danger::NoCertificateVerification::new(rustls::crypto::ring::default_provider()),
|
||||
))
|
||||
.with_no_client_auth()
|
||||
};
|
||||
@ -386,7 +460,7 @@ impl RawClient<ElectrumSslStream> {
|
||||
let domain = socket_addr.domain().unwrap_or("NONE").to_string();
|
||||
let session = ClientConnection::new(
|
||||
std::sync::Arc::new(config),
|
||||
ServerName::try_from(domain.as_str())
|
||||
ServerName::try_from(domain.clone())
|
||||
.map_err(|_| Error::InvalidDNSNameError(domain.clone()))?,
|
||||
)
|
||||
.map_err(Error::CouldNotCreateConnection)?;
|
||||
@ -425,7 +499,11 @@ impl RawClient<ElectrumProxyStream> {
|
||||
Ok(stream.into())
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "use-openssl", feature = "use-rustls"))]
|
||||
#[cfg(any(
|
||||
feature = "use-openssl",
|
||||
feature = "use-rustls",
|
||||
feature = "use-rustls-ring"
|
||||
))]
|
||||
/// Creates a new TLS client that connects to `target_addr` using `proxy_addr` as a socks proxy
|
||||
/// server. The DNS resolution of `target_addr`, if required, is done through the proxy. This
|
||||
/// allows to specify, for instance, `.onion` addresses.
|
||||
@ -484,11 +562,10 @@ impl<S: Read + Write> RawClient<S> {
|
||||
|
||||
if let Some(until_message) = until_message {
|
||||
// If we are trying to start a reader thread but the corresponding sender is
|
||||
// missing from the map, exit immediately. This can happen with batch calls,
|
||||
// since the sender is shared for all the individual queries in a call. We
|
||||
// might have already received a response for that id, but we don't know it
|
||||
// yet. Exiting here forces the calling code to fallback to the sender-receiver
|
||||
// method, and it should find a message there waiting for it.
|
||||
// missing from the map, exit immediately. We might have already received a
|
||||
// response for that id, but we don't know it yet. Exiting here forces the
|
||||
// calling code to fallback to the sender-receiver method, and it should find
|
||||
// a message there waiting for it.
|
||||
if self.waiting_map.lock()?.get(&until_message).is_none() {
|
||||
return Err(Error::CouldntLockReader);
|
||||
}
|
||||
@ -559,7 +636,7 @@ impl<S: Read + Write> RawClient<S> {
|
||||
// No id, that's probably a notification.
|
||||
let mut resp = resp;
|
||||
|
||||
if let Some(ref method) = resp["method"].take().as_str() {
|
||||
if let Some(method) = resp["method"].take().as_str() {
|
||||
self.handle_notification(method, resp["params"].take())?;
|
||||
} else {
|
||||
warn!("Unexpected response: {:?}", resp);
|
||||
@ -648,17 +725,11 @@ impl<S: Read + Write> RawClient<S> {
|
||||
|
||||
fn handle_notification(&self, method: &str, result: serde_json::Value) -> Result<(), Error> {
|
||||
match method {
|
||||
"blockchain.headers.subscribe" => {
|
||||
let mut queue = self.headers.lock()?;
|
||||
match queue.as_mut() {
|
||||
None => return Err(Error::NotSubscribedToHeaders),
|
||||
Some(queue) => queue.append(
|
||||
&mut serde_json::from_value::<Vec<RawHeaderNotification>>(result)?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
}
|
||||
}
|
||||
"blockchain.headers.subscribe" => self.headers.lock()?.append(
|
||||
&mut serde_json::from_value::<Vec<RawHeaderNotification>>(result)?
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
"blockchain.scripthash.subscribe" => {
|
||||
let unserialized: ScriptNotification = serde_json::from_value(result)?;
|
||||
let mut script_notifications = self.script_notifications.lock()?;
|
||||
@ -682,7 +753,7 @@ impl<S: Read + Write> RawClient<S> {
|
||||
) -> Result<serde_json::Value, Error> {
|
||||
let req = Request::new_id(
|
||||
self.last_id.fetch_add(1, Ordering::SeqCst),
|
||||
&method_name,
|
||||
method_name,
|
||||
params,
|
||||
);
|
||||
let result = self.call(req)?;
|
||||
@ -713,22 +784,23 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
|
||||
fn batch_call(&self, batch: &Batch) -> Result<Vec<serde_json::Value>, Error> {
|
||||
let mut raw = Vec::new();
|
||||
|
||||
let mut missing_responses = BTreeSet::new();
|
||||
let mut missing_responses = Vec::new();
|
||||
let mut answers = BTreeMap::new();
|
||||
|
||||
// Add our listener to the map before we send the request, Here we will clone the sender
|
||||
// for every request id, so that we only have to monitor one receiver.
|
||||
let (sender, receiver) = channel();
|
||||
// Add our listener to the map before we send the request
|
||||
|
||||
for (method, params) in batch.iter() {
|
||||
let req = Request::new_id(
|
||||
self.last_id.fetch_add(1, Ordering::SeqCst),
|
||||
&method,
|
||||
method,
|
||||
params.to_vec(),
|
||||
);
|
||||
missing_responses.insert(req.id);
|
||||
// Add distinct channel to each request so when we remove our request id (and sender) from the waiting_map
|
||||
// we can be sure that the response gets sent to the correct channel in self.recv
|
||||
let (sender, receiver) = channel();
|
||||
missing_responses.push((req.id, receiver));
|
||||
|
||||
self.waiting_map.lock()?.insert(req.id, sender.clone());
|
||||
self.waiting_map.lock()?.insert(req.id, sender);
|
||||
|
||||
raw.append(&mut serde_json::to_vec(&req)?);
|
||||
raw.extend_from_slice(b"\n");
|
||||
@ -747,8 +819,8 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
|
||||
|
||||
self.increment_calls();
|
||||
|
||||
for req_id in missing_responses.iter() {
|
||||
match self.recv(&receiver, *req_id) {
|
||||
for (req_id, receiver) in missing_responses.iter() {
|
||||
match self.recv(receiver, *req_id) {
|
||||
Ok(mut resp) => answers.insert(req_id, resp["result"].take()),
|
||||
Err(e) => {
|
||||
// In case of error our sender could still be left in the map, depending on where
|
||||
@ -756,7 +828,7 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
|
||||
warn!("got error for req_id {}: {:?}", req_id, e);
|
||||
warn!("removing all waiting req of this batch");
|
||||
let mut guard = self.waiting_map.lock()?;
|
||||
for req_id in missing_responses.iter() {
|
||||
for (req_id, _) in missing_responses.iter() {
|
||||
guard.remove(req_id);
|
||||
}
|
||||
return Err(e);
|
||||
@ -764,15 +836,10 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
|
||||
};
|
||||
}
|
||||
|
||||
Ok(answers.into_iter().map(|(_, r)| r).collect())
|
||||
Ok(answers.into_values().collect())
|
||||
}
|
||||
|
||||
fn block_headers_subscribe_raw(&self) -> Result<RawHeaderNotification, Error> {
|
||||
let mut headers = self.headers.lock()?;
|
||||
if headers.is_none() {
|
||||
*headers = Some(VecDeque::new());
|
||||
}
|
||||
|
||||
let req = Request::new_id(
|
||||
self.last_id.fetch_add(1, Ordering::SeqCst),
|
||||
"blockchain.headers.subscribe",
|
||||
@ -784,11 +851,7 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
|
||||
}
|
||||
|
||||
fn block_headers_pop_raw(&self) -> Result<Option<RawHeaderNotification>, Error> {
|
||||
let mut queue = self.headers.lock()?;
|
||||
match queue.as_mut() {
|
||||
None => Err(Error::NotSubscribedToHeaders),
|
||||
Some(queue) => Ok(queue.pop_front()),
|
||||
}
|
||||
Ok(self.headers.lock()?.pop_front())
|
||||
}
|
||||
|
||||
fn block_header_raw(&self, height: usize) -> Result<Vec<u8>, Error> {
|
||||
@ -1062,6 +1125,49 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
|
||||
Ok(serde_json::from_value(result)?)
|
||||
}
|
||||
|
||||
fn batch_transaction_get_merkle<I>(
|
||||
&self,
|
||||
txids_and_heights: I,
|
||||
) -> Result<Vec<GetMerkleRes>, Error>
|
||||
where
|
||||
I: IntoIterator + Clone,
|
||||
I::Item: Borrow<(Txid, usize)>,
|
||||
{
|
||||
impl_batch_call!(self, txids_and_heights, transaction_get_merkle)
|
||||
}
|
||||
|
||||
fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result<Txid, Error> {
|
||||
let params = vec![Param::Usize(height), Param::Usize(tx_pos)];
|
||||
let req = Request::new_id(
|
||||
self.last_id.fetch_add(1, Ordering::SeqCst),
|
||||
"blockchain.transaction.id_from_pos",
|
||||
params,
|
||||
);
|
||||
let result = self.call(req)?;
|
||||
|
||||
Ok(serde_json::from_value(result)?)
|
||||
}
|
||||
|
||||
fn txid_from_pos_with_merkle(
|
||||
&self,
|
||||
height: usize,
|
||||
tx_pos: usize,
|
||||
) -> Result<TxidFromPosRes, Error> {
|
||||
let params = vec![
|
||||
Param::Usize(height),
|
||||
Param::Usize(tx_pos),
|
||||
Param::Bool(true),
|
||||
];
|
||||
let req = Request::new_id(
|
||||
self.last_id.fetch_add(1, Ordering::SeqCst),
|
||||
"blockchain.transaction.id_from_pos",
|
||||
params,
|
||||
);
|
||||
let result = self.call(req)?;
|
||||
|
||||
Ok(serde_json::from_value(result)?)
|
||||
}
|
||||
|
||||
fn server_features(&self) -> Result<ServerFeaturesRes, Error> {
|
||||
let req = Request::new_id(
|
||||
self.last_id.fetch_add(1, Ordering::SeqCst),
|
||||
@ -1094,8 +1200,10 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::utils;
|
||||
|
||||
use super::RawClient;
|
||||
use api::ElectrumApi;
|
||||
use crate::api::ElectrumApi;
|
||||
|
||||
fn get_test_server() -> String {
|
||||
std::env::var("TEST_ELECTRUM_SERVER").unwrap_or("electrum.blockstream.info:50001".into())
|
||||
@ -1116,12 +1224,32 @@ mod test {
|
||||
assert_eq!(resp.hash_function, Some("sha256".into()));
|
||||
assert_eq!(resp.pruning, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore = "depends on a live server"]
|
||||
fn test_batch_response_ordering() {
|
||||
// The electrum.blockstream.info:50001 node always sends back ordered responses which will make this always pass.
|
||||
// However, many servers do not, so we use one of those servers for this test.
|
||||
let client = RawClient::new("exs.dyshek.org:50001", None).unwrap();
|
||||
let heights: Vec<u32> = vec![1, 4, 8, 12, 222, 6666, 12];
|
||||
let result_times = [
|
||||
1231469665, 1231470988, 1231472743, 1231474888, 1231770653, 1236456633, 1231474888,
|
||||
];
|
||||
// Check ordering 10 times. This usually fails within 5 if ordering is incorrect.
|
||||
for _ in 0..10 {
|
||||
let results = client.batch_block_header(&heights).unwrap();
|
||||
for (index, result) in results.iter().enumerate() {
|
||||
assert_eq!(result_times[index], result.time);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_relay_fee() {
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
let resp = client.relay_fee().unwrap();
|
||||
assert_eq!(resp, 0.00001);
|
||||
assert!(resp > 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1194,10 +1322,10 @@ mod test {
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
// Mt.Gox hack address
|
||||
let addr = bitcoin::Address::from_str("1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF").unwrap();
|
||||
let resp = client
|
||||
.script_get_history(&addr.payload.script_pubkey())
|
||||
.unwrap();
|
||||
let addr = bitcoin::Address::from_str("1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF")
|
||||
.unwrap()
|
||||
.assume_checked();
|
||||
let resp = client.script_get_history(&addr.script_pubkey()).unwrap();
|
||||
|
||||
assert!(resp.len() >= 328);
|
||||
assert_eq!(
|
||||
@ -1215,10 +1343,10 @@ mod test {
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
// Peter todd's sha256 bounty address https://bitcointalk.org/index.php?topic=293382.0
|
||||
let addr = bitcoin::Address::from_str("35Snmmy3uhaer2gTboc81ayCip4m9DT4ko").unwrap();
|
||||
let resp = client
|
||||
.script_list_unspent(&addr.payload.script_pubkey())
|
||||
.unwrap();
|
||||
let addr = bitcoin::Address::from_str("35Snmmy3uhaer2gTboc81ayCip4m9DT4ko")
|
||||
.unwrap()
|
||||
.assume_checked();
|
||||
let resp = client.script_list_unspent(&addr.script_pubkey()).unwrap();
|
||||
|
||||
assert!(resp.len() >= 9);
|
||||
let txid = "397f12ee15f8a3d2ab25c0f6bb7d3c64d2038ca056af10dd8251b98ae0f076b0";
|
||||
@ -1239,7 +1367,7 @@ mod test {
|
||||
// Peter todd's sha256 bounty address https://bitcointalk.org/index.php?topic=293382.0
|
||||
let script_1 = bitcoin::Address::from_str("35Snmmy3uhaer2gTboc81ayCip4m9DT4ko")
|
||||
.unwrap()
|
||||
.payload
|
||||
.assume_checked()
|
||||
.script_pubkey();
|
||||
|
||||
let resp = client
|
||||
@ -1261,7 +1389,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_transaction_get() {
|
||||
use bitcoin::Txid;
|
||||
use bitcoin::{transaction, Txid};
|
||||
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
@ -1271,7 +1399,7 @@ mod test {
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(resp.version, 1);
|
||||
assert_eq!(resp.version, transaction::Version::ONE);
|
||||
assert_eq!(resp.lock_time.to_consensus_u32(), 0);
|
||||
}
|
||||
|
||||
@ -1315,21 +1443,166 @@ mod test {
|
||||
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
let resp = client
|
||||
.transaction_get_merkle(
|
||||
&Txid::from_str("cc2ca076fd04c2aeed6d02151c447ced3d09be6fb4d4ef36cb5ed4e7a3260566")
|
||||
.unwrap(),
|
||||
630000,
|
||||
)
|
||||
.unwrap();
|
||||
let txid =
|
||||
Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d")
|
||||
.unwrap();
|
||||
let resp = client.transaction_get_merkle(&txid, 630000).unwrap();
|
||||
assert_eq!(resp.block_height, 630000);
|
||||
assert_eq!(resp.pos, 0);
|
||||
assert_eq!(resp.pos, 68);
|
||||
assert_eq!(resp.merkle.len(), 12);
|
||||
assert_eq!(
|
||||
resp.merkle[0],
|
||||
[
|
||||
30, 10, 161, 245, 132, 125, 136, 198, 186, 138, 107, 216, 92, 22, 145, 81, 130,
|
||||
126, 200, 65, 121, 158, 105, 111, 38, 151, 38, 147, 144, 224, 5, 218
|
||||
34, 65, 51, 64, 49, 139, 115, 189, 185, 246, 70, 225, 168, 193, 217, 195, 47, 66,
|
||||
179, 240, 153, 24, 114, 215, 144, 196, 212, 41, 39, 155, 246, 25
|
||||
]
|
||||
);
|
||||
|
||||
// Check we can verify the merkle proof validity, but fail if we supply wrong data.
|
||||
let block_header = client.block_header(resp.block_height).unwrap();
|
||||
assert!(utils::validate_merkle_proof(
|
||||
&txid,
|
||||
&block_header.merkle_root,
|
||||
&resp
|
||||
));
|
||||
|
||||
let mut fail_resp = resp.clone();
|
||||
fail_resp.pos = 13;
|
||||
assert!(!utils::validate_merkle_proof(
|
||||
&txid,
|
||||
&block_header.merkle_root,
|
||||
&fail_resp
|
||||
));
|
||||
|
||||
let fail_block_header = client.block_header(resp.block_height + 1).unwrap();
|
||||
assert!(!utils::validate_merkle_proof(
|
||||
&txid,
|
||||
&fail_block_header.merkle_root,
|
||||
&resp
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_batch_transaction_get_merkle() {
|
||||
use bitcoin::Txid;
|
||||
|
||||
struct TestCase {
|
||||
txid: Txid,
|
||||
block_height: usize,
|
||||
exp_pos: usize,
|
||||
exp_bytes: [u8; 32],
|
||||
}
|
||||
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
let test_cases: Vec<TestCase> = vec![
|
||||
TestCase {
|
||||
txid: Txid::from_str(
|
||||
"1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d",
|
||||
)
|
||||
.unwrap(),
|
||||
block_height: 630000,
|
||||
exp_pos: 68,
|
||||
exp_bytes: [
|
||||
34, 65, 51, 64, 49, 139, 115, 189, 185, 246, 70, 225, 168, 193, 217, 195, 47,
|
||||
66, 179, 240, 153, 24, 114, 215, 144, 196, 212, 41, 39, 155, 246, 25,
|
||||
],
|
||||
},
|
||||
TestCase {
|
||||
txid: Txid::from_str(
|
||||
"70a8639bc9b743c0610d1231103a2f8e99f4a25670946b91f16c55a5373b37d1",
|
||||
)
|
||||
.unwrap(),
|
||||
block_height: 630001,
|
||||
exp_pos: 25,
|
||||
exp_bytes: [
|
||||
169, 100, 34, 99, 168, 101, 25, 168, 184, 90, 77, 50, 151, 245, 130, 101, 193,
|
||||
229, 136, 128, 63, 110, 241, 19, 242, 59, 184, 137, 245, 249, 188, 110,
|
||||
],
|
||||
},
|
||||
TestCase {
|
||||
txid: Txid::from_str(
|
||||
"a0db149ace545beabbd87a8d6b20ffd6aa3b5a50e58add49a3d435f898c272cf",
|
||||
)
|
||||
.unwrap(),
|
||||
block_height: 840000,
|
||||
exp_pos: 0,
|
||||
exp_bytes: [
|
||||
43, 184, 95, 75, 0, 75, 230, 218, 84, 247, 102, 193, 124, 30, 133, 81, 135, 50,
|
||||
113, 18, 194, 49, 239, 47, 243, 94, 186, 208, 234, 103, 198, 158,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
let txids_and_heights: Vec<(Txid, usize)> = test_cases
|
||||
.iter()
|
||||
.map(|case| (case.txid, case.block_height))
|
||||
.collect();
|
||||
|
||||
let resp = client
|
||||
.batch_transaction_get_merkle(&txids_and_heights)
|
||||
.unwrap();
|
||||
|
||||
for (i, (res, test_case)) in resp.iter().zip(test_cases).enumerate() {
|
||||
assert_eq!(res.block_height, test_case.block_height);
|
||||
assert_eq!(res.pos, test_case.exp_pos);
|
||||
assert_eq!(res.merkle.len(), 12);
|
||||
assert_eq!(res.merkle[0], test_case.exp_bytes);
|
||||
|
||||
// Check we can verify the merkle proof validity, but fail if we supply wrong data.
|
||||
let block_header = client.block_header(res.block_height).unwrap();
|
||||
assert!(utils::validate_merkle_proof(
|
||||
&txids_and_heights[i].0,
|
||||
&block_header.merkle_root,
|
||||
res
|
||||
));
|
||||
|
||||
let mut fail_res = res.clone();
|
||||
fail_res.pos = 13;
|
||||
assert!(!utils::validate_merkle_proof(
|
||||
&txids_and_heights[i].0,
|
||||
&block_header.merkle_root,
|
||||
&fail_res
|
||||
));
|
||||
|
||||
let fail_block_header = client.block_header(res.block_height + 1).unwrap();
|
||||
assert!(!utils::validate_merkle_proof(
|
||||
&txids_and_heights[i].0,
|
||||
&fail_block_header.merkle_root,
|
||||
res
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_txid_from_pos() {
|
||||
use bitcoin::Txid;
|
||||
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
let txid =
|
||||
Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d")
|
||||
.unwrap();
|
||||
let resp = client.txid_from_pos(630000, 68).unwrap();
|
||||
assert_eq!(resp, txid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_txid_from_pos_with_merkle() {
|
||||
use bitcoin::Txid;
|
||||
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
let txid =
|
||||
Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d")
|
||||
.unwrap();
|
||||
let resp = client.txid_from_pos_with_merkle(630000, 68).unwrap();
|
||||
assert_eq!(resp.tx_hash, txid);
|
||||
assert_eq!(
|
||||
resp.merkle[0],
|
||||
[
|
||||
34, 65, 51, 64, 49, 139, 115, 189, 185, 246, 70, 225, 168, 193, 217, 195, 47, 66,
|
||||
179, 240, 153, 24, 114, 215, 144, 196, 212, 41, 39, 155, 246, 25
|
||||
]
|
||||
);
|
||||
}
|
||||
@ -1348,16 +1621,6 @@ mod test {
|
||||
assert!(resp.height >= 639000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_block_headers_subscribe_pop() {
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
let resp = client.block_headers_pop();
|
||||
assert_eq!(format!("{:?}", resp), "Err(NotSubscribedToHeaders)");
|
||||
client.block_headers_subscribe().unwrap();
|
||||
let resp = client.block_headers_pop();
|
||||
assert!(resp.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_script_subscribe() {
|
||||
use std::str::FromStr;
|
||||
@ -1365,12 +1628,12 @@ mod test {
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
// Mt.Gox hack address
|
||||
let addr = bitcoin::Address::from_str("1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF").unwrap();
|
||||
let addr = bitcoin::Address::from_str("1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF")
|
||||
.unwrap()
|
||||
.assume_checked();
|
||||
|
||||
// Just make sure that the call returns Ok(something)
|
||||
client
|
||||
.script_subscribe(&addr.payload.script_pubkey())
|
||||
.unwrap();
|
||||
client.script_subscribe(&addr.script_pubkey()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1383,7 +1646,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_raw_call() {
|
||||
use types::Param;
|
||||
use crate::types::Param;
|
||||
|
||||
let client = RawClient::new(get_test_server(), None).unwrap();
|
||||
|
||||
|
||||
@ -99,7 +99,7 @@ impl ToTargetAddr for (Ipv6Addr, u16) {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToTargetAddr for (&'a str, u16) {
|
||||
impl ToTargetAddr for (&str, u16) {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
// try to parse as an IP first
|
||||
if let Ok(addr) = self.0.parse::<Ipv4Addr>() {
|
||||
@ -114,7 +114,7 @@ impl<'a> ToTargetAddr for (&'a str, u16) {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ToTargetAddr for &'a str {
|
||||
impl ToTargetAddr for &str {
|
||||
fn to_target_addr(&self) -> io::Result<TargetAddr> {
|
||||
// try to parse as an IP first
|
||||
if let Ok(addr) = self.parse::<SocketAddrV4>() {
|
||||
|
||||
@ -18,12 +18,7 @@ fn read_response(socket: &mut TcpStream) -> io::Result<SocketAddrV4> {
|
||||
|
||||
match response.read_u8()? {
|
||||
90 => {}
|
||||
91 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"request rejected or failed",
|
||||
))
|
||||
}
|
||||
91 => return Err(io::Error::other("request rejected or failed")),
|
||||
92 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::PermissionDenied,
|
||||
@ -109,7 +104,7 @@ impl Socks4Stream {
|
||||
let _ = packet.write_u32::<BigEndian>(Ipv4Addr::new(0, 0, 0, 1).into());
|
||||
let _ = packet.write_all(userid.as_bytes());
|
||||
let _ = packet.write_u8(0);
|
||||
let _ = packet.extend(host.as_bytes());
|
||||
packet.extend(host.as_bytes());
|
||||
let _ = packet.write_u8(0);
|
||||
}
|
||||
}
|
||||
@ -117,10 +112,7 @@ impl Socks4Stream {
|
||||
socket.write_all(&packet)?;
|
||||
let proxy_addr = read_response(&mut socket)?;
|
||||
|
||||
Ok(Socks4Stream {
|
||||
socket: socket,
|
||||
proxy_addr: proxy_addr,
|
||||
})
|
||||
Ok(Socks4Stream { socket, proxy_addr })
|
||||
}
|
||||
|
||||
/// Returns the proxy-side address of the connection between the proxy and
|
||||
@ -151,7 +143,7 @@ impl Read for Socks4Stream {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Read for &'a Socks4Stream {
|
||||
impl Read for &Socks4Stream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
(&self.socket).read(buf)
|
||||
}
|
||||
@ -167,7 +159,7 @@ impl Write for Socks4Stream {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Write for &'a Socks4Stream {
|
||||
impl Write for &Socks4Stream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
(&self.socket).write(buf)
|
||||
}
|
||||
|
||||
@ -37,10 +37,7 @@ fn read_addr<R: Read>(socket: &mut R) -> io::Result<TargetAddr> {
|
||||
ip, port, 0, 0,
|
||||
))))
|
||||
}
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"unsupported address type",
|
||||
)),
|
||||
_ => Err(io::Error::other("unsupported address type")),
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,35 +51,15 @@ fn read_response(socket: &mut TcpStream) -> io::Result<TargetAddr> {
|
||||
|
||||
match socket.read_u8()? {
|
||||
0 => {}
|
||||
1 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"general SOCKS server failure",
|
||||
))
|
||||
}
|
||||
2 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"connection not allowed by ruleset",
|
||||
))
|
||||
}
|
||||
3 => return Err(io::Error::new(io::ErrorKind::Other, "network unreachable")),
|
||||
4 => return Err(io::Error::new(io::ErrorKind::Other, "host unreachable")),
|
||||
5 => return Err(io::Error::new(io::ErrorKind::Other, "connection refused")),
|
||||
6 => return Err(io::Error::new(io::ErrorKind::Other, "TTL expired")),
|
||||
7 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"command not supported",
|
||||
))
|
||||
}
|
||||
8 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"address kind not supported",
|
||||
))
|
||||
}
|
||||
_ => return Err(io::Error::new(io::ErrorKind::Other, "unknown error")),
|
||||
1 => return Err(io::Error::other("general SOCKS server failure")),
|
||||
2 => return Err(io::Error::other("connection not allowed by ruleset")),
|
||||
3 => return Err(io::Error::other("network unreachable")),
|
||||
4 => return Err(io::Error::other("host unreachable")),
|
||||
5 => return Err(io::Error::other("connection refused")),
|
||||
6 => return Err(io::Error::other("TTL expired")),
|
||||
7 => return Err(io::Error::other("command not supported")),
|
||||
8 => return Err(io::Error::other("address kind not supported")),
|
||||
_ => return Err(io::Error::other("unknown error")),
|
||||
}
|
||||
|
||||
if socket.read_u8()? != 0 {
|
||||
@ -110,7 +87,7 @@ fn write_addr(mut packet: &mut [u8], target: &TargetAddr) -> io::Result<usize> {
|
||||
}
|
||||
TargetAddr::Domain(ref domain, port) => {
|
||||
packet.write_u8(3).unwrap();
|
||||
if domain.len() > u8::max_value() as usize {
|
||||
if domain.len() > u8::MAX as usize {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"domain name too long",
|
||||
@ -135,7 +112,7 @@ enum Authentication<'a> {
|
||||
None,
|
||||
}
|
||||
|
||||
impl<'a> Authentication<'a> {
|
||||
impl Authentication<'_> {
|
||||
fn id(&self) -> u8 {
|
||||
match *self {
|
||||
Authentication::Password { .. } => 2,
|
||||
@ -144,11 +121,7 @@ impl<'a> Authentication<'a> {
|
||||
}
|
||||
|
||||
fn is_no_auth(&self) -> bool {
|
||||
if let Authentication::None = *self {
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
matches!(*self, Authentication::None)
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,6 +177,9 @@ impl Socks5Stream {
|
||||
TcpStream::connect(proxy)?
|
||||
};
|
||||
|
||||
socket.set_read_timeout(timeout)?;
|
||||
socket.set_write_timeout(timeout)?;
|
||||
|
||||
let target = target.to_target_addr()?;
|
||||
|
||||
let packet_len = if auth.is_no_auth() { 3 } else { 4 };
|
||||
@ -228,14 +204,11 @@ impl Socks5Stream {
|
||||
}
|
||||
|
||||
if selected_method == 0xff {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"no acceptable auth methods",
|
||||
));
|
||||
return Err(io::Error::other("no acceptable auth methods"));
|
||||
}
|
||||
|
||||
if selected_method != auth.id() && selected_method != Authentication::None.id() {
|
||||
return Err(io::Error::new(io::ErrorKind::Other, "unknown auth method"));
|
||||
return Err(io::Error::other("unknown auth method"));
|
||||
}
|
||||
|
||||
match *auth {
|
||||
@ -254,10 +227,7 @@ impl Socks5Stream {
|
||||
|
||||
let proxy_addr = read_response(&mut socket)?;
|
||||
|
||||
Ok(Socks5Stream {
|
||||
socket: socket,
|
||||
proxy_addr: proxy_addr,
|
||||
})
|
||||
Ok(Socks5Stream { socket, proxy_addr })
|
||||
}
|
||||
|
||||
fn password_authentication(
|
||||
@ -265,13 +235,13 @@ impl Socks5Stream {
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> io::Result<()> {
|
||||
if username.len() < 1 || username.len() > 255 {
|
||||
if username.is_empty() || username.len() > 255 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"invalid username",
|
||||
));
|
||||
};
|
||||
if password.len() < 1 || password.len() > 255 {
|
||||
if password.is_empty() || password.len() > 255 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"invalid password",
|
||||
@ -333,7 +303,7 @@ impl Read for Socks5Stream {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Read for &'a Socks5Stream {
|
||||
impl Read for &Socks5Stream {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
(&self.socket).read(buf)
|
||||
}
|
||||
@ -349,7 +319,7 @@ impl Write for Socks5Stream {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Write for &'a Socks5Stream {
|
||||
impl Write for &Socks5Stream {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
(&self.socket).write(buf)
|
||||
}
|
||||
@ -471,10 +441,7 @@ impl Socks5Datagram {
|
||||
let socket = UdpSocket::bind(addr)?;
|
||||
socket.connect(&stream.proxy_addr)?;
|
||||
|
||||
Ok(Socks5Datagram {
|
||||
socket: socket,
|
||||
stream: stream,
|
||||
})
|
||||
Ok(Socks5Datagram { socket, stream })
|
||||
}
|
||||
|
||||
/// Like `UdpSocket::send_to`.
|
||||
@ -523,11 +490,7 @@ impl Socks5Datagram {
|
||||
let addr = read_addr(&mut header)?;
|
||||
|
||||
unsafe {
|
||||
ptr::copy(
|
||||
buf.as_ptr(),
|
||||
buf.as_mut_ptr().offset(header.len() as isize),
|
||||
overflow,
|
||||
);
|
||||
ptr::copy(buf.as_ptr(), buf.as_mut_ptr().add(header.len()), overflow);
|
||||
}
|
||||
buf[..header.len()].copy_from_slice(header);
|
||||
|
||||
|
||||
54
src/types.rs
54
src/types.rs
@ -9,16 +9,15 @@ use std::sync::Arc;
|
||||
|
||||
use bitcoin::blockdata::block;
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::hashes::{sha256, Hash};
|
||||
use bitcoin::hex::{DisplayHex, FromHex};
|
||||
use bitcoin::{Script, Txid};
|
||||
|
||||
use bitcoin_private::hex::exts::DisplayHex;
|
||||
use serde::{de, Deserialize, Serialize};
|
||||
|
||||
static JSONRPC_2_0: &str = "2.0";
|
||||
|
||||
pub type Call = (String, Vec<Param>);
|
||||
pub(crate) type Call = (String, Vec<Param>);
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
#[serde(untagged)]
|
||||
@ -88,7 +87,7 @@ impl From<[u8; 32]> for Hex32Bytes {
|
||||
}
|
||||
|
||||
impl Hex32Bytes {
|
||||
pub(crate) fn to_hex(&self) -> String {
|
||||
pub(crate) fn to_hex(self) -> String {
|
||||
self.0.to_lower_hex_string()
|
||||
}
|
||||
}
|
||||
@ -161,7 +160,7 @@ where
|
||||
}
|
||||
|
||||
/// Response to a [`script_get_history`](../client/struct.Client.html#method.script_get_history) request.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GetHistoryRes {
|
||||
/// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of
|
||||
/// its inputs are unconfirmed too.
|
||||
@ -173,7 +172,7 @@ pub struct GetHistoryRes {
|
||||
}
|
||||
|
||||
/// Response to a [`script_list_unspent`](../client/struct.Client.html#method.script_list_unspent) request.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ListUnspentRes {
|
||||
/// Confirmation height of the transaction that created this output.
|
||||
pub height: usize,
|
||||
@ -186,7 +185,7 @@ pub struct ListUnspentRes {
|
||||
}
|
||||
|
||||
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ServerFeaturesRes {
|
||||
/// Server version reported.
|
||||
pub server_version: String,
|
||||
@ -204,7 +203,7 @@ pub struct ServerFeaturesRes {
|
||||
}
|
||||
|
||||
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GetHeadersRes {
|
||||
/// Maximum number of headers returned in a single response.
|
||||
pub max: usize,
|
||||
@ -219,7 +218,7 @@ pub struct GetHeadersRes {
|
||||
}
|
||||
|
||||
/// Response to a [`script_get_balance`](../client/struct.Client.html#method.script_get_balance) request.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GetBalanceRes {
|
||||
/// Confirmed balance in Satoshis for the address.
|
||||
pub confirmed: u64,
|
||||
@ -230,7 +229,7 @@ pub struct GetBalanceRes {
|
||||
}
|
||||
|
||||
/// Response to a [`transaction_get_merkle`](../client/struct.Client.html#method.transaction_get_merkle) request.
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct GetMerkleRes {
|
||||
/// Height of the block that confirmed the transaction
|
||||
pub block_height: usize,
|
||||
@ -241,8 +240,19 @@ pub struct GetMerkleRes {
|
||||
pub merkle: Vec<[u8; 32]>,
|
||||
}
|
||||
|
||||
/// Response to a [`txid_from_pos_with_merkle`](../client/struct.Client.html#method.txid_from_pos_with_merkle)
|
||||
/// request.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct TxidFromPosRes {
|
||||
/// Txid of the transaction.
|
||||
pub tx_hash: Txid,
|
||||
/// The merkle path of the transaction.
|
||||
#[serde(deserialize_with = "from_hex_array")]
|
||||
pub merkle: Vec<[u8; 32]>,
|
||||
}
|
||||
|
||||
/// Notification of a new block header
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct HeaderNotification {
|
||||
/// New block height.
|
||||
pub height: usize,
|
||||
@ -252,7 +262,7 @@ pub struct HeaderNotification {
|
||||
}
|
||||
|
||||
/// Notification of a new block header with the header encoded as raw bytes
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct RawHeaderNotification {
|
||||
/// New block height.
|
||||
pub height: usize,
|
||||
@ -273,7 +283,7 @@ impl TryFrom<RawHeaderNotification> for HeaderNotification {
|
||||
}
|
||||
|
||||
/// Notification of the new status of a script
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ScriptNotification {
|
||||
/// Address that generated this notification.
|
||||
pub scripthash: ScriptHash,
|
||||
@ -288,8 +298,8 @@ pub enum Error {
|
||||
IOError(std::io::Error),
|
||||
/// Wraps `serde_json::error::Error`
|
||||
JSON(serde_json::error::Error),
|
||||
/// Wraps `bitcoin::hashes::hex::Error`
|
||||
Hex(bitcoin::hashes::hex::Error),
|
||||
/// Wraps `bitcoin::hex::HexToBytesError`
|
||||
Hex(bitcoin::hex::HexToBytesError),
|
||||
/// Error returned by the Electrum server
|
||||
Protocol(serde_json::Value),
|
||||
/// Error during the deserialization of a Bitcoin data structure
|
||||
@ -316,8 +326,7 @@ pub enum Error {
|
||||
CouldntLockReader,
|
||||
/// Broken IPC communication channel: the other thread probably has exited
|
||||
Mpsc,
|
||||
|
||||
#[cfg(feature = "use-rustls")]
|
||||
#[cfg(any(feature = "use-rustls", feature = "use-rustls-ring"))]
|
||||
/// Could not create a rustls client connection
|
||||
CouldNotCreateConnection(rustls::Error),
|
||||
|
||||
@ -327,9 +336,6 @@ pub enum Error {
|
||||
#[cfg(feature = "use-openssl")]
|
||||
/// SSL Handshake failed with the server
|
||||
SslHandshakeError(openssl::ssl::HandshakeError<std::net::TcpStream>),
|
||||
|
||||
/// Expecting notification on headers but we are not subscribed
|
||||
NotSubscribedToHeaders,
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
@ -344,7 +350,10 @@ impl Display for Error {
|
||||
Error::SslHandshakeError(e) => Display::fmt(e, f),
|
||||
#[cfg(feature = "use-openssl")]
|
||||
Error::InvalidSslMethod(e) => Display::fmt(e, f),
|
||||
#[cfg(feature = "use-rustls")]
|
||||
#[cfg(any(
|
||||
feature = "use-rustls",
|
||||
feature = "use-rustls-ring",
|
||||
))]
|
||||
Error::CouldNotCreateConnection(e) => Display::fmt(e, f),
|
||||
|
||||
Error::Message(e) => f.write_str(e),
|
||||
@ -367,7 +376,6 @@ impl Display for Error {
|
||||
Error::MissingDomain => f.write_str("Missing domain while it was explicitly asked to validate it"),
|
||||
Error::CouldntLockReader => f.write_str("Couldn't take a lock on the reader mutex. This means that there's already another reader thread is running"),
|
||||
Error::Mpsc => f.write_str("Broken IPC communication channel: the other thread probably has exited"),
|
||||
Error::NotSubscribedToHeaders => write!(f, "Expecting notification on headers but we are not subscribed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -386,7 +394,7 @@ macro_rules! impl_error {
|
||||
|
||||
impl_error!(std::io::Error, IOError);
|
||||
impl_error!(serde_json::Error, JSON);
|
||||
impl_error!(bitcoin::hashes::hex::Error, Hex);
|
||||
impl_error!(bitcoin::hex::HexToBytesError, Hex);
|
||||
impl_error!(bitcoin::consensus::encode::Error, Bitcoin);
|
||||
|
||||
impl<T> From<std::sync::PoisonError<T>> for Error {
|
||||
|
||||
43
src/utils.rs
Normal file
43
src/utils.rs
Normal file
@ -0,0 +1,43 @@
|
||||
//! Utilities helping to handle Electrum-related data.
|
||||
|
||||
use crate::types::GetMerkleRes;
|
||||
use bitcoin::hash_types::TxMerkleNode;
|
||||
use bitcoin::hashes::sha256d::Hash as Sha256d;
|
||||
use bitcoin::hashes::{Hash, HashEngine};
|
||||
use bitcoin::Txid;
|
||||
|
||||
/// Verifies a Merkle inclusion proof as retrieved via [`transaction_get_merkle`] for a transaction with the
|
||||
/// given `txid` and `merkle_root` as included in the [`BlockHeader`].
|
||||
///
|
||||
/// Returns `true` if the transaction is included in the corresponding block, and `false`
|
||||
/// otherwise.
|
||||
///
|
||||
/// [`transaction_get_merkle`]: crate::ElectrumApi::transaction_get_merkle
|
||||
/// [`BlockHeader`]: bitcoin::block::Header
|
||||
pub fn validate_merkle_proof(
|
||||
txid: &Txid,
|
||||
merkle_root: &TxMerkleNode,
|
||||
merkle_res: &GetMerkleRes,
|
||||
) -> bool {
|
||||
let mut index = merkle_res.pos;
|
||||
let mut cur = txid.to_raw_hash();
|
||||
for mut bytes in merkle_res.merkle.iter().cloned() {
|
||||
bytes.reverse();
|
||||
let next_hash = Sha256d::from_byte_array(bytes);
|
||||
|
||||
cur = Sha256d::from_engine({
|
||||
let mut engine = Sha256d::engine();
|
||||
if index % 2 == 0 {
|
||||
engine.input(cur.as_ref());
|
||||
engine.input(next_hash.as_ref());
|
||||
} else {
|
||||
engine.input(next_hash.as_ref());
|
||||
engine.input(cur.as_ref());
|
||||
};
|
||||
engine
|
||||
});
|
||||
index /= 2;
|
||||
}
|
||||
|
||||
cur == merkle_root.to_raw_hash()
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user