Compare commits

..

28 Commits

Author SHA1 Message Date
merge-script
b2fdc5ccf8
Merge bitcoindevkit/rust-electrum-client#195: feat(ci): add justfile
Some checks failed
CI / Test (stable) (push) Has been cancelled
CI / Rust fmt (push) Has been cancelled
CI / Rust clippy (push) Has been cancelled
CI / Test (1.75.0) (push) Has been cancelled
df6675dc09 fix(docs): fix `bitcoin::block::Header` link (Luis Schwab)
4b98565ee2 feat(ci): add justfile (Luis Schwab)

Pull request description:

  This PR adds a `justfile` stolen from `rust-esplora-client`.

ACKs for top commit:
  oleonardolima:
    tACK df6675dc09

Tree-SHA512: 8d8f43c27631d7dc1c0eac28e01828510118a0600c762e9459b779a6423ceb6b1e3000f5c797d065a2ef6e511e2c47f6ad7bcf0182a515e573fc097e74f9e04b
2026-01-20 20:58:18 -03:00
Luis Schwab
df6675dc09
fix(docs): fix bitcoin::block::Header link 2026-01-19 17:13:06 -03:00
Luis Schwab
4b98565ee2
feat(ci): add justfile 2026-01-19 17:10:29 -03:00
merge-script
5d7be37136
Merge bitcoindevkit/rust-electrum-client#180: feat!: Change ConfigBuilder::timeout to accept Option<Duration>
5dc4bb6360 ci: bump clippy to 1.90.0 (valued mammal)
e31cf4773b feat!: Change ConfigBuilder::timeout to accept Option<Duration> (valued mammal)

Pull request description:

  Change `ConfigBuilder::timeout` to accept `Option<Duration>`. This makes the API more explicit and less error prone, as the caller no longer needs to assume the units of the given duration.

  Also updated the code to run clippy check in CI using rust 1.90.0.

  BREAKING:

  The `timeout` method on `ConfigBuilder` is changed to accept a `timeout: Option<Duration>`. Previously it was `Option<u8>`.

  fix #151
  fix #175
  Supersedes #179

ACKs for top commit:
  oleonardolima:
    ACK 5dc4bb6360

Tree-SHA512: cd6ca6fdd91dcf6f193327ca40f6d37ea8b1b548102f080d0b72a0df509f6455aa50ee43148e4746ca6b295a30a78cb7d786fb0e90c050448e96e122a5b8c92d
2025-11-10 21:32:04 -05:00
merge-script
761796c94a
Merge bitcoindevkit/rust-electrum-client#185: Release 0.24.1
Some checks failed
CI / Test (1.75.0) (push) Has been cancelled
CI / Test (stable) (push) Has been cancelled
CI / Rust fmt (push) Has been cancelled
CI / Rust clippy (push) Has been cancelled
c2656995fc chore: Bump version to 0.24.1 (valued mammal)

Pull request description:

  Bump version to `0.24.1`.

  Update CHANGELOG.md.

ACKs for top commit:
  oleonardolima:
    ACK c2656995fc

Tree-SHA512: 1e73dc9268f0c09224df758cd84620d591f0b0b82e114dfda07507ede5c58b0c43d99d1935b301db46a162e71de35bde56c9f8d36b4d9451f90f56890b48cc07
2025-11-04 14:38:42 -05:00
valued mammal
5dc4bb6360
ci: bump clippy to 1.90.0
- Fixed hidden lifetime in `Batch::iter`
- Use `io::Error::other` in place of `io::Error::new`
when the error kind is `ErrorKind::Other`.
2025-11-03 10:10:26 -05:00
valued mammal
e31cf4773b
feat!: Change ConfigBuilder::timeout to accept Option<Duration> 2025-11-03 10:10:26 -05:00
valued mammal
c2656995fc
chore: Bump version to 0.24.1
Update CHANGELOG.md.
2025-11-03 10:06:12 -05:00
merge-script
b185259561
Merge bitcoindevkit/rust-electrum-client#183: Default to ring if multiple rustls features are set
80bf744a70 test: don't assert the value returned by `relay_fee` (valued mammal)
0e28021b3e Check in CI that we compile if all `rustls` features are set (Elias Rohrer)
980fa4afd6 Default to `ring` if multiple `rustls` features are set (Elias Rohrer)

Pull request description:

  Fixes #181

  We default to use `ring` if multiple features are set, ensuring the features are additive. To that end, we also add a check to CI that asserts we succeed to build with both features set.

  Would be cool to get a patch release for this, as it's currently breaking our docs.rs builds in `lightning-liquidity`.

ACKs for top commit:
  ValuedMammal:
    ACK 80bf744a70.
  oleonardolima:
    cACK 80bf744a70

Tree-SHA512: 0b1bacb0f3a57fd8c666e1ece14b9a733f9d5dfdec7efd6461b400d58ef0e302c286597531381e417b5fdf1b97659ed36266d2cf89f8a0c4cab5e4d9b3fdeed7
2025-11-03 09:55:26 -05:00
valued mammal
80bf744a70
test: don't assert the value returned by relay_fee
Changed `test_relay_fee` to no longer assert the value of min
relay fee, and instead assert that the value is non-zero.
This fixes a test failure likely due to some nodes now having a
smaller default min relay fee (100sat).
2025-11-03 08:04:32 -05:00
Elias Rohrer
0e28021b3e
Check in CI that we compile if all rustls features are set
We assert that we still succeed compilation if multiple `rustls`
features are set.
2025-11-03 10:49:30 +01:00
Elias Rohrer
980fa4afd6
Default to ring if multiple rustls features are set 2025-11-03 10:49:01 +01:00
merge-script
b20d41ace8
Merge bitcoindevkit/rust-electrum-client#174: chore(release): bump version to 0.24.0 and update CHANGELOG.md
Some checks failed
CI / Rust clippy (push) Has been cancelled
CI / Test (1.75.0) (push) Has been cancelled
CI / Test (stable) (push) Has been cancelled
CI / Rust fmt (push) Has been cancelled
24d07e2cac chore(release): update `CHANGELOG.md` (Leonardo Lima)
fbffdfe65e chore(release): bump version to `0.24.0` (Leonardo Lima)

Pull request description:

  fixes #173

  - bump the crate version to `0.24.0`.
  - updates the `CHANGELOG.md` with relevant changes: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.24.0...HEAD

ACKs for top commit:
  ValuedMammal:
    ACK 24d07e2cac

Tree-SHA512: 23060238562788fbc8d4ecdb2273db25329c4f645700d4d515a8d21a98b04bb421d0c8532d1b38ff566257d4df9095299d5b539261b88d090507350f80141d86
2025-07-30 11:04:08 -04:00
Leonardo Lima
24d07e2cac
chore(release): update CHANGELOG.md 2025-07-30 11:43:53 -03:00
Leonardo Lima
fbffdfe65e
chore(release): bump version to 0.24.0 2025-07-30 11:43:44 -03:00
merge-script
dd13e4e422
Merge bitcoindevkit/rust-electrum-client#170: feat: add batch_transaction_get_merkle
74ac97cfe4 feat: add `batch_transaction_get_merkle` (Leonardo Lima)

Pull request description:

  partially addresses https://github.com/bitcoindevkit/bdk/issues/1987

  - adds the new batch method for `blockchain.transaction.get_merkle`.
  - adds a new test for `batch_transaction_get_merkle` with 3 different
    txids and block_heights.

ACKs for top commit:
  ValuedMammal:
    ACK 74ac97cfe4

Tree-SHA512: 430e07d4cb3f0dc812a389b9b093b55f934ac72d32de5eebe5e072780dc4bd82c15796ba70ff3ae9354e822fac45e479618ff76e0360bd8d887edd7b62dd1327
2025-07-29 18:45:20 -04:00
merge-script
1372898ff2
Merge bitcoindevkit/rust-electrum-client#171: Install rustls's CryptoProvider based on features
f24458248a Install `rustls`'s `CryptoProvider` based on features (Elias Rohrer)

Pull request description:

  Previously, we'd already assume `use-rustls` to use the default `aws-lc-rs` provider and `use-rustls-ring` to use the `ring` `CryptoProvider`, e.g., for `NoCertificateVerification`.

  However, we **wouldn't** actually install the respective provider based on the features, leading to a **reachable** panic at runtime when user tried to access `ssl://` Electrum servers.

  Here, we fix this omission and install the default provider according to the configured features.

  (cc @oleonardolima @thunderbiscuit)

ACKs for top commit:
  ValuedMammal:
    ACK f24458248a
  oleonardolima:
    ACK f24458248a

Tree-SHA512: b502b97e4162c0dd46e17ccf1c0a0a8461158dbec06833d7d8715072fa4feeb87beb3ee2dd93c594689ef0c2ecd84ed52a4f9309d826bebb95f3c9e57dd933fb
2025-07-23 17:14:44 -04:00
Elias Rohrer
f24458248a
Install rustls's CryptoProvider based on features
Previously, we'd already assume `use-rustls` to use the default
`aws-lc-rs` provider and `use-rustls-ring` to use the `ring`
`CryptoProvider`, e.g., for `NoCertificateVerification`.

However, we **wouldn't** actually install the respective provider based
on the features, leading to a **reachable** panic at runtime when user
tried to access `ssl://` Electrum servers.

Here, we fix this omission and install the default provider according to
the configured features.
2025-07-18 14:04:40 +02:00
Leonardo Lima
74ac97cfe4
feat: add batch_transaction_get_merkle
- adds the new batch method for `blockchain.transaction.get_merkle`.
- adds a new test for `batch_transaction_get_merkle` with 3 different
  txids and block_heights.
2025-07-14 11:09:00 -03:00
valued mammal
7de4cb758d
Merge bitcoindevkit/rust-electrum-client#169: chore(release): bump version to 0.23.1
Some checks failed
CI / Test (1.75.0) (push) Has been cancelled
CI / Test (stable) (push) Has been cancelled
CI / Rust fmt (push) Has been cancelled
CI / Rust clippy (push) Has been cancelled
ed9784ee79 chore(release): bump version to `0.23.1` (Leonardo Lima)
e14f66b2f1 ci: use `ubuntu-latest` instead of `ubuntu-20.04` (Leonardo Lima)

Pull request description:

  fixes #168

  - updates the CI to run on `ubuntu-latest` GitHub runner, as the previously used `ubuntu-20.04` has been officially deprecated as of 2025-04-15, see: https://github.com/actions/runner-images/issues/11101.
  - updates the project version to `0.23.1`.
  - updates the `CHANGELOG.md` with changes to latest version.

ACKs for top commit:
  ValuedMammal:
    ACK ed9784ee79

Tree-SHA512: d448fc6fa2f9626dc4344ea3c8e7d1a948587f388803d923c755b454c73e4d6db860b70cba103a1092147c20656f7837e535e35eeefbead3dcb2f4260b25d839
2025-04-22 10:00:50 -04:00
Leonardo Lima
ed9784ee79
chore(release): bump version to 0.23.1
- updates the project version to `0.23.1`.
- updates the `CHANGELOG.md` with changes to latest version.
2025-04-17 17:30:39 -03:00
Leonardo Lima
e14f66b2f1
ci: use ubuntu-latest instead of ubuntu-20.04
- update the used ubuntu action runner, as `ubuntu-20.04` has been
  officially deprecated, see: https://github.com/actions/runner-images/issues/11101
2025-04-17 17:30:39 -03:00
valued mammal
862280a882
Merge bitcoindevkit/rust-electrum-client#160: fix: fix batch ordering issue
a3488f4ff9 ci: bump actions/cache to v4 (valued mammal)
0ee0a6ad7a test: add batch response ordering test (marshallyale)
6721929f22 fix: fix batch ordering issue (marshallyale)

Pull request description:

  Fixes issue #75
  This is my first open source pull request so I apologize for any formatting issues. Additionally, I don't know the repository as well as others so there may be a better way to implement the fix.

  I believe I found the root cause of this. I added a pull request to fix, but I'm going to copy/paste what I believe is causing the error.

  The main issue in the code is inside raw_client.rs inside the `recv` method implementation (snippet below):
  805ea0af30/src/raw_client.rs (L671-L685)

  When this is first called, the `self._reader_thread` will run. Inside the `self._reader_thread`, if the request id matches the response id, everything works fine. However, if the request id does not match the response id, we run the following code:
  805ea0af30/src/raw_client.rs (L602-L612)

  The channel that the response is sent back into is not unique, but rather all the channels share the same sender.clone() and receiver. The only validation that is done is to check that the request id is still being searched for inside `self.waiting_map`. This means that the receiver channel receives whatever the next response is into the channel without any validation that it matches the request id which happens here `match receiver.recv()?`.

  This is fixed by implementing unique channels for every request id. This fix can be verified with the code johnzweng used to show the issue

  If you run this with the initial code, it will error out after 1-10 cycles normally. However, after the fix this runs indefinitely.

ACKs for top commit:
  ValuedMammal:
    reACK a3488f4ff9

Tree-SHA512: c56d572c0d9e709352fde0c0438103fe4c0338e4b591d5290468b1658d6d73dbc818044e1b7ea6307e449a8d4380d9deba6adf2b89eb1dcbc119cec277fd721c
2025-03-28 12:44:41 -04:00
valued mammal
a3488f4ff9
ci: bump actions/cache to v4 2025-03-28 12:34:47 -04:00
marshallyale
0ee0a6ad7a
test: add batch response ordering test 2025-03-28 12:34:40 -04:00
marshallyale
6721929f22
fix: fix batch ordering issue
Fixes issue #75
Raw client waiting map was using the same channel
for every request/response. When items were put
back into the channel inside of _reader_thread
the waiting receiver in recv would just take the
next response without validating it on request id
request.
This fixes this by using unique channels for each
request response inside of the waiting map.
2025-03-28 12:34:28 -04:00
Lloyd Fournier
372eda9455
Merge pull request #163 from evanlinjin/electrum_api_deref
Allow types that reference `ElectrumApi` to also implement `ElectrumApi`
2025-02-24 11:29:45 +11:00
志宇
eb38c038b2
feat: allow references to ElectrumApi impls to also impl it 2025-02-08 03:27:23 +11:00
12 changed files with 676 additions and 81 deletions

View File

@ -5,7 +5,7 @@ name: CI
jobs:
test:
name: Test
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
env:
TEST_ELECTRUM_SERVER: electrum.blockstream.info:50001
strategy:
@ -17,7 +17,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Cache
uses: actions/cache@v2
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
@ -41,6 +41,7 @@ jobs:
- 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
@ -61,13 +62,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: dtolnay/rust-toolchain@v1
with:
toolchain: 1.84.0
toolchain: 1.90.0
components: clippy
- name: Rust Cache
uses: Swatinem/rust-cache@v2.2.1
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --all-features --all-targets -- -D warnings
- run: cargo clippy --all-features --all-targets -- -D warnings

View File

@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [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]
@ -45,9 +55,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.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...v0.19.0
[0.20.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.19.0...v0.20.0
[0.21.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.20.0...v0.21.0
[0.22.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.21.0...v0.22.0
[0.23.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.22.0...v0.23.0
[Unreleased]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.23.0...HEAD
[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

View File

@ -1,6 +1,6 @@
[package]
name = "electrum-client"
version = "0.23.0"
version = "0.24.1"
authors = ["Alekos Filini <alekos.filini@gmail.com>"]
license = "MIT"
homepage = "https://github.com/bitcoindevkit/rust-electrum-client"

34
justfile Normal file
View 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

View File

@ -2,6 +2,7 @@
use std::borrow::Borrow;
use std::convert::TryInto;
use std::ops::Deref;
use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::{block, Script, Transaction, Txid};
@ -9,6 +10,178 @@ use bitcoin::{block, Script, Transaction, Txid};
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 {
/// Gets the block header for height `height`.
@ -200,6 +373,17 @@ 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>;
@ -222,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>>();
}
}

View File

@ -62,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)];
@ -77,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,

View File

@ -327,6 +327,22 @@ 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)
@ -432,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();

View File

@ -55,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
}

View File

@ -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};
@ -406,6 +406,29 @@ impl RawClient<ElectrumSslStream> {
) -> Result<Self, Error> {
use std::convert::TryFrom;
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 {
@ -426,7 +449,7 @@ impl RawClient<ElectrumSslStream> {
builder
.dangerous()
.with_custom_certificate_verifier(std::sync::Arc::new(
#[cfg(feature = "use-rustls")]
#[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()),
@ -539,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);
}
@ -762,12 +784,10 @@ 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(
@ -775,9 +795,12 @@ impl<T: Read + Write> ElectrumApi for RawClient<T> {
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");
@ -796,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
@ -805,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);
@ -1102,6 +1125,17 @@ 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(
@ -1190,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]
@ -1428,6 +1482,98 @@ mod test {
));
}
#[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;

View File

@ -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,

View File

@ -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 {
@ -227,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 {

View File

@ -13,7 +13,7 @@ use bitcoin::Txid;
/// otherwise.
///
/// [`transaction_get_merkle`]: crate::ElectrumApi::transaction_get_merkle
/// [`BlockHeader`]: bitcoin::BlockHeader
/// [`BlockHeader`]: bitcoin::block::Header
pub fn validate_merkle_proof(
txid: &Txid,
merkle_root: &TxMerkleNode,