Compare commits

...

194 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
valued mammal
83747b131b
Merge bitcoindevkit/rust-electrum-client#161: chore: bump version to 0.23.0
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
0e459b6bd6 chore: bump version to 0.23.0 (valued mammal)

Pull request description:

  Bump crate version to 0.23.0 and update `CHANGELOG.md`

ACKs for top commit:
  notmandatory:
    ACK 0e459b6bd6
  oleonardolima:
    ACK 0e459b6bd6

Tree-SHA512: b50296b65b7533114674b90991ead8446f35625931bf153f09c149f4fcd3430c412a338ff4a1aa8a9616d4f0af6706a632a5268261431dbd257e6c15929fc7dd
2025-02-04 14:10:11 -05:00
valued mammal
0e459b6bd6
chore: bump version to 0.23.0 2025-02-04 11:47:36 -05:00
Steve Myers
805ea0af30
Merge bitcoindevkit/rust-electrum-client#159: ci(msrv): bump MSRV to 1.75 and rustls to 0.23.21
3dc4daceaf ci(clippy): bump to `1.84` (Leonardo Lima)
5a476fdfd9 deps(rustls): bump to `0.23.21` (Leonardo Lima)
ec90685324 ci(msrv): bump rust MSRV to `1.75` (Leonardo Lima)

Pull request description:

  partially fixes https://github.com/bitcoindevkit/bdk/issues/1750

  It bumps the project MSRV to `1.75.0` in order to follow along with LDK changes, and to remove the pinned version of `rustls`, which now is under `1.71` MSRV, bumping it to latest `0.23.21`.

  It also updates the `clippy` step on CI to use rust `1.84`, fixing the found clippy issues.

ACKs for top commit:
  tnull:
    > ACK [3dc4dac](3dc4daceaf)
  ValuedMammal:
    ACK 3dc4dac
  notmandatory:
    ACK 3dc4daceaf

Tree-SHA512: 3b9c2feffc8cc32cb9751a36406e7f0d5ed4692afa1af186621bef93da924def3e23c5132e004deef955e40bd7c3242a2d07c33e3843b1b76b613be2f54afefe
2025-01-23 09:42:08 -06:00
Leonardo Lima
3dc4daceaf
ci(clippy): bump to 1.84
- updates the rust version on clippy step to `1.84`
- fixes the found clippy issues
2025-01-20 13:57:27 -03:00
Leonardo Lima
5a476fdfd9
deps(rustls): bump to 0.23.21 2025-01-20 13:57:26 -03:00
Leonardo Lima
ec90685324
ci(msrv): bump rust MSRV to 1.75
- bumps the MSRV on CI to 1.75.
- updates the `Cargo.toml` rust-version to 1.75.
- removes the guideline to pin `rustls` version, as previously required
  by old 1.63 MSRV.
- update `README.md` badges to `1.75` MSRV.
- update `clippy.toml` to `1.75` MSRV.
2025-01-20 13:57:25 -03:00
Steve Myers
15f753f05f
Merge bitcoindevkit/rust-electrum-client#158: chore: Enforce min rustls version to 0.23.19
ed0cdae329 chore: bump version to 0.22.1, update CHANGELOG (Steve Myers)
7ef3ff6873 chore: Enforce min `rustls` version to 0.23.19 (志宇)

Pull request description:

  `rustls` versions 0.23.18 and 0.23.19 contains fix for vulnerability RUSTSEC-2024-0399. However, 0.23.18 bumps MSRV to 1.71. 0.23.19 reverts MSRV back to 1.63.

  We enforce min `rustls` version to 0.23.19 to make it easier to compile on MSRV and ensure we include the RUSTSEC-2024-0399 fix.

  Note that in CI, I decided to pin `rustls` dependency to 0.23.19 explicitly. This is because in future versions of `rustls`, the MSRV will be changed to 1.71.

  Context: https://github.com/rustls/rustls/pull/2244

ACKs for top commit:
  notmandatory:
    ACK ed0cdae329

Tree-SHA512: 9b6319d520a1002b3404743f051019a5ff63f53a9fe37ed080ed8e761f3db3edd446d014d2145e9c098279d7b3e6e017e82dd633baeef202a904afb37c49c4d9
2024-12-05 21:34:34 -06:00
Steve Myers
ed0cdae329
chore: bump version to 0.22.1, update CHANGELOG 2024-12-05 21:28:31 -06:00
志宇
7ef3ff6873
chore: Enforce min rustls version to 0.23.19
`rustls` versions 0.23.18 and 0.23.19 contains fix for vulnerability
RUSTSEC-2024-0399. However, 0.23.18 bumps MSRV to 1.71. 0.23.19 reverts
MSRV back to 1.63.

We enforce min `rustls` version to 0.23.19 to make it easier to compile
on MSRV and ensure we include the RUSTSEC-2024-0399 fix.

Note that in CI, I decided to pin `rustls` dependency to 0.23.19
explicitly. This is because in future versions of `rustls`, the MSRV
will be changed to 1.71.
2024-12-05 21:14:43 -06:00
Steve Myers
f00b9998d1
Merge bitcoindevkit/rust-electrum-client#157: ci: pin msrv dep version for rustls
Some checks failed
CI / Rust fmt (push) Has been cancelled
CI / clippy_check (push) Has been cancelled
CI / Test (1.63.0) (push) Has been cancelled
CI / Test (stable) (push) Has been cancelled
b0a1bfb0cb ci: pin msrv dep version for rustls (Steve Myers)

Pull request description:

  I'm not thrilled about having to pin `rustls` to meet our MSRV, but we need to make a new release and can't count on rustls/rustls#2239 being accepted by the `rustls` team.

ACKs for top commit:
  luisschwab:
    utACK b0a1bfb0cb
  ValuedMammal:
    utACK b0a1bfb0cb
  oleonardolima:
    ACK b0a1bfb0cb

Tree-SHA512: 1f37b423f3e10a7e3fa4e2072d5ad38b96341a91f78c1bcffc011dedc08071bd176b556646e40e635c3b558e6b771a4a7baa85b10d14da64d2cd385a69342ee4
2024-11-25 20:44:46 -08:00
Steve Myers
b0a1bfb0cb
ci: pin msrv dep version for rustls 2024-11-25 09:34:19 -08:00
Steve Myers
6e2a9b4b64
Merge bitcoindevkit/rust-electrum-client#156: chore: bump version to 0.22.0 and update CHANGELOG.md
43da8e9e6b chore: bump version to 0.22.0 and update CHANGELOG.md (Steve Myers)

Pull request description:

ACKs for top commit:
  LagginTimes:
    ACK 43da8e9e6b
  oleonardolima:
    ACK 43da8e9e6b

Tree-SHA512: 0acab16cce96cef3d5f10c1788d69d372395b22755344df0c5121c9d6df173af132f206959271548fbd1ced2edf81ce13a1b691f135fb11fb7438c7c7e3c90e1
2024-11-25 09:06:03 -08:00
Steve Myers
43da8e9e6b
chore: bump version to 0.22.0 and update CHANGELOG.md 2024-11-21 20:32:29 -06:00
valued mammal
3c20ca18b6
Merge bitcoindevkit/rust-electrum-client#145: Update URLs in Cargo.toml
d917f891e8 Update URLs in `Cargo.toml` (Elias Rohrer)

Pull request description:

  .. while it's still magical, the URLs seem to be outdated ..

ACKs for top commit:
  ValuedMammal:
    ACK d917f891e8

Tree-SHA512: cf652d6949d8be67f5884457f388e8fb5bc68a54d826e387dcc3542c00c2d71d62bad1dd9cb78a95f55b4f19d1beec2379540b7fea2e610a71817b8cb743f90d
2024-11-19 14:02:48 -05:00
Elias Rohrer
d917f891e8
Update URLs in Cargo.toml
.. while it's still magical, the URLs seem to be outdated ..
2024-11-19 13:52:11 -05:00
valued mammal
1fcddcba9b
Merge bitcoindevkit/rust-electrum-client#155: feat: add id_from_pos support
a871b084ec feat: add `id_from_pos` support (Wei Chen)

Pull request description:

  This PR introduces the `blockchain.transaction.id_from_pos` feature from Electrum. This functionality is essential for an ongoing implementation in `bdk_electrum`, which requires retrieving the coinbase transaction from a block at a specified `height`. Currently, there is no straightforward method to achieve this. By implementing the `id_from_pos` feature, we will effectively address this gap.

ACKs for top commit:
  oleonardolima:
    utACK a871b084ec
  ValuedMammal:
    ACK a871b084ec
  evanlinjin:
    ACK a871b084ec

Tree-SHA512: e8f1ab44473f67030b4f19e766fcee472b7cfc1dac6df8c9f0e08d966e0ed48289de03768771623b9fc7e7ef15dacdb7e3b26c04f9b0de7566d67cc7585a7fa6
2024-11-19 11:55:58 -05:00
Wei Chen
a871b084ec
feat: add id_from_pos support 2024-11-12 19:18:04 +08:00
valued mammal
6fe96fddae
Merge bitcoindevkit/rust-electrum-client#150: fix(insecure-tls): NoCertificateVerification implementation
05771a81d7 fix: `NoCertificateVerification` implementation (Leonardo Lima)

Pull request description:

  fixes #149 https://github.com/bitcoindevkit/bdk/issues/1598
  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  ### Description

  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->

  It has been noticed some issues by both users and developers, as reported in #149, https://github.com/bitcoindevkit/bdk/issues/1598 and https://github.com/wizardsardine/liana/issues/1300, when using the library with `use-rustls-{ring}` feature to connect to electrum servers that use self-signed certificates, there are even some issues when trying to connect to `ssl://electrum.blockstream.info:50002` server.

  To connect in an insecure manner either with `rustls` or `openssl` features, the user can set the `validate_domain` field in the `Config` to false, this will either set the `SslVerifyMode::NONE` when using `openssl`, or use the custom `NoCertificateVerification` for the
  `rustls::client::danger::ServerCertVerifier` trait when using `rustls`, that said it should ignore the certificate verification when used.

  At the current library state, it's failing because we didn't set up the supported `rustls::SignatureScheme` properly, returning an empty vector at the moment. This PR focuses on fixing this issue by relying on the `CryptoProvider` in usage to get the correct and supported signature schemes.

  As part of the research to understand the problem, I've noticed that ideally, we should still use both the `rustls::webpki::verify_tls12_signature` and `rustls::webpki::verify_tls12_signature` and only rely on `rustls::client::danger::ServerCertVerified::assertion()` to ignore the certificate verification, however, it would still fail in scenarios such as https://github.com/bitcoindevkit/bdk/issues/1598 which uses X.509 certificates with any version other than 3 (it uses version 1), see [here](1a0d1646d0/src/cert.rs (L202-L218)).

  I kept the current behavior to also ignore the TLS signature, but I still would like to bring this to the discussion, should we validate it properly and update the documentation to mention the `webpki` limitation instead ?

  ### Notes to the reviewers

  I kept the current behavior to also ignore the TLS signature, but I still would like to bring this to the discussion, should we validate it properly and update the documentation to mention the `webpki` limitation instead ?

  <!-- In this section you can include notes directed to the reviewers, like explaining why some parts
  of the PR were done in a specific way -->

  ### Changelog notice

  - Updates the `NoCertificateVerification` implementation for the
  `rustls::client::danger::ServerCertVerifier` to use the `rustls::SignatureScheme` from `CryptoProvider` in use.

  <!-- Notice the release manager should include in the release tag message changelog -->
  <!-- See https://keepachangelog.com/en/1.0.0/ for examples -->

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

  #### New Features:

  * [ ] I've added tests for the new feature
  * [ ] I've added docs for the new feature

  #### Bugfixes:

  * [ ] This pull request breaks the existing API
  * [ ] I've added tests to reproduce the issue which are now passing
  * [ ] I'm linking the issue being fixed by this PR

ACKs for top commit:
  LLFourn:
    ACK 05771a81d7
  ValuedMammal:
    ACK 05771a81d7
  notmandatory:
    ACK 05771a81d7

Tree-SHA512: f74dedf458853fb19cd21dedb5b92158acd865ee0ab0fd6bbb2b3e267bac22edc7cf004d2dc0068f66d2665d87e6dd419231710a02317e3b2bfaa9f408b30759
2024-10-23 12:51:45 -04:00
Leonardo Lima
05771a81d7
fix: NoCertificateVerification implementation
It updates the `NoCertificateVerification` implementation of
`rustls::client::danger::ServerCertVerifier` trait, it keeps the usage
of both `ServerCertVerified::assertion()` and
`HandshakeSignatureValid::assertion()` usage, but now instead of having
an empty vector vector of supported `SignatureScheme`, it uses the ones
supported by the used `CryptoProvider`.
2024-09-27 14:36:14 -03:00
Steve Myers
0b97659b38
Merge bitcoindevkit/rust-electrum-client#141: Bump version to 0.21.0 and update CHANGELOG.md
Some checks failed
CI / Test (1.63.0) (push) Has been cancelled
CI / Test (stable) (push) Has been cancelled
CI / Rust fmt (push) Has been cancelled
CI / clippy_check (push) Has been cancelled
4dd7e2135e Bump version to 0.21.0 and update CHANGELOG.md (Steve Myers)

Pull request description:

  Bumped crate version to 0.21.0 and added below to changelog:

  ## 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

ACKs for top commit:
  oleonardolima:
    ACK 4dd7e2135e
  ValuedMammal:
    ACK 4dd7e2135e

Tree-SHA512: 3fcec2fb437733eac235bccb1b9c8f6b706e7a713c71de85016adc93f7db128ca6eadb5e9d1d44df27f1b49cce139b222aa9c21343afcf25befdf80a47442e51
2024-08-06 16:04:34 -05:00
Steve Myers
4dd7e2135e
Bump version to 0.21.0 and update CHANGELOG.md 2024-08-06 15:43:01 -05:00
Steve Myers
9f09f50217
Merge bitcoindevkit/rust-electrum-client#139: chore: set rust edition to 2021, fix clippy, add ci fmt and clippy checks
0dd51408a3 chore(ci): add fmt and clippy checks (Steve Myers)
e7af332684 chore: fix clippy errors and set msrv to 1.63 (Steve Myers)
84f4f609b9 chore: set rust edition to 2021 (Steve Myers)

Pull request description:

  A little house keeping to avoid the warning about missing the edition. After setting the rust edition to 2021 I had to fix clippy warnings.  I also added CI fmt and clippy checks.

ACKs for top commit:
  ValuedMammal:
    ACK 0dd51408a3
  oleonardolima:
    ACK 0dd51408a3

Tree-SHA512: ee37c3ef06e217f76dcb622cdbb53e9b5c94163b789ca584037d5a8bb4bdef13c6cf251b555ac0e0ede309d7436295f1ede4e2ac6c0dc141c9bf2e3d6ecf9238
2024-08-06 15:31:10 -05:00
Steve Myers
0dd51408a3
chore(ci): add fmt and clippy checks 2024-08-06 15:06:16 -05:00
Steve Myers
e7af332684
chore: fix clippy errors and set msrv to 1.63 2024-07-31 21:37:10 -05:00
Steve Myers
84f4f609b9
chore: set rust edition to 2021 2024-07-31 18:50:13 -05:00
Steve Myers
746a0e6463
Merge bitcoindevkit/rust-electrum-client#134: refactor: make validate_merkle_proof more efficient
3a1f1bffb0 refactor: make `validate_merkle_proof` more efficient (志宇)

Pull request description:

ACKs for top commit:
  notmandatory:
    ACK 3a1f1bffb0

Tree-SHA512: 995d1582bc13d21c13b76dcd5e5edb633a59588e713b1e0aaf33363e17c47aafb22c7f250483e60cdc68e85fc94f041a0c39815160dc2592bedf959679f804dc
2024-07-31 18:20:24 -05:00
Steve Myers
54797a0be8
Merge bitcoindevkit/rust-electrum-client#135: Add use-rustls-ring feature
8d71f9597c feat: add use-rustls-ring feature (thunderbiscuit)

Pull request description:

  This PR adds the ability to build the client using the `ring` dependency for `rustls` instead of the new default `aws-lc-rs`.

  As of the [`0.23.0` release](https://github.com/rustls/rustls/releases/tag/v%2F0.23.0), rustls changed its default cryptography provider to [aws-lc-rs](https://crates.io/crates/aws-lc-rs). This new library is actually a set of bindings to a C library maintained by AWS, and they provide prebuilt bindings for [some platforms](https://aws.github.io/aws-lc-rs/platform_support.html) but not all. On these other platforms, the compilation step will attempt to build the bindings, requiring extra dependencies (CMake, libclang and others depending on the platform). This compilation step is what is currently breaking our Android and Swift builds for bdk-ffi. It is certainly possible to build the bindings (and the AWS docs on it are very nice), but for some reason I have not been able to make it work everywhere yet (local, CI, Windows).

  This PR enables us to use the previous default `ring` library for rustls. I basically have to turn off the default features on `rustls` and re-enable all of them _except_ for the `aws_lc_rs`. We also have a few feature-gated constructs in the library, for which I needed to add the new proposed `use-rustls-ring` feature in order to make all of this work for us. Let me know if there are maybe better ways to achieve this!

ACKs for top commit:
  oleonardolima:
    ACK 8d71f9597c
  notmandatory:
    ACK 8d71f9597c

Tree-SHA512: 5ea8bfac7a18700e32035518e9e8253252c8ff9064b011e14a060ac8ed7b478876ee408ce06a89af9e53de837ffa9a13fbe5030d12b48a76558fd4e8187e5651
2024-07-31 17:09:46 -05:00
thunderbiscuit
8d71f9597c
feat: add use-rustls-ring feature 2024-07-31 15:06:11 -05:00
志宇
3a1f1bffb0
refactor: make validate_merkle_proof more efficient 2024-06-18 12:03:02 +08:00
Steve Myers
64c77ee1bc
Merge bitcoindevkit/rust-electrum-client#128: ci: add test with MSRV 1.63.0
Some checks failed
CI / Test (1.63.0) (push) Has been cancelled
CI / Test (stable) (push) Has been cancelled
4b421084e9 ci: add test with MSRV 1.63.0 (Steve Myers)

Pull request description:

  Since the main BDK crates are changing to MSRV 1.63.0 as is LDK the CI for this project should also have an MSRV and test against it in CI.

  Also rustc 1.63 is the version shipped with the current debian stable (bookworm): https://packages.debian.org/stable/rust/rustc

ACKs for top commit:
  oleonardolima:
    utACK 4b421084e9
  storopoli:
    ACK 4b42108
  ValuedMammal:
    ACK 4b421084e9 looks good to me

Tree-SHA512: baff75887008a586af0ef0c4f50dd8c7c3de782f2dc6bc7d94c072c95205d4ef1a339860b3674dd781ad5558b98b071ed49b3a48507219b747e14c394dfe0a7b
2024-05-28 10:03:22 -05:00
Steve Myers
4b421084e9
ci: add test with MSRV 1.63.0 2024-05-27 12:56:17 -05:00
Steve Myers
1899234f2e
Merge bitcoindevkit/rust-electrum-client#133: chore(deps): upgrade rust-bitcoin to 0.32.0
6cf723504f deps: bump crate version to `0.20.0` (Leonardo Lima)
78cb066966 chore(deps)): upgrade `rust-bitcoin` to `0.32.0` (Leonardo Lima)

Pull request description:

  <!-- You can erase any parts of this template not applicable to your Pull Request. -->

  partially fixes [#1422](https://github.com/bitcoindevkit/bdk/issues/1422)

  ### Description

  It updates the rust-bitcoin to 0.32.0, the `bitcoin` crate dependency.

  _NOTE: The overall BDK update to `0.32.0` still requires and depends on some other crates, please refer to [#1422](https://github.com/bitcoindevkit/bdk/issues/1422)._

  <!-- Describe the purpose of this PR, what's being adding and/or fixed -->

  ### Notes to the reviewers

  It's open for any comments.
  <!-- In this section you can include notes directed to the reviewers, like explaining why some parts
  of the PR were done in a specific way -->

  ### Changelog notice

  - Update the `bitcoin` crate dependency to `0.32.0`

  <!-- Notice the release manager should include in the release tag message changelog -->
  <!-- See https://keepachangelog.com/en/1.0.0/ for examples -->

  ### Checklists

  #### All Submissions:

  * [x] I've signed all my commits
  * [x] I followed the [contribution guidelines](https://github.com/bitcoindevkit/bdk/blob/master/CONTRIBUTING.md)
  * [x] I ran `cargo fmt` and `cargo clippy` before committing

ACKs for top commit:
  notmandatory:
    utACK 6cf723504f

Tree-SHA512: c1e170d8da7687b40916b7c2de48f08ca393a2af79522abc85933bae1da6f79d2aa05d59c73b99dcbd01cc7add4def1b8a14e7858550d4e7a007c07279b45854
2024-05-27 12:54:09 -05:00
Steve Myers
1bbae7d5a0
Merge bitcoindevkit/rust-electrum-client#132: Upgrade rustls to 0.23
28b1aaa0c3 upgrade rustls to 0.23 (Nick Farrow)

Pull request description:

  With rustls 0.23 there is no longer a dependency on ring, allowing easier compilation for various targets.

  Not super confident with my updates to `ServerCertVerifier` and `Der` of certificates (is this being tested?), needs review.

ACKs for top commit:
  notmandatory:
    utACK 28b1aaa0c3

Tree-SHA512: 6561c4d20d446d86ca7a6c04ddb5a8acb136756606c82ca00e9b4a1f0eb2a3b00120d6db475f14474a89ebaa2ad600208d51c777cb5aeed0dcf62335a84fee5a
2024-05-27 12:53:26 -05:00
Leonardo Lima
6cf723504f
deps: bump crate version to 0.20.0 2024-05-14 22:09:00 -03:00
Leonardo Lima
78cb066966
chore(deps)): upgrade rust-bitcoin to 0.32.0 2024-05-14 22:08:49 -03:00
Nick Farrow
28b1aaa0c3
upgrade rustls to 0.23
* With rustls 0.23 there is no longer a dependency on ring, allowing for
  easier compilation for various targets.
2024-04-22 18:30:14 +10:00
Steve Myers
898f230081
Merge bitcoindevkit/rust-electrum-client#126: Bump version to 0.19.0 and add CHANGELOG.md
Some checks failed
CI / Test (push) Has been cancelled
ef9fd6bf50 Bump version to 0.19.0 and add CHANGELOG.md (Steve Myers)

Pull request description:

  Besides bumping the version I've also added a simple changelog that list the PRs in this (0.19.0) and the prior (0.18.0) release. The main reason for this release is the bump of the rust-bitcoin version to 0.31.0 which is needed to upgrade dependent projects like bdk to also use rust-bitcoin 0.31.0.

ACKs for top commit:
  evanlinjin:
    ACK ef9fd6bf50

Tree-SHA512: c18a0312915adfad40ef900ac4f1ee10454863f7a29882f1fa8f967f4c13df34b4e3929703614d0c3e6b8a189338f3a4d15b8d4598f68b87948454184fb13957
2023-12-19 08:38:16 -06:00
Steve Myers
ef9fd6bf50
Bump version to 0.19.0 and add CHANGELOG.md 2023-12-11 21:18:21 -06:00
Steve Myers
fd81717f8e
Merge bitcoindevkit/rust-electrum-client#125: enforce timeout on initial socks5 proxy connection
d8554fb550 enforce timeout on initial socks5 proxy connection (conduition)

Pull request description:

  This PR fixes a bug which excepted initial SOCKS5 proxy connection attempts from the punctual enforcement of timeouts.

  Before this change, invoking `Socks5Stream::connect` (or `Socks5Stream::connect_with_password`) could block for much longer than the configured timeout. In practice, this manifested as `Client::from_config` apparently failing to respect the timeout specified in the `Config` passed to it. AFAICT this only applied to SOCKS proxy connections.

  ## Example

  To demonstrate, here is a simple example program which attempts to connect to an unreachable electrum server with a 10 second timeout.
  ```rust
  use electrum_client::{Client, ConfigBuilder, Socks5Config};

  fn main() {
      let proxy = Socks5Config::new("127.0.0.1:9050");
      let config = ConfigBuilder::new()
          .socks5(Some(proxy))
          .timeout(Some(10))
          .build();

      let start = std::time::SystemTime::now();
      let result = Client::from_config(
          "tcp://bejqtnc64qttdempkczylydg7l3ordwugbdar7yqbndck53ukx7wnwad.onion:50001",
          config,
      );
      match result {
          Ok(_) => {
              println!("Successfully connected")
          }
          Err(e) => {
              println!(
                  "failed to connect after {:.2}s: {e}",
                  start.elapsed().unwrap().as_secs_f64()
              );
          }
      };
  }
  ```

  You'd expect the connection attempt to always fail at around 10 seconds, but in fact most attempts take considerably longer.

  ```
  $ for i in {1..10} ; do cargo run -q ; done
  failed to connect after 7.65s: host unreachable
  failed to connect after 47.78s: host unreachable
  failed to connect after 18.17s: host unreachable
  failed to connect after 29.24s: host unreachable
  failed to connect after 16.15s: host unreachable
  failed to connect after 14.40s: host unreachable
  failed to connect after 16.89s: host unreachable
  failed to connect after 9.93s: host unreachable
  failed to connect after 8.81s: host unreachable
  failed to connect after 17.80s: host unreachable
  ```

  ## Cause and Fix

  This was happening because the private method `Socks5Stream::connect_raw` [only respected the `timeout` parameter for _the initial connection to the proxy address_](5ecb26fd7d/src/socks/v5.rs (L200-L205)).

  Once that TCP socket is established, the SOCKS5 client code must exchange a couple of messages with the proxy itself: One request/response cycle to authenticate, and then another request/response cycle to configure the forward proxy to the ultimate destination address. The latter of these two request/response cycles could block for long periods of time, in the case where the proxy was responsive but the ultimate destination was unresponsive.

  Since no timeout was set on the socket at this stage, the `Socks5Stream` code would wait for an indefinite amount of time for a reply from the proxy, usually only once the proxy itself times out and finally sends a reply.

  My suggested fix in this PR is to set the read/write timeouts immediately on the socket connecting to the proxy, so that if the proxy doesn't reply in time, we return an error to the caller promptly.

ACKs for top commit:
  RCasatta:
    utACK d8554fb550

Tree-SHA512: 4bc0ca203465c0d9722680de7251ee49dbea6d5a2b2a833a1ed42e792342a7ac977ad72649603caacd3e58d233b1a516af1ad40c6935addb8165b720d823cf42
2023-12-08 16:11:00 -06:00
conduition
d8554fb550
enforce timeout on initial socks5 proxy connection 2023-12-07 19:05:00 +00:00
Steve Myers
91228df8a5
Merge bitcoindevkit/rust-electrum-client#121: Upgrade bitcoin
dd3c171a7a Upgrade bitcoin (Tobin C. Harding)

Pull request description:

  Upgrade bitcoin dependency to `rust-bitcoin v0.31.0-rc1`:

  Allows us to remove the dependency on `bitcoin-private` because the `hex` stuff is exposed by `bitcoin` now.

ACKs for top commit:
  notmandatory:
    ACK dd3c171a7a

Tree-SHA512: 9082d3c2136445230bb23669f83fed58c90d1baf28d35527fc3dea1d40c9d3bdebeddbc44b3bdbc8e79c9b701e09cb453c018af0bc4ac8bcd1c4a14d11c90e39
2023-12-07 11:27:10 -06:00
Tobin C. Harding
dd3c171a7a
Upgrade bitcoin
Upgrade bitcoin dependency to `rust-bitcoin v0.31.0`:

Allows us to remove the dependency on `bitcoin-private` because the
`hex` stuff is exposed by `bitcoin` now.
2023-12-07 17:16:46 +11:00
Steve Myers
e4d2b1d194
Merge bitcoindevkit/rust-electrum-client#122: Add utility to validate GetMerkleRes
54fd52d898 Add test coverage for `validate_merkle_proof` (Elias Rohrer)
fe33e19bda Add utility for validating a Merkle inclusion proof (Elias Rohrer)
dd872d6714 Make response types `Clone` (Elias Rohrer)

Pull request description:

  I recently needed to validate a Merkle inclusion proof as retrieved via `transaction_get_merkle`.

  As I figured it might be useful to other people, too, we add it here as a simple utility method.

ACKs for top commit:
  notmandatory:
    ACK 54fd52d898

Tree-SHA512: aac12160d5b91a011988f45013eb92924c2dfb244c1720e73dc5bcb731e69065c38e022502c756100d8ee6c9af06efa0de9bbfbb2b9e3c2e34d3223539206e1c
2023-12-06 22:55:03 -06:00
Steve Myers
dacd772d5e
Merge bitcoindevkit/rust-electrum-client#117: Remove webpki and bump webpki-roots to v0.25
a331ae8059 Remove `webpki` and bump `webpki-roots` to `v0.25` (Yuki Kishimoto)

Pull request description:

  I noticed that `webpki` dependency is no longer maintained and that has a high severity vulnerability.
  This PR remove the `webpki` dependency and bump `webpki-roots` to v0.25

ACKs for top commit:
  notmandatory:
    ACK a331ae8059

Tree-SHA512: 63e9498dc0d56a07e7dd09dd43ca9a924d7e9ebb09934f2c762e64c9ce163cd58edb4d1563db4eba18a0fdf22642cb7d801940baeb97b6ce5473970b739278d4
2023-12-06 22:53:02 -06:00
Steve Myers
8b31e5fe41
Merge bitcoindevkit/rust-electrum-client#94: Add Batch::raw and improve docs
adb0c7444e Add `Batch::raw` and improve docs (志宇)

Pull request description:

  Being able to add raw requests to `Batch` makes sense from an API standpoint because we already allow raw non-batched requests. This is also useful when the electrum server API gets an updated version and our client is unable to keep up.

  Additional to this, I have improved the documentation and made `Call` private (since `Call` is never used externally).

ACKs for top commit:
  notmandatory:
    ACK adb0c7444e

Tree-SHA512: 808dccf1152b750881e45a9709fb4127835ecff3da5ecccffcb9f03e62171192c58154860195db7d3d3467ae8e3e450bba845ff4e8d4dffb302c3d8d6eb837fd
2023-12-06 22:45:25 -06:00
Elias Rohrer
54fd52d898
Add test coverage for validate_merkle_proof 2023-11-10 09:51:04 +01:00
Elias Rohrer
fe33e19bda
Add utility for validating a Merkle inclusion proof
... as retrieved via `transaction_get_merkle`.
2023-11-10 09:51:04 +01:00
Elias Rohrer
dd872d6714
Make response types Clone
.. as it's convenient.
2023-10-27 16:32:51 +02:00
Yuki Kishimoto
a331ae8059
Remove webpki and bump webpki-roots to v0.25 2023-08-25 12:17:38 +02:00
Daniela Brozzoni
5ecb26fd7d
Merge bitcoindevkit/rust-electrum-client#116: Bump to version 0.18
3f6dd0c796 Bump to version 0.18 (Daniela Brozzoni)

Pull request description:

ACKs for top commit:
  RCasatta:
    ACK 3f6dd0c796

Tree-SHA512: f42f2008d79b9a0334c375652842fb2745d2481422a24f27dd4e350bcc17e47f0da2888545a344e1438ede288ed2ad0de8b359b87004fbe059f310aad886c60a
2023-08-05 21:28:48 +02:00
Daniela Brozzoni
3f6dd0c796
Bump to version 0.18
Some checks failed
CI / Test (push) Has been cancelled
2023-08-03 15:48:20 +02:00
Daniela Brozzoni
ed9bb09d02
Merge bitcoindevkit/rust-electrum-client#115: Revert "errors if expecing headers notification but not subscribed"
2d44350b44 Revert "errors if expecing headers notification but not subscribed" (Riccardo Casatta)

Pull request description:

  This reverts commit b86f2bb22c.

  Some errors started to happen in downstream tests after this commit

  #114

ACKs for top commit:
  danielabrozzoni:
    utACK 2d44350b44

Tree-SHA512: d31174055f4245cc9d99f336b166a44271067b8daecaf2bb55d507ecfa4eb557b9802d576742fba59fd4dfda3f45fc76f02d7896af31353f19fc3a38698ac5a2
2023-08-03 12:28:07 +02:00
Riccardo Casatta
2d44350b44
Revert "errors if expecing headers notification but not subscribed"
This reverts commit b86f2bb22c.

Some errors started to happen in downstream tests after this commit
2023-07-21 15:34:12 +02:00
Daniela Brozzoni
84b1435df5
Merge bitcoindevkit/rust-electrum-client#111: Bump to v0.17.0
7535debbd6 Bump to v0.17.0 (Daniela Brozzoni)

Pull request description:

ACKs for top commit:
  danielabrozzoni:
    self-ACK 7535debbd6

Tree-SHA512: 92292e6d93fcd79fd542b4153cd7689050525bb3834a556cd8e9604307f13ebb4e7012c7c4c9aee0a744046fbf9646e3b05eb93770c49b8252de7f7f2c5debbd
2023-07-13 18:51:19 -06:00
Daniela Brozzoni
7535debbd6
Bump to v0.17.0
Some checks failed
CI / Test (push) Has been cancelled
2023-07-13 18:49:46 -06:00
Daniela Brozzoni
87348e5809
Merge bitcoindevkit/rust-electrum-client#112: Ignore other unrecoverable errors in retry client
7c11f208c0 Ignore other unrecoverable errors in retry client (Riccardo Casatta)
dbec56a95b tests: fix list_unspent tests (Riccardo Casatta)

Pull request description:

  similar to https://github.com/bitcoindevkit/rust-electrum-client/pull/107

ACKs for top commit:
  danielabrozzoni:
    utACK 7c11f208c0

Tree-SHA512: 74768e197b754dd98bbd435abaa5bbea018e65103615fba0b874252202c59fee5ec07ca90c0a19e112d3a1351e7ac172bef3bdff2eb97d245eb981c2a58660f9
2023-07-13 18:48:06 -06:00
Riccardo Casatta
7c11f208c0
Ignore other unrecoverable errors in retry client 2023-07-13 09:31:24 +02:00
Riccardo Casatta
dbec56a95b
tests: fix list_unspent tests
the mtgox address used got too many txs, resulting in a server error:
"too many txs"

The address is changed with the peter todd sha256 breaking challenge, which
should be very difficult to spend :) and as of now has just a few txs
2023-07-13 09:31:21 +02:00
Daniela Brozzoni
20493aa475
Merge bitcoindevkit/rust-electrum-client#110: Update rustls dependency
f5a438cd0a Bump version to 0.16.0 (Tobin C. Harding)
9a7cc14c94 Update rustls dependency to version 0.21 (Tobin C. Harding)

Pull request description:

  Update the `rustls` dependency to version 0.21 and bump the version of this crate to 0.16 so the change can be released.

  I don't know what stage in your release cycle you guys are up to so I put the version bump as a separate patch - can drop it if not needed.

  Done to help with https://github.com/bitcoindevkit/rust-esplora-client/pull/51

ACKs for top commit:
  danielabrozzoni:
    utACK f5a438cd0a

Tree-SHA512: 106cbe57651a6e1365f87836598cbec9b1e837f0376ddc1c56cb75e5615543659f5c05cea7d5eb8da8b6cf278fcbe6ef866a019b9829db40cc8055798eb0f541
2023-06-28 09:46:12 +02:00
Tobin C. Harding
f5a438cd0a
Bump version to 0.16.0
Some checks failed
CI / Test (push) Has been cancelled
We just updated the rustls dependency, in order for downstream crates to
take advantage of this change bump the version so we can release.
2023-06-28 07:57:25 +10:00
Tobin C. Harding
9a7cc14c94
Update rustls dependency to version 0.21
Update to the latest version of `rustls`.
2023-06-28 07:56:21 +10:00
Daniela Brozzoni
c84507e45b
Merge bitcoindevkit/rust-electrum-client#109: Errors if expecting headers notification but not subscribed
b86f2bb22c errors if expecing headers notification but not subscribed (Riccardo Casatta)

Pull request description:

  the opposite, erroring when subscribing multiple times, is not handled because clients could call multiple times to trigger the server to reply with the tip

ACKs for top commit:
  danielabrozzoni:
    utACK b86f2bb22c - changes look good to me, didn't test locally

Tree-SHA512: e1a21223448e708cc054271eb1ac5285dc98bdadf7497a856fc4a19ff51655879352ca6ef9f3fb5ec5f9071dd7ee50b2e44d4c65e70694e5d9fa53d280179dd2
2023-06-27 15:12:08 +02:00
Riccardo Casatta
b86f2bb22c
errors if expecing headers notification but not subscribed
the opposite, erroring when subscribing multiple times, is not handled because
clients could call multiple times to trigger the server to reply with the tip
2023-06-20 15:45:11 +02:00
Daniela Brozzoni
d2dbab93be
Merge bitcoindevkit/rust-electrum-client#108: Bump to v0.15.1
0f356ef94d Bump to v0.15.1 (Alekos Filini)

Pull request description:

  Release as a bug fix with #107 merged

ACKs for top commit:
  danielabrozzoni:
    ACK 0f356ef94d

Tree-SHA512: 3da6a4701707549d62cfa936f5c185eec0967766797dfbacce1fb68b716b90f7e7e163c797dfb7792435cad2769a43e1b23247b0ec86f9a098a79ea4589ee281
2023-05-16 17:49:47 +02:00
Alekos Filini
0f356ef94d
Bump to v0.15.1
Some checks failed
CI / Test (push) Has been cancelled
2023-05-14 12:57:11 +02:00
Alekos Filini
27eb4d0cb0
Merge bitcoindevkit/rust-electrum-client#107: Retry client should return AlreadySubscribed error
3a61037733 Retry client should return AlreadySubscribed error (Riccardo Casatta)

Pull request description:

  Users may leverage the Error to avoid remembering client-side the subscription status. eg. Always subscribing and calling script_pop in case AlreadySubscribed is returned.

  Backport for version 0.14.0

ACKs for top commit:
  afilini:
    ACK 3a61037733

Tree-SHA512: d6542227ca03b9f3755cec05115f86de54789cfc49fc87b603b653e0458f6b03ae9fea57fa110221395a4e33cf62a5b2bd7639c4269ae5291e4c92c99ef41eba
2023-05-14 12:56:43 +02:00
Riccardo Casatta
3a61037733
Retry client should return AlreadySubscribed error
Users may leverage the Error to avoid remembering client-side the subscription
status. eg. Always subscribing and calling script_pop in case AlreadySubscribed
is returned.
2023-05-12 11:24:10 +02:00
Daniela Brozzoni
d8ee94c30d
Merge bitcoindevkit/rust-electrum-client#105: Bump to v0.15.0
bd16116f9a Bump to v0.15.0 (Alekos Filini)

Pull request description:

ACKs for top commit:
  danielabrozzoni:
    ACK bd16116f9a

Tree-SHA512: c184fe6ed2f2b3ad5282b4ddb2d3186dcadee4685a417ebff0f9ab9e1c9a5596d91dc561c831ae7131fc63a0401f4dc29ece30024c09e11b99683f613e8ed856
2023-05-02 20:30:53 +02:00
Alekos Filini
bd16116f9a
Bump to v0.15.0
Some checks failed
CI / Test (push) Has been cancelled
2023-05-02 20:27:00 +02:00
Daniela Brozzoni
b989ddb04b
Merge bitcoindevkit/rust-electrum-client#103: upgrade bitcoin dep to 0.30.0
c925a9179e upgrade bitcoin dep to 0.30.0 (Riccardo Casatta)

Pull request description:

ACKs for top commit:
  danielabrozzoni:
    ACK c925a9179e

Tree-SHA512: 2b9b7756639af4d931213046866dc36ebee2a5adcbd01c906e27990696b07f1bcf87c2ee009a959f96b17407637801b856e8d4fb9a933fab148f22a619a0d586
2023-05-02 20:18:14 +02:00
Riccardo Casatta
c925a9179e
upgrade bitcoin dep to 0.30.0 2023-04-06 11:50:13 +02:00
Alekos Filini
abdbd020e1
Bump to v0.14.0
Some checks failed
CI / Test (push) Has been cancelled
2023-04-06 10:11:18 +02:00
Alekos Filini
a04a84299d
Merge bitcoindevkit/rust-electrum-client#101: Make ScriptStatus Serializable
80b2adeb4b Make ScriptStatus Serializable (Riccardo Casatta)

Pull request description:

  it's common to cache this value client side, so making it Serializable simplify things downstream

ACKs for top commit:
  afilini:
    ACK 80b2adeb4b

Tree-SHA512: 9467b4f2ee24ee2a684931df31607135f44c624f65d7d3ee64b3991a06de73a07475e4d1a4b8062093500e6a7cb47c1b2ebf094596fca33bf4cc54b26851b910
2023-04-06 10:09:38 +02:00
Alekos Filini
d26dfd7f54
Merge bitcoindevkit/rust-electrum-client#100: Implement batch call for script subscribe
40cecd5010 Use Borrow for all items in IntoIterator (Riccardo Casatta)
bf2f8ef2e5 script_subscribe IntoIterator accepts Borrow<&Script> (Riccardo Casatta)
156e6fc839 Implement batch call for script subscribe (Riccardo Casatta)

Pull request description:

  script_subscribe call hasn't the batch counterpart which may be convenient for multiple subsequent subscribe.

ACKs for top commit:
  afilini:
    ACK 40cecd5010

Tree-SHA512: 26a3ebb9d1267a224c85b765cf793523771bf7e74990dd804a35693bfebc549f7524214725092eac2276f22cd24eafc72270986549ac02f52641db83fcb71324
2023-04-06 10:07:20 +02:00
Riccardo Casatta
80b2adeb4b
Make ScriptStatus Serializable
it's common to cache this value client side, so making it Serializable
simplify things downstream
2023-04-05 11:34:23 +02:00
Riccardo Casatta
40cecd5010
Use Borrow for all items in IntoIterator 2023-03-22 17:05:02 +01:00
Riccardo Casatta
bf2f8ef2e5
script_subscribe IntoIterator accepts Borrow<&Script>
So that is callable with `&Vec<Script>` and `Vec<&Script>`
2023-03-22 15:53:02 +01:00
Riccardo Casatta
156e6fc839
Implement batch call for script subscribe 2023-03-21 17:03:19 +01:00
Alekos Filini
84d6860144
Bump to v0.13.0
Some checks failed
CI / Test (push) Has been cancelled
2023-03-11 11:41:19 +01:00
Daniela Brozzoni
413c964e5c
Merge bitcoindevkit/rust-electrum-client#97: Don't .unwrap() on invalid proxies
3568b60612 Don't `.unwrap()` on invalid proxies (Riccardo Mazzarini)

Pull request description:

ACKs for top commit:
  danielabrozzoni:
    utACK 3568b60612

Tree-SHA512: c2ebb21d6e78616f34a8d3becb68551a857c7b391c46ff4d1e379e482a5a5d90a3c12807896fc4da99a7442b1911bf00f02db41ba0cb890bf0acd284b1e6e3e2
2023-02-27 09:18:05 +01:00
Riccardo Mazzarini
3568b60612
Don't .unwrap() on invalid proxies 2023-02-23 03:15:26 +01:00
志宇
adb0c7444e
Add Batch::raw and improve docs
Being able to add raw requests to `Batch` makes sense from an API
standpoint because we already allow raw non-batched requests. This is
also useful when the electrum server API gets an updated version and our
client is unable to keep up.

Additional to this, I have improved the documentation and made `Call`
private (since `Call` is never used externally).
2023-02-14 14:16:07 +13:00
Alekos Filini
129081999c
Merge bitcoindevkit/rust-electrum-client#88: timeout() and socks() don't return Result
561db261d3 `timeout()` and `socks()` don't return Result (Riccardo Casatta)

Pull request description:

  The `Result` was necessary in the past because they couldn't both be set,
  this is not true anymore after 7493630ed8

ACKs for top commit:
  afilini:
    ACK 561db261d3

Tree-SHA512: 7654cfbbc7beac6acec786060e373bbc046e1dae0faf2b2ca1df33121e9e92a8f4950c7f8775e44226a62483aeae41c76162cf037ce38137c44d2fedd38560c8
2022-09-13 18:36:59 +02:00
Riccardo Casatta
561db261d3
timeout() and socks() don't return Result
The `Result` was necessary in the past because they couldn't both be set,
this is not true anymore after 7493630ed8
2022-09-13 09:27:03 +02:00
Alekos Filini
add1b7853e
Merge bitcoindevkit/rust-electrum-client#87: Bump version to 0.12.0
97e60bad4b Bump version to 0.12.0 (Alekos Filini)

Pull request description:

ACKs for top commit:
  danielabrozzoni:
    ACK 97e60bad4b

Tree-SHA512: e71c2093229552b5317f9309008924a6b2e8f6a48564535d2cd77e6830f342359574518836cfa579460b642c6aa8a2f36b81637b2ee8a48a68f695425333f1a2
2022-09-05 11:53:22 +02:00
Alekos Filini
97e60bad4b
Bump version to 0.12.0
Some checks failed
CI / Test (push) Has been cancelled
2022-09-05 11:31:47 +02:00
Alekos Filini
3adfbd783f
Merge bitcoindevkit/rust-electrum-client#81: Bump bitcoin dependency to ^0.29, minor related fixes
d132572c14 Bump bitcoin dependency to ^0.29, minor related fixes (Steve Myers)

Pull request description:

  This version bump for `bitcoin` dependency is required to fix https://github.com/bitcoindevkit/bdk/issues/646.

ACKs for top commit:
  afilini:
    ACK d132572c14

Tree-SHA512: ad89362fa32e386f02ef5721489d2c24b342af8997ceac09933dfc979f70a12b876b4856dd8982942968d8ad82ae34c49386744f51db22c70289a259a195c943
2022-09-05 11:31:08 +02:00
Steve Myers
d132572c14
Bump bitcoin dependency to ^0.29, minor related fixes 2022-08-31 07:54:48 -05:00
Alekos Filini
ad62e7a80f
Merge bitcoindevkit/rust-electrum-client#86: Bump version to 0.11.0
f3caf4a279 Bump version to 0.11.0 (Alekos Filini)

Pull request description:

ACKs for top commit:
  danielabrozzoni:
    ACK f3caf4a279

Tree-SHA512: 1b8f9853452f1462daa3788bfa3f110481d8674b870ad67eebcf8857f409312b3d69dc188ee1bc5093708f965c59c5e89fd75605bfa9059239de56fb1dbdeaee
2022-08-31 11:50:28 +02:00
Alekos Filini
f3caf4a279
Bump version to 0.11.0
Some checks failed
CI / Test (push) Has been cancelled
2022-08-30 18:26:51 +02:00
Alekos Filini
cdfc21bd6a
Merge bitcoindevkit/rust-electrum-client#84: Use idiomatic rust for the raw_call method
fc8ba9de90 Use idiomatic rust for the `raw_call` method (Alekos Filini)

Pull request description:

ACKs for top commit:
  danielabrozzoni:
    tACK fc8ba9de90

Tree-SHA512: 1b2e92eff78df5b9fe3e6d9121af7c090e27e25aecd39187e6983252b288e0109350603d175ded2cf25365d6a85601ad4da5d8495c9943fe27280c36fe5f0c49
2022-08-30 18:25:29 +02:00
Alekos Filini
fc8ba9de90
Use idiomatic rust for the raw_call method 2022-08-30 12:58:44 +02:00
Alekos Filini
9994f8a2e1
Merge bitcoindevkit/rust-electrum-client#83: Revert "Ignore empty lines"
cbda7a22db Revert "Ignore empty lines" (Alekos Filini)

Pull request description:

  This reverts commit 0e3e6325e6.

  This has been causing issues with tests timing out

ACKs for top commit:
  notmandatory:
    utACK cbda7a22db

Tree-SHA512: ff683e16e623e71f017a361dc85e12fd7ea23d8ebd0f0616dd543aa3b12645540ae82f7eaab233dd846f5e802540064ce6a56e056d225a79e5347d730a736926
2022-08-30 12:57:17 +02:00
Alekos Filini
cbda7a22db
Revert "Ignore empty lines"
This reverts commit 0e3e6325e6.

This has been causing issues with tests timing out
2022-08-24 14:35:38 +02:00
Alekos Filini
7e40dfd441
Merge bitcoindevkit/rust-electrum-client#80: Client: Drop script subscriptions to free lock
aa0b957818 Client: Drop script subscriptions to free lock (Sebastian Kung)

Pull request description:

  This fixes a supposed deadlock when the reader thread receives a script subscription notification from the server before it receives a subscribing response.

  I am not 100% sure if this actually fixes a deadlock, but I ran into this in a regtest setup where a bunch of subscriptions were made, while some script notifications could be processed in parallel. My test failed sporadically with the electrum client locking up completely and adding this drop made the tests consistently pass again.

ACKs for top commit:
  afilini:
    ACK aa0b957818, makes a lot of sense!

Tree-SHA512: b1bbea71fb56a189081b9aa602f768ef50d349a6ad6f3a3f64264259954a541fee85cad84461afeafe1ba17e11950c18fdcadb7c47a5fa1b3ce2fa2ac1cdcdc0
2022-08-18 11:42:34 +02:00
Alekos Filini
c34fde5698
Revert "Handle EOF in the reader thread"
This reverts commit acc05c3666 which
caused many failures in BDK. It was released as part of `0.10.3`, which
has now been yanked from crates.io
2022-08-18 11:39:56 +02:00
Sebastian Kung
aa0b957818
Client: Drop script subscriptions to free lock
This fixes a supposed deadlock when the reader thread receives a script
subscription notification from the server before it receives a
subscribing response
2022-08-16 22:21:47 +02:00
Alekos Filini
737cedb74e
Make the socks module public 2022-08-08 11:52:26 +02:00
Alekos Filini
a148dfb9d1
Merge bitcoindevkit/rust-electrum-client#76: Implement timeout for socks connection
7493630ed8 Implement timeout for socks connection (Riccardo Casatta)

Pull request description:

  To achieve this we have to include a modified socks library, with supports for
  timeout

  Remove the error `BothSocksAndTimeout` which isn't needed anymore

ACKs for top commit:
  afilini:
    ACK 7493630ed8

Tree-SHA512: ec1b1eba739bd954ba7811eef603cf60f3ba29d7efcf98a9eadc8dcb9b67a49fce364760718faf7b860e78b7f9eebdc4da65783e7af47792573d32cd6c76ff10
2022-08-08 11:51:00 +02:00
Alekos Filini
195d23accd
Bump version to 0.10.3 2022-08-08 11:38:27 +02:00
Alekos Filini
0d77c1860f
Merge bitcoindevkit/rust-electrum-client#78: Handle EOF in the reader thread
acc05c3666 Handle EOF in the reader thread (Alekos Filini)

Pull request description:

  Instead of ignoring empty lines we should return an error, because it means we are at EOF.

  Fixes #70 and hopefully also #67

ACKs for top commit:
  RCasatta:
    ACK acc05c3666

Tree-SHA512: e92b27610c91a419cc0f71ecd77205fdaeed592b50e38c42fd8cde805773f4e9e8eeda75ba14f73002d6ae20a70986f981741e6a69d1a89cf61971c174614951
2022-08-08 11:32:41 +02:00
Alekos Filini
acc05c3666
Handle EOF in the reader thread 2022-08-03 11:24:41 +02:00
Riccardo Casatta
7493630ed8
Implement timeout for socks connection
To achieve this we have to include a modified socks library, with supports for
timeout

Remove the error `BothSocksAndTimeout` which isn't needed anymore
2022-08-02 14:52:47 +02:00
Alekos Filini
d9668c86f4
Bump version to 0.10.2
Some checks failed
CI / Test (push) Has been cancelled
2022-08-01 12:32:49 +02:00
Alekos Filini
e9bee5a46d
Merge bitcoindevkit/rust-electrum-client#73: Update build badge to use GitHub badge
d8e339c4c1 Update build badge to use GitHub badge (dandroid)

Pull request description:

  The current build badge using Travis is not working anymore.
  This PR uses the badge from GitHub instead.

ACKs for top commit:
  afilini:
    ACK d8e339c4c1

Tree-SHA512: c08b5dc4b0d6bc0915843d95bfcf84c62317bb8a210f219feca31ce622b2f2b1241ae7987f5c395a980c6b2b3defc48227d43e8491e1adeafadc6e71ecde0db1
2022-08-01 12:31:01 +02:00
Alekos Filini
e1c20b8a0a
Merge bitcoindevkit/rust-electrum-client#74: Add raw_call to ElectrumApi
3d96644a0f Add raw_call to ElectrumApi (Zoe Faltibà)

Pull request description:

  In order to give more flexibility to the users of this library I added a `raw_call` method to the `ElectrumApi` trait that will allow to call any arbitrary electrum API call.

ACKs for top commit:
  afilini:
    ACK 3d96644a0f

Tree-SHA512: b88ece7bd9ff8121272c3b3528430e9ccb8778661b15cce276c6f67a8797a4dbf4c52a8521191942c21e8b67ebde8187fd75ac49189440b0fbb71274974b4b41
2022-08-01 12:30:05 +02:00
Zoe Faltibà
3d96644a0f
Add raw_call to ElectrumApi 2022-07-27 15:06:47 +02:00
dandroid
d8e339c4c1 Update build badge to use GitHub badge 2022-07-24 15:46:34 +02:00
Alekos Filini
ca48628539
Merge bitcoindevkit/rust-electrum-client#72: doc: s/Satoshi/Bitcoin
29d77de2f5 doc: s/Satoshi/Bitcoin (zkao)

Pull request description:

  Unit was wrong in the docs

ACKs for top commit:
  afilini:
    ACK 29d77de2f5

Tree-SHA512: a172647cb3c7dc55444f89f9b05832c2b073fc6062c439719bb5c4aecc739be20592ee93111ecee491b0b37e0a0c49f7c3c331a1099bbd6070711d9f5ae0c093
2022-07-15 12:48:39 +02:00
zkao
29d77de2f5
doc: s/Satoshi/Bitcoin
Unit was wrong in the docs
2022-07-13 20:07:08 +02:00
Alekos Filini
e9a7fba7a9
Merge bitcoindevkit/rust-electrum-client#71: Add Config::builder method
aea9da234b Add `Config::builder` method (志宇)

Pull request description:

  The intention is to reduce some confusion on how to manipulate `Config`.
  The alternative is to make `Config` fields public.

ACKs for top commit:
  afilini:
    ACK aea9da234b

Tree-SHA512: 4077e821260c8f6e4bc32a66548e11b1091d384988b392c981102dc3c198a5040831c5feb13b1f7b1a88fd3890d7c44c7516b94f4c2e9cbbc551581c8c72d803
2022-06-28 10:47:58 +02:00
志宇
aea9da234b
Add Config::builder method
The intention is to reduce some confusion on how to manipulate Config.
The alternative is to make Config fields public.
2022-06-24 20:23:54 +08:00
Alekos Filini
866c2424e1
Bump version to 0.10.1
Some checks failed
CI / Test (push) Has been cancelled
2022-06-08 11:13:15 +02:00
Alekos Filini
2ecd93b9b3
Merge bitcoindevkit/rust-electrum-client#68: Ignore empty lines
0e3e6325e6 Ignore empty lines (Alekos Filini)

Pull request description:

  Fixes #67

Top commit has no ACKs.

Tree-SHA512: da61610be72dc80fa0e25eb4bdd7ea9e5d9816f1b5eb51a198e09ac95a27284206228a174e67038894bc83892622548c4eb7c13b33b1f261802804bffd35ece6
2022-06-08 11:12:42 +02:00
Alekos Filini
0e3e6325e6
Ignore empty lines
Fixes #67
2022-06-01 09:55:42 +02:00
Alekos Filini
19f00a576c
Release 0.10.0
Some checks failed
CI / Test (push) Has been cancelled
2022-04-26 14:17:41 +02:00
Alekos Filini
5c8555c2f6
Release 0.10.0-rc.2
Some checks failed
CI / Test (push) Has been cancelled
2022-04-19 11:19:57 +02:00
Alekos Filini
4732dcc424
Release 0.9.0
Some checks failed
CI / Test (push) Has been cancelled
2022-03-24 11:51:18 +01:00
Alekos Filini
8b32726335
Merge bitcoindevkit/rust-electrum-client#62: Upgrade rustls and webpki version
68d2f23306 Upgrade rustls and webpki version (Thomas Eizinger)

Pull request description:

ACKs for top commit:
  afilini:
    ACK 68d2f23306

Tree-SHA512: 3578954d6629470742fec326525fb34377380c429ff51d5f15e128045161fc6fba4852a22089c2fa9e6bfa7c35ffbaf06eadb0f457fbcb414d15962b9283cd64
2022-03-24 11:50:28 +01:00
Thomas Eizinger
68d2f23306
Upgrade rustls and webpki version 2022-03-24 16:18:45 +11:00
Alekos Filini
98e1bf205c
Release 0.8.0
Some checks failed
CI / Test (push) Has been cancelled
2021-08-03 10:44:03 +03:00
Sandipan Dey
f012323274
bump rust-bitcoin to v0.27 2021-07-27 11:19:35 +05:30
Alekos Filini
47053c0302
Remove unused import 2021-04-13 09:13:59 +02:00
Dr Maxim Orlovsky
cfc7bfe037
Improving RawClient::batch_call after review suggestion 2021-04-12 15:39:35 +02:00
Dr Maxim Orlovsky
d64aaffb3f
Streamlining batch_call and removing unnecessary unwraps() 2021-04-06 14:31:36 +02:00
Alekos Filini
d5766abb8e
Merge commit 'refs/pull/52/head' of github.com:bitcoindevkit/rust-electrum-client 2021-03-19 20:32:38 +01:00
Riccardo Casatta
f30c37b69f
expose Config 2021-03-19 14:58:16 +01:00
Dr Maxim Orlovsky
7776ec0732
Improving error handing according to #33 comments 2021-02-22 15:29:05 +01:00
Dr Maxim Orlovsky
e7eab4f77a
Getting rid of all expect()'s 2021-02-22 15:17:42 +01:00
Dr Maxim Orlovsky
88e92af429
Getting rid of lock().unwrap() 2021-02-22 15:14:50 +01:00
Alekos Filini
e3aa6a5a73
Release 0.7.0
Some checks failed
CI / Test (push) Has been cancelled
2021-02-22 15:11:17 +01:00
Alekos Filini
f30be3bfde
Add a test for connection timeouts 2021-02-22 14:51:25 +01:00
Alekos Filini
fa9359cf73
Implement timeouts when there are multiple socket addrs 2021-02-22 14:47:23 +01:00
Dr Maxim Orlovsky
25dc3fa5e5
Fix CI testing for all features 2021-02-22 12:56:48 +01:00
Alekos Filini
d46ba79972
Merge commit 'refs/pull/48/head' of github.com:bitcoindevkit/rust-electrum-client 2021-02-22 11:43:17 +01:00
Alekos Filini
078e513f62
Make unconfirmed an i64
Some servers like electrs return the unconfirmed balance for an address as a
negative value.

Closes #45
2021-02-22 09:49:42 +01:00
Thomas Eizinger
c8bace9407
Apply new retry logic also to re-creating client 2021-02-18 15:56:51 +11:00
Thomas Eizinger
cb9b51f356
Don't cast usize to u8 but deal with the overflow properly 2021-02-17 12:15:56 +11:00
Thomas Eizinger
2652016f99
Improve logging in case of call failure
Previously, we were logging the retry attempt before we even evaluated
whether or not we are going to retry again. Instead, we now compute
the number of failed attempts and either log and bail from the function
or inform the user that we are going to retry the call.

We also include the name of the failed call and the reason why it failed
in case we are retrying it. Once we have exhausted the the number of
retries, we return all errors anyway so there is no reason to log them.
It is up to the caller to perform error handling.
2021-02-16 11:33:13 +11:00
Thomas Eizinger
23c2004f38
Actually retry the call based on the value of config.retry
Fixes #47.
2021-02-16 11:18:18 +11:00
Alekos Filini
7db9a1331b
Merge commit 'refs/pull/37/head' of github.com:bitcoindevkit/rust-electrum-client 2021-01-25 14:35:59 -05:00
Dr. Maxim Orlovsky
d927387df3
Undo exhaustive enum type 2021-01-25 16:38:48 +01:00
Alekos Filini
1d59d9605e
Release 0.6.0
Some checks failed
CI / Test (push) Has been cancelled
2021-01-21 22:06:45 -05:00
Alekos Filini
af10aeac99
Merge commit 'refs/pull/32/head' of github.com:bitcoindevkit/rust-electrum-client 2021-01-21 09:01:27 -05:00
Dr Maxim Orlovsky
efdc7d2243
Improving error type 2021-01-21 01:49:46 +01:00
Dr Maxim Orlovsky
7d7a3088e6
Fixing calls_made fn signature 2021-01-21 00:54:25 +01:00
Dr Maxim Orlovsky
3ac7f6ec6b
Updating to rust-bitcoin 0.26 2021-01-21 00:49:16 +01:00
Alekos Filini
aacb1291a5
Release 0.5.0-beta.1
Some checks failed
CI / Test (push) Has been cancelled
2021-01-11 13:54:23 +01:00
Riccardo Casatta
8b81ad1d90
remove travis 2021-01-11 12:25:52 +01:00
Riccardo Casatta
226b12aafa
fix CI by using TargetAddr only with proxy feature 2021-01-11 12:17:15 +01:00
Riccardo Casatta
89fdf749f1
Sets the timeout as optional 2021-01-11 11:50:21 +01:00
Alekos Filini
3e266037a1
Release 0.4.0-beta.1
Some checks failed
CI / Test (push) Has been cancelled
2020-11-30 12:29:06 +01:00
Riccardo Casatta
c4735374f9
fix test_script_list_unspent by not depending on order of returned results 2020-11-27 17:07:40 +01:00
Riccardo Casatta
1a0252450c
allow missing docs on variant instead of hiding, avoid redundant clone 2020-11-27 15:38:55 +01:00
Alekos Filini
b08c59815d
Add support for TLS over socks5 proxy
Some checks failed
CI / Test (push) Has been cancelled
2020-11-27 15:13:35 +01:00
Riccardo Casatta
2477ab4440
wait with an exponential backoff capped at 1 minute 2020-11-25 15:33:12 +01:00
Riccardo Casatta
e4c72ed4bc
Propagate error also in the reader, not just in waiting threads 2020-11-25 15:33:10 +01:00
Riccardo Casatta
add53ebfa1
internal loop for client re-creation, keeping the lock 2020-11-24 15:50:09 +01:00
Riccardo Casatta
ef3ddfdabc
Pass Arc<io::Error> to waiting threads 2020-11-24 13:46:20 +01:00
Riccardo Casatta
f200b7fc61
Implements retry, timeout, socks credential and others fixes
Retry works by checking if an non-protocol error happened during the call, if so an attempt to take a write lock on the client is made and a new client attempted
if both operationare succesfull the old_client is substituted with the new one.

Timeout is a single parameter for connect,read and write TCP timeouts, special handling is required for the following situation: cannot set a timeout with a proxy and cannot set timeout with multipl SocketAddrs

Since configurations parameter grow, a Config struct with a builder has been introduced

Some new errors variant has been introduced

Errors in reading input in the reader thread now warns other eventual threads with ChannelMessage::Error

Add github actions equivalent to travis cause travis is slow
2020-11-24 12:03:51 +01:00
Nadav Ivgi
c1f4e9d76d
Allow to initialize SSL clients with an existing TcpStream 2020-11-07 03:15:07 +02:00
Alekos Filini
11c2b1b6ce
Release 0.3.0-beta.1 2020-09-30 11:28:58 +02:00
Riccardo Casatta
6f8cefd1f1
bump bitcoin version to 0.25 2020-09-30 10:23:51 +02:00
Alekos Filini
5a5fdca348
Fix conditional imports of client::* 2020-08-03 11:18:23 +02:00
Alekos Filini
3502a5c7ab
Release 0.2.0-beta.2 2020-08-03 10:18:13 +02:00
Riccardo Casatta
d5e4bfe3e7
allow Client with openssl+proxy 2020-07-31 17:58:11 +02:00
23 changed files with 3547 additions and 364 deletions

71
.github/workflows/cont_integration.yml vendored Normal file
View File

@ -0,0 +1,71 @@
on: [push, pull_request]
name: CI
jobs:
test:
name: Test
runs-on: ubuntu-latest
env:
TEST_ELECTRUM_SERVER: electrum.blockstream.info:50001
strategy:
matrix:
rust:
- stable # STABLE
- 1.75.0 # MSRV
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
- 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
run: sudo ip6tables -I INPUT 1 -p tcp -d ::1 --dport 60000 -j DROP
- name: Timeout test
run: cargo test -- --ignored test_local_timeout
- run: cargo check --verbose --features=use-openssl
- run: cargo check --verbose --no-default-features --features=proxy
- run: cargo check --verbose --no-default-features --features=minimal
- 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

View File

@ -1,26 +0,0 @@
language: rust
rust:
- stable
# - 1.31.0 TODO: support the first 2018-edition toolchain
env:
- TEST_ELECTRUM_SERVER=electrum.blockstream.info:50001
- TEST_ELECTRUM_SERVER=bitcoin.aranguren.org:50001
before_script:
- rustup component add rustfmt
# Run tests and check a few combinations of features
script:
- cargo fmt -- --check --verbose
- cargo test --verbose --all
- cargo check --verbose --features=use-openssl
- cargo check --verbose --no-default-features --features=proxy
- cargo check --verbose --no-default-features --features=minimal
- cargo check --verbose --no-default-features --features=minimal,debug-calls
- cargo check --verbose --no-default-features --features=proxy,use-openssl
- cargo check --verbose --no-default-features --features=proxy,use-rustls
before_cache:
- rm -rf "$TRAVIS_HOME/.cargo/registry/src"
cache: cargo
notifications:
email: false

66
CHANGELOG.md Normal file
View 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

View File

@ -1,14 +1,16 @@
[package] [package]
name = "electrum-client" name = "electrum-client"
version = "0.2.0-beta.1" version = "0.24.1"
authors = ["Alekos Filini <alekos.filini@gmail.com>"] authors = ["Alekos Filini <alekos.filini@gmail.com>"]
license = "MIT" license = "MIT"
homepage = "https://github.com/MagicalBitcoin/rust-electrum-client" homepage = "https://github.com/bitcoindevkit/rust-electrum-client"
repository = "https://github.com/MagicalBitcoin/rust-electrum-client" repository = "https://github.com/bitcoindevkit/rust-electrum-client"
documentation = "https://docs.rs/electrum-client/" documentation = "https://docs.rs/electrum-client/"
description = "Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers." description = "Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers."
keywords = ["bitcoin", "electrum"] keywords = ["bitcoin", "electrum"]
readme = "README.md" readme = "README.md"
rust-version = "1.75.0"
edition = "2021"
# loosely based on https://github.com/evgeniy-scherbina/rust-electrumx-client # loosely based on https://github.com/evgeniy-scherbina/rust-electrumx-client
@ -18,21 +20,28 @@ path = "src/lib.rs"
[dependencies] [dependencies]
log = "^0.4" log = "^0.4"
bitcoin = { version = "0.23", features = ["use-serde"] } bitcoin = { version = "0.32", features = ["serde"] }
serde = { version = "^1.0", features = ["derive"] } serde = { version = "^1.0", features = ["derive"] }
serde_json = { version = "^1.0" } serde_json = { version = "^1.0" }
# Optional dependencies # Optional dependencies
socks = { version = "^0.3", optional = true } openssl = { version = "0.10", optional = true }
openssl = { version = "^0.10", optional = true } rustls = { version = "0.23.21", optional = true, default-features = false }
rustls = { version = "0.16.0", optional = true, features = ["dangerous_configuration"] } webpki-roots = { version = "0.25", optional = true }
webpki = { version = "0.21.0", optional = true }
webpki-roots = { version = "^0.19", optional = true } byteorder = { version = "1.0", optional = true }
[target.'cfg(unix)'.dependencies]
libc = { version = "0.2", optional = true }
[target.'cfg(windows)'.dependencies]
winapi = { version="0.3.9", features=["winsock2"], optional = true }
[features] [features]
default = ["socks", "webpki", "webpki-roots", "rustls"] default = ["proxy", "use-rustls"]
minimal = [] minimal = []
debug-calls = [] debug-calls = []
proxy = ["socks"] 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"] use-openssl = ["openssl"]

View File

@ -1,9 +1,15 @@
# rust-electrum-client [![Build Status]][travis] [![Latest Version]][crates.io] # rust-electrum-client
[![Build Status]][GitHub Workflow] [![Latest Version]][crates.io] [![MSRV Badge]][Rust Blog]
[Build Status]: https://api.travis-ci.org/MagicalBitcoin/rust-electrum-client.svg?branch=master [Build Status]: https://github.com/bitcoindevkit/rust-electrum-client/actions/workflows/cont_integration.yml/badge.svg
[travis]: https://travis-ci.org/MagicalBitcoin/rust-electrum-client [GitHub Workflow]: https://github.com/bitcoindevkit/rust-electrum-client/actions?query=workflow%3ACI
[Latest Version]: https://img.shields.io/crates/v/electrum-client.svg [Latest Version]: https://img.shields.io/crates/v/electrum-client.svg
[crates.io]: https://crates.io/crates/electrum-client [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. 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
View File

@ -0,0 +1 @@
msrv="1.75.0"

View File

@ -3,7 +3,7 @@ extern crate electrum_client;
use electrum_client::{Client, ElectrumApi}; use electrum_client::{Client, ElectrumApi};
fn main() { fn main() {
let client = Client::new("tcp://electrum.blockstream.info:50001", None).unwrap(); let client = Client::new("tcp://electrum.blockstream.info:50001").unwrap();
let res = client.server_features(); let res = client.server_features();
println!("{:#?}", res); println!("{:#?}", res);
} }

View File

@ -3,7 +3,7 @@ extern crate electrum_client;
use electrum_client::{Client, ElectrumApi}; use electrum_client::{Client, ElectrumApi};
fn main() { fn main() {
let client = Client::new("ssl://electrum.blockstream.info:50002", None).unwrap(); let client = Client::new("ssl://electrum.blockstream.info:50002").unwrap();
let res = client.server_features(); let res = client.server_features();
println!("{:#?}", res); println!("{:#?}", res);
} }

View File

@ -1,19 +1,21 @@
extern crate electrum_client; extern crate electrum_client;
use electrum_client::{Client, ElectrumApi}; use electrum_client::{Client, ConfigBuilder, ElectrumApi, Socks5Config};
fn main() { fn main() {
// NOTE: This assumes Tor is running localy, with an unauthenticated Socks5 listening at // NOTE: This assumes Tor is running localy, with an unauthenticated Socks5 listening at
// localhost:9050 // localhost:9050
let proxy = Socks5Config::new("127.0.0.1:9050");
let config = ConfigBuilder::new().socks5(Some(proxy)).build();
let client = Client::new("tcp://explorernuoc63nb.onion:110", Some("127.0.0.1:9050")).unwrap(); let client = Client::from_config("tcp://explorernuoc63nb.onion:110", config.clone()).unwrap();
let res = client.server_features(); let res = client.server_features();
println!("{:#?}", res); println!("{:#?}", res);
// works both with onion v2/v3 (if your Tor supports them) // works both with onion v2/v3 (if your Tor supports them)
let client = Client::new( let client = Client::from_config(
"tcp://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion:110", "tcp://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion:110",
Some("127.0.0.1:9050"), config,
) )
.unwrap(); .unwrap();
let res = client.server_features(); let res = client.server_features();

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

@ -1,17 +1,191 @@
//! Electrum APIs //! Electrum APIs
use std::borrow::Borrow;
use std::convert::TryInto; use std::convert::TryInto;
use std::ops::Deref;
use bitcoin::consensus::encode::{deserialize, serialize}; use bitcoin::consensus::encode::{deserialize, serialize};
use bitcoin::{BlockHeader, Script, Transaction, Txid}; use bitcoin::{block, Script, Transaction, Txid};
use batch::Batch; use crate::batch::Batch;
use types::*; 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 /// API calls exposed by an Electrum client
pub trait ElectrumApi { pub trait ElectrumApi {
/// Gets the block header for height `height`. /// Gets the block header for height `height`.
fn block_header(&self, height: usize) -> Result<BlockHeader, Error> { fn block_header(&self, height: usize) -> Result<block::Header, Error> {
Ok(deserialize(&self.block_header_raw(height)?)?) Ok(deserialize(&self.block_header_raw(height)?)?)
} }
@ -38,7 +212,8 @@ pub trait ElectrumApi {
/// Takes a list of `txids` and returns a list of transactions. /// Takes a list of `txids` and returns a list of transactions.
fn batch_transaction_get<'t, I>(&self, txids: I) -> Result<Vec<Transaction>, Error> fn batch_transaction_get<'t, I>(&self, txids: I) -> Result<Vec<Transaction>, Error>
where where
I: IntoIterator<Item = &'t Txid>, I: IntoIterator + Clone,
I::Item: Borrow<&'t Txid>,
{ {
self.batch_transaction_get_raw(txids)? self.batch_transaction_get_raw(txids)?
.iter() .iter()
@ -49,9 +224,10 @@ pub trait ElectrumApi {
/// Batch version of [`block_header`](#method.block_header). /// Batch version of [`block_header`](#method.block_header).
/// ///
/// Takes a list of `heights` of blocks and returns a list of headers. /// Takes a list of `heights` of blocks and returns a list of headers.
fn batch_block_header<'s, I>(&self, heights: I) -> Result<Vec<BlockHeader>, Error> fn batch_block_header<I>(&self, heights: I) -> Result<Vec<block::Header>, Error>
where where
I: IntoIterator<Item = u32>, I: IntoIterator + Clone,
I::Item: Borrow<u32>,
{ {
self.batch_block_header_raw(heights)? self.batch_block_header_raw(heights)?
.iter() .iter()
@ -65,10 +241,17 @@ pub trait ElectrumApi {
self.transaction_broadcast_raw(&buffer) self.transaction_broadcast_raw(&buffer)
} }
/// Executes the requested API call returning the raw answer.
fn raw_call(
&self,
method_name: &str,
params: impl IntoIterator<Item = Param>,
) -> Result<serde_json::Value, Error>;
/// Execute a queue of calls stored in a [`Batch`](../batch/struct.Batch.html) struct. Returns /// Execute a queue of calls stored in a [`Batch`](../batch/struct.Batch.html) struct. Returns
/// `Ok()` **only if** all of the calls are successful. The order of the JSON `Value`s returned /// `Ok()` **only if** all of the calls are successful. The order of the JSON `Value`s returned
/// reflects the order in which the calls were made on the `Batch` struct. /// reflects the order in which the calls were made on the `Batch` struct.
fn batch_call(&self, batch: Batch) -> Result<Vec<serde_json::Value>, Error>; fn batch_call(&self, batch: &Batch) -> Result<Vec<serde_json::Value>, Error>;
/// Subscribes to notifications for new block headers, by sending a `blockchain.headers.subscribe` call and /// Subscribes to notifications for new block headers, by sending a `blockchain.headers.subscribe` call and
/// returns the current tip as raw bytes instead of deserializing them. /// returns the current tip as raw bytes instead of deserializing them.
@ -84,7 +267,7 @@ pub trait ElectrumApi {
/// Tries to fetch `count` block headers starting from `start_height`. /// Tries to fetch `count` block headers starting from `start_height`.
fn block_headers(&self, start_height: usize, count: usize) -> Result<GetHeadersRes, Error>; fn block_headers(&self, start_height: usize, count: usize) -> Result<GetHeadersRes, Error>;
/// Estimates the fee required in **Satoshis per kilobyte** to confirm a transaction in `number` blocks. /// Estimates the fee required in **Bitcoin per kilobyte** to confirm a transaction in `number` blocks.
fn estimate_fee(&self, number: usize) -> Result<f64, Error>; fn estimate_fee(&self, number: usize) -> Result<f64, Error>;
/// Returns the minimum accepted fee by the server's node in **Bitcoin, not Satoshi**. /// Returns the minimum accepted fee by the server's node in **Bitcoin, not Satoshi**.
@ -99,6 +282,16 @@ pub trait ElectrumApi {
/// already subscribed to the script. /// already subscribed to the script.
fn script_subscribe(&self, script: &Script) -> Result<Option<ScriptStatus>, Error>; fn script_subscribe(&self, script: &Script) -> Result<Option<ScriptStatus>, Error>;
/// Batch version of [`script_subscribe`](#method.script_subscribe).
///
/// Takes a list of scripts and returns a list of script status responses.
///
/// Note you should pass a reference to a collection because otherwise an expensive clone is made
fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result<Vec<Option<ScriptStatus>>, Error>
where
I: IntoIterator + Clone,
I::Item: Borrow<&'s Script>;
/// Subscribes to notifications for activity on a specific *scriptPubKey*. /// Subscribes to notifications for activity on a specific *scriptPubKey*.
/// ///
/// Returns a `bool` with the server response when successful. /// Returns a `bool` with the server response when successful.
@ -118,7 +311,8 @@ pub trait ElectrumApi {
/// Takes a list of scripts and returns a list of balance responses. /// Takes a list of scripts and returns a list of balance responses.
fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result<Vec<GetBalanceRes>, Error> fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result<Vec<GetBalanceRes>, Error>
where where
I: IntoIterator<Item = &'s Script>; I: IntoIterator + Clone,
I::Item: Borrow<&'s Script>;
/// Returns the history for a *scriptPubKey* /// Returns the history for a *scriptPubKey*
fn script_get_history(&self, script: &Script) -> Result<Vec<GetHistoryRes>, Error>; fn script_get_history(&self, script: &Script) -> Result<Vec<GetHistoryRes>, Error>;
@ -128,7 +322,8 @@ pub trait ElectrumApi {
/// Takes a list of scripts and returns a list of history responses. /// Takes a list of scripts and returns a list of history responses.
fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result<Vec<Vec<GetHistoryRes>>, Error> fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result<Vec<Vec<GetHistoryRes>>, Error>
where where
I: IntoIterator<Item = &'s Script>; I: IntoIterator + Clone,
I::Item: Borrow<&'s Script>;
/// Returns the list of unspent outputs for a *scriptPubKey* /// Returns the list of unspent outputs for a *scriptPubKey*
fn script_list_unspent(&self, script: &Script) -> Result<Vec<ListUnspentRes>, Error>; fn script_list_unspent(&self, script: &Script) -> Result<Vec<ListUnspentRes>, Error>;
@ -141,7 +336,8 @@ pub trait ElectrumApi {
scripts: I, scripts: I,
) -> Result<Vec<Vec<ListUnspentRes>>, Error> ) -> Result<Vec<Vec<ListUnspentRes>>, Error>
where where
I: IntoIterator<Item = &'s Script>; I: IntoIterator + Clone,
I::Item: Borrow<&'s Script>;
/// Gets the raw bytes of a transaction with `txid`. Returns an error if not found. /// Gets the raw bytes of a transaction with `txid`. Returns an error if not found.
fn transaction_get_raw(&self, txid: &Txid) -> Result<Vec<u8>, Error>; fn transaction_get_raw(&self, txid: &Txid) -> Result<Vec<u8>, Error>;
@ -151,22 +347,25 @@ pub trait ElectrumApi {
/// Takes a list of `txids` and returns a list of transactions raw bytes. /// Takes a list of `txids` and returns a list of transactions raw bytes.
fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result<Vec<Vec<u8>>, Error> fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result<Vec<Vec<u8>>, Error>
where where
I: IntoIterator<Item = &'t Txid>; I: IntoIterator + Clone,
I::Item: Borrow<&'t Txid>;
/// Batch version of [`block_header_raw`](#method.block_header_raw). /// Batch version of [`block_header_raw`](#method.block_header_raw).
/// ///
/// Takes a list of `heights` of blocks and returns a list of block header raw bytes. /// Takes a list of `heights` of blocks and returns a list of block header raw bytes.
fn batch_block_header_raw<'s, I>(&self, heights: I) -> Result<Vec<Vec<u8>>, Error> fn batch_block_header_raw<I>(&self, heights: I) -> Result<Vec<Vec<u8>>, Error>
where where
I: IntoIterator<Item = u32>; I: IntoIterator + Clone,
I::Item: Borrow<u32>;
/// Batch version of [`estimate_fee`](#method.estimate_fee). /// Batch version of [`estimate_fee`](#method.estimate_fee).
/// ///
/// Takes a list of `numbers` of blocks and returns a list of fee required in /// Takes a list of `numbers` of blocks and returns a list of fee required in
/// **Satoshis per kilobyte** to confirm a transaction in the given number of blocks. /// **Satoshis per kilobyte** to confirm a transaction in the given number of blocks.
fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result<Vec<f64>, Error> fn batch_estimate_fee<I>(&self, numbers: I) -> Result<Vec<f64>, Error>
where where
I: IntoIterator<Item = usize>; I: IntoIterator + Clone,
I::Item: Borrow<usize>;
/// Broadcasts the raw bytes of a transaction to the network. /// Broadcasts the raw bytes of a transaction to the network.
fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result<Txid, Error>; fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result<Txid, Error>;
@ -174,6 +373,28 @@ pub trait ElectrumApi {
/// Returns the merkle path for the transaction `txid` confirmed in the block at `height`. /// 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>; 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. /// Returns the capabilities of the server.
fn server_features(&self) -> Result<ServerFeaturesRes, Error>; fn server_features(&self) -> Result<ServerFeaturesRes, Error>;
@ -183,5 +404,227 @@ pub trait ElectrumApi {
#[cfg(feature = "debug-calls")] #[cfg(feature = "debug-calls")]
/// Returns the number of network calls made since the creation of the client. /// Returns the number of network calls made since the creation of the client.
fn calls_made(&self) -> usize; 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

@ -2,10 +2,9 @@
//! //!
//! This module contains definitions and helper functions used when making batch calls. //! This module contains definitions and helper functions used when making batch calls.
use bitcoin::hashes::hex::ToHex;
use bitcoin::{Script, Txid}; use bitcoin::{Script, Txid};
use types::{Param, ToElectrumScriptHash}; use crate::types::{Call, Param, ToElectrumScriptHash};
/// Helper structure that caches all the requests before they are actually sent to the server. /// Helper structure that caches all the requests before they are actually sent to the server.
/// ///
@ -17,11 +16,17 @@ use types::{Param, ToElectrumScriptHash};
/// [`Client`](../client/struct.Client.html), like /// [`Client`](../client/struct.Client.html), like
/// [`batch_script_get_balance`](../client/struct.Client.html#method.batch_script_get_balance) to ask the /// [`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. /// server for the balance of multiple scripts with a single request.
#[derive(Default)]
pub struct Batch { pub struct Batch {
calls: Vec<(String, Vec<Param>)>, calls: Vec<Call>,
} }
impl Batch { 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 /// Add one `blockchain.scripthash.listunspent` request to the batch queue
pub fn script_list_unspent(&mut self, script: &Script) { pub fn script_list_unspent(&mut self, script: &Script) {
let params = vec![Param::String(script.to_electrum_scripthash().to_hex())]; let params = vec![Param::String(script.to_electrum_scripthash().to_hex())];
@ -43,13 +48,31 @@ impl Batch {
.push((String::from("blockchain.scripthash.get_balance"), params)); .push((String::from("blockchain.scripthash.get_balance"), params));
} }
/// Add one `blockchain.scripthash.listunspent` request to the batch queue
pub fn script_subscribe(&mut self, script: &Script) {
let params = vec![Param::String(script.to_electrum_scripthash().to_hex())];
self.calls
.push((String::from("blockchain.scripthash.subscribe"), params));
}
/// Add one `blockchain.transaction.get` request to the batch queue /// Add one `blockchain.transaction.get` request to the batch queue
pub fn transaction_get(&mut self, tx_hash: &Txid) { pub fn transaction_get(&mut self, tx_hash: &Txid) {
let params = vec![Param::String(tx_hash.to_hex())]; let params = vec![Param::String(format!("{:x}", tx_hash))];
self.calls self.calls
.push((String::from("blockchain.transaction.get"), params)); .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 /// Add one `blockchain.estimatefee` request to the batch queue
pub fn estimate_fee(&mut self, number: usize) { pub fn estimate_fee(&mut self, number: usize) {
let params = vec![Param::Usize(number)]; let params = vec![Param::Usize(number)];
@ -63,6 +86,14 @@ impl Batch {
self.calls self.calls
.push((String::from("blockchain.block.header"), params)); .push((String::from("blockchain.block.header"), params));
} }
/// Returns an iterator on the batch
pub fn iter(&self) -> BatchIter<'_> {
BatchIter {
batch: self,
index: 0,
}
}
} }
impl std::iter::IntoIterator for Batch { impl std::iter::IntoIterator for Batch {
@ -74,8 +105,17 @@ impl std::iter::IntoIterator for Batch {
} }
} }
impl std::default::Default for Batch { pub struct BatchIter<'a> {
fn default() -> Self { batch: &'a Batch,
Batch { calls: Vec::new() } index: usize,
}
impl<'a> std::iter::Iterator for BatchIter<'a> {
type Item = &'a (String, Vec<Param>);
fn next(&mut self) -> Option<Self::Item> {
let val = self.batch.calls.get(self.index);
self.index += 1;
val
} }
} }

View File

@ -1,40 +1,145 @@
//! Electrum Client //! Electrum Client
use std::{borrow::Borrow, sync::RwLock};
use log::{info, warn};
use bitcoin::{Script, Txid}; use bitcoin::{Script, Txid};
use api::ElectrumApi; use crate::api::ElectrumApi;
use batch::Batch; use crate::batch::Batch;
use raw_client::*; use crate::config::Config;
use types::*; use crate::raw_client::*;
use crate::types::*;
use std::convert::TryFrom;
/// Generalized Electrum client that supports multiple backends. This wraps /// Generalized Electrum client that supports multiple backends. This wraps
/// [`RawClient`](client/struct.RawClient.html) and provides a more user-friendly /// [`RawClient`](client/struct.RawClient.html) and provides a more user-friendly
/// constructor that can choose the right backend based on the url prefix. /// constructor that can choose the right backend based on the url prefix.
/// ///
/// **This is only available with the `default` features.** /// **This is available only with the `default` features, or if `proxy` and one ssl implementation are enabled**
#[cfg(feature = "default")] pub enum ClientType {
pub enum Client { #[allow(missing_docs)]
#[doc(hidden)]
TCP(RawClient<ElectrumPlaintextStream>), TCP(RawClient<ElectrumPlaintextStream>),
#[doc(hidden)] #[allow(missing_docs)]
SSL(RawClient<ElectrumSslStream>), SSL(RawClient<ElectrumSslStream>),
#[doc(hidden)] #[allow(missing_docs)]
Socks5(RawClient<ElectrumProxyStream>), Socks5(RawClient<ElectrumProxyStream>),
} }
/// Generalized Electrum client that supports multiple backends. Can re-instantiate client_type if connections
/// drops
pub struct Client {
client_type: RwLock<ClientType>,
config: Config,
url: String,
}
macro_rules! impl_inner_call { macro_rules! impl_inner_call {
( $self:expr, $name:ident $(, $args:expr)* ) => { ( $self:expr, $name:ident $(, $args:expr)* ) => {
match $self { {
Client::TCP(inner) => inner.$name( $($args, )* ), let mut errors = vec![];
Client::SSL(inner) => inner.$name( $($args, )* ), loop {
Client::Socks5(inner) => inner.$name( $($args, )* ), let read_client = $self.client_type.read().unwrap();
let res = match &*read_client {
ClientType::TCP(inner) => inner.$name( $($args, )* ),
ClientType::SSL(inner) => inner.$name( $($args, )* ),
ClientType::Socks5(inner) => inner.$name( $($args, )* ),
};
drop(read_client);
match res {
Ok(val) => return Ok(val),
Err(Error::Protocol(_) | Error::AlreadySubscribed(_)) => {
return res;
},
Err(e) => {
let failed_attempts = errors.len() + 1;
if retries_exhausted(failed_attempts, $self.config.retry()) {
warn!("call '{}' failed after {} attempts", stringify!($name), failed_attempts);
return Err(Error::AllAttemptsErrored(errors));
}
warn!("call '{}' failed with {}, retry: {}/{}", stringify!($name), e, failed_attempts, $self.config.retry());
errors.push(e);
// Only one thread will try to recreate the client getting the write lock,
// other eventual threads will get Err and will block at the beginning of
// previous loop when trying to read()
if let Ok(mut write_client) = $self.client_type.try_write() {
loop {
std::thread::sleep(std::time::Duration::from_secs((1 << errors.len()).min(30) as u64));
match ClientType::from_config(&$self.url, &$self.config) {
Ok(new_client) => {
info!("Succesfully created new client");
*write_client = new_client;
break;
},
Err(e) => {
let failed_attempts = errors.len() + 1;
if retries_exhausted(failed_attempts, $self.config.retry()) {
warn!("re-creating client failed after {} attempts", failed_attempts);
return Err(Error::AllAttemptsErrored(errors));
}
warn!("re-creating client failed with {}, retry: {}/{}", e, failed_attempts, $self.config.retry());
errors.push(e);
}
}
}
}
},
}
}}
}
}
fn retries_exhausted(failed_attempts: usize, configured_retries: u8) -> bool {
match u8::try_from(failed_attempts) {
Ok(failed_attempts) => failed_attempts > configured_retries,
Err(_) => true, // if the usize doesn't fit into a u8, we definitely exhausted our retries
}
}
impl ClientType {
/// Constructor that supports multiple backends and allows configuration through
/// the [Config]
pub fn from_config(url: &str, config: &Config) -> Result<Self, Error> {
if url.starts_with("ssl://") {
let url = url.replacen("ssl://", "", 1);
let client = match config.socks5() {
Some(socks5) => RawClient::new_proxy_ssl(
url.as_str(),
config.validate_domain(),
socks5,
config.timeout(),
)?,
None => {
RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?
}
};
Ok(ClientType::SSL(client))
} else {
let url = url.replacen("tcp://", "", 1);
Ok(match config.socks5().as_ref() {
None => ClientType::TCP(RawClient::new(url.as_str(), config.timeout())?),
Some(socks5) => ClientType::Socks5(RawClient::new_proxy(
url.as_str(),
socks5,
config.timeout(),
)?),
})
} }
} }
} }
#[cfg(feature = "default")]
impl Client { impl Client {
/// Generic constructor that supports multiple backends and, optionally, a socks5 proxy. /// Default constructor supporting multiple backends by providing a prefix
/// ///
/// Supported prefixes are: /// Supported prefixes are:
/// - tcp:// for a TCP plaintext client. /// - tcp:// for a TCP plaintext client.
@ -42,38 +147,42 @@ impl Client {
/// ///
/// If no prefix is specified, then `tcp://` is assumed. /// If no prefix is specified, then `tcp://` is assumed.
/// ///
/// The `socks5` argument can optionally be prefixed with `socks5://`. /// See [Client::from_config] for more configuration options
/// pub fn new(url: &str) -> Result<Self, Error> {
/// **NOTE**: SSL-over-socks5 is currently not supported and will generate a runtime error. Self::from_config(url, Config::default())
pub fn new(url: &str, socks5: Option<&str>) -> Result<Self, Error> { }
let socks5 = socks5.map(|s| s.replacen("socks5://", "", 1));
if url.starts_with("ssl://") { /// Generic constructor that supports multiple backends and allows configuration through
if socks5.is_some() { /// the [Config]
return Err(Error::SSLOverSocks5); pub fn from_config(url: &str, config: Config) -> Result<Self, Error> {
} let client_type = RwLock::new(ClientType::from_config(url, &config)?);
let url = url.replacen("ssl://", "", 1); Ok(Client {
let client = RawClient::new_ssl(url.as_str(), true)?; client_type,
config,
Ok(Client::SSL(client)) url: url.to_string(),
} else { })
let url = url.replacen("tcp://", "", 1);
let client = match socks5 {
None => Client::TCP(RawClient::new(url.as_str())?),
Some(socks5) => Client::Socks5(RawClient::new_proxy(url.as_str(), socks5)?),
};
Ok(client)
}
} }
} }
#[cfg(feature = "default")]
impl ElectrumApi for Client { impl ElectrumApi for Client {
#[inline] #[inline]
fn batch_call(&self, batch: Batch) -> Result<Vec<serde_json::Value>, Error> { fn raw_call(
&self,
method_name: &str,
params: impl IntoIterator<Item = Param>,
) -> Result<serde_json::Value, Error> {
// We can't passthrough this method to the inner client because it would require the
// `params` argument to also be `Copy` (because it's used multiple times for multiple
// retries). To avoid adding this extra trait bound we instead re-direct this call to the internal
// `RawClient::internal_raw_call_with_vec` method.
let vec = params.into_iter().collect::<Vec<Param>>();
impl_inner_call!(self, internal_raw_call_with_vec, method_name, vec.clone());
}
#[inline]
fn batch_call(&self, batch: &Batch) -> Result<Vec<serde_json::Value>, Error> {
impl_inner_call!(self, batch_call, batch) impl_inner_call!(self, batch_call, batch)
} }
@ -112,6 +221,15 @@ impl ElectrumApi for Client {
impl_inner_call!(self, script_subscribe, script) impl_inner_call!(self, script_subscribe, script)
} }
#[inline]
fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result<Vec<Option<ScriptStatus>>, Error>
where
I: IntoIterator + Clone,
I::Item: Borrow<&'s Script>,
{
impl_inner_call!(self, batch_script_subscribe, scripts.clone())
}
#[inline] #[inline]
fn script_unsubscribe(&self, script: &Script) -> Result<bool, Error> { fn script_unsubscribe(&self, script: &Script) -> Result<bool, Error> {
impl_inner_call!(self, script_unsubscribe, script) impl_inner_call!(self, script_unsubscribe, script)
@ -130,9 +248,10 @@ impl ElectrumApi for Client {
#[inline] #[inline]
fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result<Vec<GetBalanceRes>, Error> fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result<Vec<GetBalanceRes>, Error>
where where
I: IntoIterator<Item = &'s Script>, I: IntoIterator + Clone,
I::Item: Borrow<&'s Script>,
{ {
impl_inner_call!(self, batch_script_get_balance, scripts) impl_inner_call!(self, batch_script_get_balance, scripts.clone())
} }
#[inline] #[inline]
@ -143,9 +262,10 @@ impl ElectrumApi for Client {
#[inline] #[inline]
fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result<Vec<Vec<GetHistoryRes>>, Error> fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result<Vec<Vec<GetHistoryRes>>, Error>
where where
I: IntoIterator<Item = &'s Script>, I: IntoIterator + Clone,
I::Item: Borrow<&'s Script>,
{ {
impl_inner_call!(self, batch_script_get_history, scripts) impl_inner_call!(self, batch_script_get_history, scripts.clone())
} }
#[inline] #[inline]
@ -159,9 +279,10 @@ impl ElectrumApi for Client {
scripts: I, scripts: I,
) -> Result<Vec<Vec<ListUnspentRes>>, Error> ) -> Result<Vec<Vec<ListUnspentRes>>, Error>
where where
I: IntoIterator<Item = &'s Script>, I: IntoIterator + Clone,
I::Item: Borrow<&'s Script>,
{ {
impl_inner_call!(self, batch_script_list_unspent, scripts) impl_inner_call!(self, batch_script_list_unspent, scripts.clone())
} }
#[inline] #[inline]
@ -172,25 +293,28 @@ impl ElectrumApi for Client {
#[inline] #[inline]
fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result<Vec<Vec<u8>>, Error> fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result<Vec<Vec<u8>>, Error>
where where
I: IntoIterator<Item = &'t Txid>, I: IntoIterator + Clone,
I::Item: Borrow<&'t Txid>,
{ {
impl_inner_call!(self, batch_transaction_get_raw, txids) impl_inner_call!(self, batch_transaction_get_raw, txids.clone())
} }
#[inline] #[inline]
fn batch_block_header_raw<'s, I>(&self, heights: I) -> Result<Vec<Vec<u8>>, Error> fn batch_block_header_raw<'s, I>(&self, heights: I) -> Result<Vec<Vec<u8>>, Error>
where where
I: IntoIterator<Item = u32>, I: IntoIterator + Clone,
I::Item: Borrow<u32>,
{ {
impl_inner_call!(self, batch_block_header_raw, heights) impl_inner_call!(self, batch_block_header_raw, heights.clone())
} }
#[inline] #[inline]
fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result<Vec<f64>, Error> fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result<Vec<f64>, Error>
where where
I: IntoIterator<Item = usize>, I: IntoIterator + Clone,
I::Item: Borrow<usize>,
{ {
impl_inner_call!(self, batch_estimate_fee, numbers) impl_inner_call!(self, batch_estimate_fee, numbers.clone())
} }
#[inline] #[inline]
@ -203,6 +327,36 @@ impl ElectrumApi for Client {
impl_inner_call!(self, transaction_get_merkle, txid, height) 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] #[inline]
fn server_features(&self) -> Result<ServerFeaturesRes, Error> { fn server_features(&self) -> Result<ServerFeaturesRes, Error> {
impl_inner_call!(self, server_features) impl_inner_call!(self, server_features)
@ -215,7 +369,92 @@ impl ElectrumApi for Client {
#[inline] #[inline]
#[cfg(feature = "debug-calls")] #[cfg(feature = "debug-calls")]
fn calls_made(&self) -> usize { fn calls_made(&self) -> Result<usize, Error> {
impl_inner_call!(self, calls_made) impl_inner_call!(self, calls_made)
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn more_failed_attempts_than_retries_means_exhausted() {
let exhausted = retries_exhausted(10, 5);
assert!(exhausted)
}
#[test]
fn failed_attempts_bigger_than_u8_means_exhausted() {
let failed_attempts = u8::MAX as usize + 1;
let exhausted = retries_exhausted(failed_attempts, u8::MAX);
assert!(exhausted)
}
#[test]
fn less_failed_attempts_means_not_exhausted() {
let exhausted = retries_exhausted(2, 5);
assert!(!exhausted)
}
#[test]
fn attempts_equals_retries_means_not_exhausted_yet() {
let exhausted = retries_exhausted(2, 2);
assert!(!exhausted)
}
#[test]
#[ignore]
fn test_local_timeout() {
// This test assumes a couple things:
// - that `localhost` is resolved to two IP addresses, `127.0.0.1` and `::1` (with the v6
// one having higher priority)
// - that the system silently drops packets to `[::1]:60000` or a different port if
// specified through `TEST_ELECTRUM_TIMEOUT_PORT`
//
// this can be setup with: ip6tables -I INPUT 1 -p tcp -d ::1 --dport 60000 -j DROP
// and removed with: ip6tables -D INPUT -p tcp -d ::1 --dport 60000 -j DROP
//
// The test tries to create a client to `localhost` and expects it to succeed, but only
// after at least 2 seconds have passed which is roughly the timeout time for the first
// try.
use std::net::TcpListener;
use std::sync::mpsc::channel;
use std::time::{Duration, Instant};
let endpoint =
std::env::var("TEST_ELECTRUM_TIMEOUT_PORT").unwrap_or("localhost:60000".into());
let (sender, receiver) = channel();
std::thread::spawn(move || {
let listener = TcpListener::bind("127.0.0.1:60000").unwrap();
sender.send(()).unwrap();
for _stream in listener.incoming() {
std::thread::sleep(Duration::from_secs(60))
}
});
receiver
.recv_timeout(Duration::from_secs(5))
.expect("Can't start local listener");
let now = Instant::now();
let client = Client::from_config(
&endpoint,
crate::config::ConfigBuilder::new()
.timeout(Some(Duration::from_secs(5)))
.build(),
);
let elapsed = now.elapsed();
assert!(client.is_ok());
assert!(elapsed > Duration::from_secs(2));
}
}

149
src/config.rs Normal file
View File

@ -0,0 +1,149 @@
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
socks5: Option<Socks5Config>,
/// timeout in seconds, default None (depends on TcpStream default)
timeout: Option<Duration>,
/// number of retry if any error, default 1
retry: u8,
/// when ssl, validate the domain, default true
validate_domain: bool,
}
/// Configuration for Socks5
#[derive(Debug, Clone)]
pub struct Socks5Config {
/// The address of the socks5 service
pub addr: String,
/// Optional credential for the service
pub credentials: Option<Socks5Credential>,
}
/// Credential for the proxy
#[derive(Debug, Clone)]
pub struct Socks5Credential {
pub username: String,
pub password: String,
}
/// [Config] Builder
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
/// Create a builder with a default config, equivalent to [ConfigBuilder::default()]
pub fn new() -> Self {
ConfigBuilder {
config: Config::default(),
}
}
/// Set the socks5 config if Some, it accept an `Option` because it's easier for the caller to use
/// in a method chain
pub fn socks5(mut self, socks5_config: Option<Socks5Config>) -> Self {
self.config.socks5 = socks5_config;
self
}
/// Sets the timeout
pub fn timeout(mut self, timeout: Option<Duration>) -> Self {
self.config.timeout = timeout;
self
}
/// Sets the retry attempts number
pub fn retry(mut self, retry: u8) -> Self {
self.config.retry = retry;
self
}
/// Sets if the domain has to be validated
pub fn validate_domain(mut self, validate_domain: bool) -> Self {
self.config.validate_domain = validate_domain;
self
}
/// Return the config and consume the builder
pub fn build(self) -> Config {
self.config
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl Socks5Config {
/// Socks5Config constructor without credentials
pub fn new(addr: impl ToString) -> Self {
let addr = addr.to_string().replacen("socks5://", "", 1);
Socks5Config {
addr,
credentials: None,
}
}
/// Socks5Config constructor if we have credentials
pub fn with_credentials(addr: impl ToString, username: String, password: String) -> Self {
let mut config = Socks5Config::new(addr);
config.credentials = Some(Socks5Credential { username, password });
config
}
}
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()
}
}
impl Default for Config {
fn default() -> Self {
Config {
socks5: None,
timeout: None,
retry: 1,
validate_domain: true,
}
}
}

View File

@ -14,7 +14,7 @@
//! ```no_run //! ```no_run
//! use electrum_client::{Client, ElectrumApi}; //! use electrum_client::{Client, ElectrumApi};
//! //!
//! let mut client = Client::new("tcp://electrum.blockstream.info:50001", None)?; //! let mut client = Client::new("tcp://electrum.blockstream.info:50001")?;
//! let response = client.server_features()?; //! let response = client.server_features()?;
//! # Ok::<(), electrum_client::Error>(()) //! # Ok::<(), electrum_client::Error>(())
//! ``` //! ```
@ -25,27 +25,59 @@ extern crate log;
#[cfg(feature = "use-openssl")] #[cfg(feature = "use-openssl")]
extern crate openssl; extern crate openssl;
#[cfg(all( #[cfg(all(
any(feature = "default", feature = "use-rustls"), any(
feature = "default",
feature = "use-rustls",
feature = "use-rustls-ring"
),
not(feature = "use-openssl") not(feature = "use-openssl")
))] ))]
extern crate rustls; extern crate rustls;
extern crate serde; extern crate serde;
extern crate serde_json; extern crate serde_json;
#[cfg(any(feature = "default", feature = "proxy"))]
extern crate socks; #[cfg(any(
#[cfg(any(feature = "use-rustls", feature = "default"))] feature = "default",
extern crate webpki; feature = "use-rustls",
#[cfg(any(feature = "use-rustls", feature = "default"))] feature = "use-rustls-ring"
))]
extern crate webpki_roots; extern crate webpki_roots;
#[cfg(any(feature = "default", feature = "proxy"))]
extern crate byteorder;
#[cfg(all(unix, any(feature = "default", feature = "proxy")))]
extern crate libc;
#[cfg(all(windows, any(feature = "default", feature = "proxy")))]
extern crate winapi;
#[cfg(any(feature = "default", feature = "proxy"))]
pub mod socks;
mod api; mod api;
mod batch; mod batch;
#[cfg(any(
all(feature = "proxy", feature = "use-openssl"),
all(feature = "proxy", feature = "use-rustls"),
all(feature = "proxy", feature = "use-rustls-ring")
))]
pub mod client; pub mod client;
mod config;
pub mod raw_client; pub mod raw_client;
mod stream; mod stream;
mod types; mod types;
pub mod utils;
pub use api::ElectrumApi; pub use api::ElectrumApi;
pub use batch::Batch; pub use batch::Batch;
#[cfg(any(
all(feature = "proxy", feature = "use-openssl"),
all(feature = "proxy", feature = "use-rustls"),
all(feature = "proxy", feature = "use-rustls-ring")
))]
pub use client::*; pub use client::*;
pub use config::{Config, ConfigBuilder, Socks5Config};
pub use types::*; pub use types::*;

File diff suppressed because it is too large Load Diff

162
src/socks/mod.rs Normal file
View File

@ -0,0 +1,162 @@
//! SOCKS proxy clients
use std::io;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs};
use std::vec;
pub use self::v4::{Socks4Listener, Socks4Stream};
pub use self::v5::{Socks5Datagram, Socks5Listener, Socks5Stream};
mod v4;
mod v5;
mod writev;
/// A description of a connection target.
#[derive(Debug, Clone)]
pub enum TargetAddr {
/// Connect to an IP address.
Ip(SocketAddr),
/// Connect to a fully qualified domain name.
///
/// The domain name will be passed along to the proxy server and DNS lookup
/// will happen there.
Domain(String, u16),
}
impl ToSocketAddrs for TargetAddr {
type Iter = Iter;
fn to_socket_addrs(&self) -> io::Result<Iter> {
let inner = match *self {
TargetAddr::Ip(addr) => IterInner::Ip(Some(addr)),
TargetAddr::Domain(ref domain, port) => {
let it = (&**domain, port).to_socket_addrs()?;
IterInner::Domain(it)
}
};
Ok(Iter(inner))
}
}
enum IterInner {
Ip(Option<SocketAddr>),
Domain(vec::IntoIter<SocketAddr>),
}
/// An iterator over `SocketAddr`s associated with a `TargetAddr`.
pub struct Iter(IterInner);
impl Iterator for Iter {
type Item = SocketAddr;
fn next(&mut self) -> Option<SocketAddr> {
match self.0 {
IterInner::Ip(ref mut addr) => addr.take(),
IterInner::Domain(ref mut it) => it.next(),
}
}
}
/// A trait for objects that can be converted to `TargetAddr`.
pub trait ToTargetAddr {
/// Converts the value of `self` to a `TargetAddr`.
fn to_target_addr(&self) -> io::Result<TargetAddr>;
}
impl ToTargetAddr for TargetAddr {
fn to_target_addr(&self) -> io::Result<TargetAddr> {
Ok(self.clone())
}
}
impl ToTargetAddr for SocketAddr {
fn to_target_addr(&self) -> io::Result<TargetAddr> {
Ok(TargetAddr::Ip(*self))
}
}
impl ToTargetAddr for SocketAddrV4 {
fn to_target_addr(&self) -> io::Result<TargetAddr> {
SocketAddr::V4(*self).to_target_addr()
}
}
impl ToTargetAddr for SocketAddrV6 {
fn to_target_addr(&self) -> io::Result<TargetAddr> {
SocketAddr::V6(*self).to_target_addr()
}
}
impl ToTargetAddr for (Ipv4Addr, u16) {
fn to_target_addr(&self) -> io::Result<TargetAddr> {
SocketAddrV4::new(self.0, self.1).to_target_addr()
}
}
impl ToTargetAddr for (Ipv6Addr, u16) {
fn to_target_addr(&self) -> io::Result<TargetAddr> {
SocketAddrV6::new(self.0, self.1, 0, 0).to_target_addr()
}
}
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>() {
return (addr, self.1).to_target_addr();
}
if let Ok(addr) = self.0.parse::<Ipv6Addr>() {
return (addr, self.1).to_target_addr();
}
Ok(TargetAddr::Domain(self.0.to_owned(), self.1))
}
}
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>() {
return addr.to_target_addr();
}
if let Ok(addr) = self.parse::<SocketAddrV6>() {
return addr.to_target_addr();
}
// split the string by ':' and convert the second part to u16
let mut parts_iter = self.rsplitn(2, ':');
let port_str = match parts_iter.next() {
Some(s) => s,
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid socket address",
))
}
};
let host = match parts_iter.next() {
Some(s) => s,
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid socket address",
))
}
};
let port: u16 = match port_str.parse() {
Ok(p) => p,
Err(_) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid port value",
))
}
};
(host, port).to_target_addr()
}
}

280
src/socks/v4.rs Normal file
View File

@ -0,0 +1,280 @@
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::io::{self, Read, Write};
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, SocketAddrV6, TcpStream, ToSocketAddrs};
use super::{TargetAddr, ToTargetAddr};
fn read_response(socket: &mut TcpStream) -> io::Result<SocketAddrV4> {
let mut response = [0u8; 8];
socket.read_exact(&mut response)?;
let mut response = &response[..];
if response.read_u8()? != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid response version",
));
}
match response.read_u8()? {
90 => {}
91 => return Err(io::Error::other("request rejected or failed")),
92 => {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"request rejected because SOCKS server cannot connect to \
idnetd on the client",
))
}
93 => {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"request rejected because the client program and identd \
report different user-ids",
))
}
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid response code",
))
}
}
let port = response.read_u16::<BigEndian>()?;
let ip = Ipv4Addr::from(response.read_u32::<BigEndian>()?);
Ok(SocketAddrV4::new(ip, port))
}
/// A SOCKS4 client.
#[derive(Debug)]
pub struct Socks4Stream {
socket: TcpStream,
proxy_addr: SocketAddrV4,
}
impl Socks4Stream {
/// Connects to a target server through a SOCKS4 proxy.
///
/// # Note
///
/// If `target` is a `TargetAddr::Domain`, the domain name will be forwarded
/// to the proxy server using the SOCKS4A protocol extension. If the proxy
/// server does not support SOCKS4A, consider performing the DNS lookup
/// locally and passing a `TargetAddr::Ip`.
pub fn connect<T, U>(proxy: T, target: U, userid: &str) -> io::Result<Socks4Stream>
where
T: ToSocketAddrs,
U: ToTargetAddr,
{
Self::connect_raw(1, proxy, target, userid)
}
fn connect_raw<T, U>(command: u8, proxy: T, target: U, userid: &str) -> io::Result<Socks4Stream>
where
T: ToSocketAddrs,
U: ToTargetAddr,
{
let mut socket = TcpStream::connect(proxy)?;
let target = target.to_target_addr()?;
let mut packet = vec![];
let _ = packet.write_u8(4); // version
let _ = packet.write_u8(command); // command code
match target.to_target_addr()? {
TargetAddr::Ip(addr) => {
let addr = match addr {
SocketAddr::V4(addr) => addr,
SocketAddr::V6(_) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"SOCKS4 does not support IPv6",
));
}
};
let _ = packet.write_u16::<BigEndian>(addr.port());
let _ = packet.write_u32::<BigEndian>((*addr.ip()).into());
let _ = packet.write_all(userid.as_bytes());
let _ = packet.write_u8(0);
}
TargetAddr::Domain(ref host, port) => {
let _ = packet.write_u16::<BigEndian>(port);
let _ = packet.write_u32::<BigEndian>(Ipv4Addr::new(0, 0, 0, 1).into());
let _ = packet.write_all(userid.as_bytes());
let _ = packet.write_u8(0);
packet.extend(host.as_bytes());
let _ = packet.write_u8(0);
}
}
socket.write_all(&packet)?;
let proxy_addr = read_response(&mut socket)?;
Ok(Socks4Stream { socket, proxy_addr })
}
/// Returns the proxy-side address of the connection between the proxy and
/// target server.
pub fn proxy_addr(&self) -> SocketAddrV4 {
self.proxy_addr
}
/// Returns a shared reference to the inner `TcpStream`.
pub fn get_ref(&self) -> &TcpStream {
&self.socket
}
/// Returns a mutable reference to the inner `TcpStream`.
pub fn get_mut(&mut self) -> &mut TcpStream {
&mut self.socket
}
/// Consumes the `Socks4Stream`, returning the inner `TcpStream`.
pub fn into_inner(self) -> TcpStream {
self.socket
}
}
impl Read for Socks4Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.socket.read(buf)
}
}
impl Read for &Socks4Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
(&self.socket).read(buf)
}
}
impl Write for Socks4Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.socket.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.socket.flush()
}
}
impl Write for &Socks4Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
(&self.socket).write(buf)
}
fn flush(&mut self) -> io::Result<()> {
(&self.socket).flush()
}
}
/// A SOCKS4 BIND client.
#[derive(Debug)]
pub struct Socks4Listener(Socks4Stream);
impl Socks4Listener {
/// Initiates a BIND request to the specified proxy.
///
/// The proxy will filter incoming connections based on the value of
/// `target`.
pub fn bind<T, U>(proxy: T, target: U, userid: &str) -> io::Result<Socks4Listener>
where
T: ToSocketAddrs,
U: ToTargetAddr,
{
Socks4Stream::connect_raw(2, proxy, target, userid).map(Socks4Listener)
}
/// The address of the proxy-side TCP listener.
///
/// This should be forwarded to the remote process, which should open a
/// connection to it.
pub fn proxy_addr(&self) -> io::Result<SocketAddr> {
if self.0.proxy_addr.ip().octets() != [0, 0, 0, 0] {
Ok(SocketAddr::V4(self.0.proxy_addr()))
} else {
let port = self.0.proxy_addr.port();
let peer = match self.0.socket.peer_addr()? {
SocketAddr::V4(addr) => SocketAddr::V4(SocketAddrV4::new(*addr.ip(), port)),
SocketAddr::V6(addr) => SocketAddr::V6(SocketAddrV6::new(*addr.ip(), port, 0, 0)),
};
Ok(peer)
}
}
/// Waits for the remote process to connect to the proxy server.
///
/// The value of `proxy_addr` should be forwarded to the remote process
/// before this method is called.
pub fn accept(mut self) -> io::Result<Socks4Stream> {
self.0.proxy_addr = read_response(&mut self.0.socket)?;
Ok(self.0)
}
}
#[cfg(test)]
mod test {
use std::io::{Read, Write};
use std::net::{SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs};
use super::*;
fn google_ip() -> SocketAddrV4 {
"google.com:80"
.to_socket_addrs()
.unwrap()
.filter_map(|a| match a {
SocketAddr::V4(a) => Some(a),
SocketAddr::V6(_) => None,
})
.next()
.unwrap()
}
#[test]
#[ignore]
fn google() {
let mut socket = Socks4Stream::connect("127.0.0.1:1080", google_ip(), "").unwrap();
socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
let mut result = vec![];
socket.read_to_end(&mut result).unwrap();
println!("{}", String::from_utf8_lossy(&result));
assert!(result.starts_with(b"HTTP/1.0"));
assert!(result.ends_with(b"</HTML>\r\n") || result.ends_with(b"</html>"));
}
#[test]
#[ignore] // dante doesn't support SOCKS4A
fn google_dns() {
let mut socket = Socks4Stream::connect("127.0.0.1:8080", "google.com:80", "").unwrap();
socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
let mut result = vec![];
socket.read_to_end(&mut result).unwrap();
println!("{}", String::from_utf8_lossy(&result));
assert!(result.starts_with(b"HTTP/1.0"));
assert!(result.ends_with(b"</HTML>\r\n") || result.ends_with(b"</html>"));
}
#[test]
#[ignore]
fn bind() {
// First figure out our local address that we'll be connecting from
let socket = Socks4Stream::connect("127.0.0.1:1080", google_ip(), "").unwrap();
let addr = socket.proxy_addr();
let listener = Socks4Listener::bind("127.0.0.1:1080", addr, "").unwrap();
let addr = listener.proxy_addr().unwrap();
let mut end = TcpStream::connect(addr).unwrap();
let mut conn = listener.accept().unwrap();
conn.write_all(b"hello world").unwrap();
drop(conn);
let mut result = vec![];
end.read_to_end(&mut result).unwrap();
assert_eq!(result, b"hello world");
}
}

798
src/socks/v5.rs Normal file
View File

@ -0,0 +1,798 @@
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use std::cmp;
use std::io::{self, Read, Write};
use std::net::{
Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, TcpStream, ToSocketAddrs, UdpSocket,
};
use std::ptr;
use std::time::Duration;
use super::writev::WritevExt;
use super::{TargetAddr, ToTargetAddr};
const MAX_ADDR_LEN: usize = 260;
fn read_addr<R: Read>(socket: &mut R) -> io::Result<TargetAddr> {
match socket.read_u8()? {
1 => {
let ip = Ipv4Addr::from(socket.read_u32::<BigEndian>()?);
let port = socket.read_u16::<BigEndian>()?;
Ok(TargetAddr::Ip(SocketAddr::V4(SocketAddrV4::new(ip, port))))
}
3 => {
let len = socket.read_u8()?;
let mut domain = vec![0; len as usize];
socket.read_exact(&mut domain)?;
let domain = String::from_utf8(domain)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
let port = socket.read_u16::<BigEndian>()?;
Ok(TargetAddr::Domain(domain, port))
}
4 => {
let mut ip = [0; 16];
socket.read_exact(&mut ip)?;
let ip = Ipv6Addr::from(ip);
let port = socket.read_u16::<BigEndian>()?;
Ok(TargetAddr::Ip(SocketAddr::V6(SocketAddrV6::new(
ip, port, 0, 0,
))))
}
_ => Err(io::Error::other("unsupported address type")),
}
}
fn read_response(socket: &mut TcpStream) -> io::Result<TargetAddr> {
if socket.read_u8()? != 5 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid response version",
));
}
match socket.read_u8()? {
0 => {}
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 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid reserved byte",
));
}
read_addr(socket)
}
fn write_addr(mut packet: &mut [u8], target: &TargetAddr) -> io::Result<usize> {
let start_len = packet.len();
match *target {
TargetAddr::Ip(SocketAddr::V4(addr)) => {
packet.write_u8(1).unwrap();
packet.write_u32::<BigEndian>((*addr.ip()).into()).unwrap();
packet.write_u16::<BigEndian>(addr.port()).unwrap();
}
TargetAddr::Ip(SocketAddr::V6(addr)) => {
packet.write_u8(4).unwrap();
packet.write_all(&addr.ip().octets()).unwrap();
packet.write_u16::<BigEndian>(addr.port()).unwrap();
}
TargetAddr::Domain(ref domain, port) => {
packet.write_u8(3).unwrap();
if domain.len() > u8::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"domain name too long",
));
}
packet.write_u8(domain.len() as u8).unwrap();
packet.write_all(domain.as_bytes()).unwrap();
packet.write_u16::<BigEndian>(port).unwrap();
}
}
Ok(start_len - packet.len())
}
/// Authentication methods
#[derive(Debug)]
enum Authentication<'a> {
Password {
username: &'a str,
password: &'a str,
},
None,
}
impl Authentication<'_> {
fn id(&self) -> u8 {
match *self {
Authentication::Password { .. } => 2,
Authentication::None => 0,
}
}
fn is_no_auth(&self) -> bool {
matches!(*self, Authentication::None)
}
}
/// A SOCKS5 client.
#[derive(Debug)]
pub struct Socks5Stream {
socket: TcpStream,
proxy_addr: TargetAddr,
}
impl Socks5Stream {
/// Connects to a target server through a SOCKS5 proxy.
pub fn connect<T, U>(proxy: T, target: U, timeout: Option<Duration>) -> io::Result<Socks5Stream>
where
T: ToSocketAddrs,
U: ToTargetAddr,
{
Self::connect_raw(1, proxy, target, &Authentication::None, timeout)
}
/// Connects to a target server through a SOCKS5 proxy using given
/// username and password.
pub fn connect_with_password<T, U>(
proxy: T,
target: U,
username: &str,
password: &str,
timeout: Option<Duration>,
) -> io::Result<Socks5Stream>
where
T: ToSocketAddrs,
U: ToTargetAddr,
{
let auth = Authentication::Password { username, password };
Self::connect_raw(1, proxy, target, &auth, timeout)
}
fn connect_raw<T, U>(
command: u8,
proxy: T,
target: U,
auth: &Authentication,
timeout: Option<Duration>,
) -> io::Result<Socks5Stream>
where
T: ToSocketAddrs,
U: ToTargetAddr,
{
let mut socket = if let Some(timeout) = timeout {
let addr = proxy.to_socket_addrs()?.next().unwrap();
TcpStream::connect_timeout(&addr, timeout)?
} else {
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 };
let packet = [
5, // protocol version
if auth.is_no_auth() { 1 } else { 2 }, // method count
auth.id(), // method
0, // no auth (always offered)
];
socket.write_all(&packet[..packet_len])?;
let mut buf = [0; 2];
socket.read_exact(&mut buf)?;
let response_version = buf[0];
let selected_method = buf[1];
if response_version != 5 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid response version",
));
}
if selected_method == 0xff {
return Err(io::Error::other("no acceptable auth methods"));
}
if selected_method != auth.id() && selected_method != Authentication::None.id() {
return Err(io::Error::other("unknown auth method"));
}
match *auth {
Authentication::Password { username, password } if selected_method == auth.id() => {
Self::password_authentication(&mut socket, username, password)?
}
_ => (),
}
let mut packet = [0; MAX_ADDR_LEN + 3];
packet[0] = 5; // protocol version
packet[1] = command; // command
packet[2] = 0; // reserved
let len = write_addr(&mut packet[3..], &target)?;
socket.write_all(&packet[..len + 3])?;
let proxy_addr = read_response(&mut socket)?;
Ok(Socks5Stream { socket, proxy_addr })
}
fn password_authentication(
socket: &mut TcpStream,
username: &str,
password: &str,
) -> io::Result<()> {
if username.is_empty() || username.len() > 255 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid username",
));
};
if password.is_empty() || password.len() > 255 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid password",
));
}
let mut packet = [0; 515];
let packet_size = 3 + username.len() + password.len();
packet[0] = 1; // version
packet[1] = username.len() as u8;
packet[2..2 + username.len()].copy_from_slice(username.as_bytes());
packet[2 + username.len()] = password.len() as u8;
packet[3 + username.len()..packet_size].copy_from_slice(password.as_bytes());
socket.write_all(&packet[..packet_size])?;
let mut buf = [0; 2];
socket.read_exact(&mut buf)?;
if buf[0] != 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid response version",
));
}
if buf[1] != 0 {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"password authentication failed",
));
}
Ok(())
}
/// Returns the proxy-side address of the connection between the proxy and
/// target server.
pub fn proxy_addr(&self) -> &TargetAddr {
&self.proxy_addr
}
/// Returns a shared reference to the inner `TcpStream`.
pub fn get_ref(&self) -> &TcpStream {
&self.socket
}
/// Returns a mutable reference to the inner `TcpStream`.
pub fn get_mut(&mut self) -> &mut TcpStream {
&mut self.socket
}
/// Consumes the `Socks5Stream`, returning the inner `TcpStream`.
pub fn into_inner(self) -> TcpStream {
self.socket
}
}
impl Read for Socks5Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.socket.read(buf)
}
}
impl Read for &Socks5Stream {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
(&self.socket).read(buf)
}
}
impl Write for Socks5Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.socket.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.socket.flush()
}
}
impl Write for &Socks5Stream {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
(&self.socket).write(buf)
}
fn flush(&mut self) -> io::Result<()> {
(&self.socket).flush()
}
}
/// A SOCKS5 BIND client.
#[derive(Debug)]
pub struct Socks5Listener(Socks5Stream);
impl Socks5Listener {
/// Initiates a BIND request to the specified proxy.
///
/// The proxy will filter incoming connections based on the value of
/// `target`.
pub fn bind<T, U>(proxy: T, target: U, timeout: Option<Duration>) -> io::Result<Socks5Listener>
where
T: ToSocketAddrs,
U: ToTargetAddr,
{
Socks5Stream::connect_raw(2, proxy, target, &Authentication::None, timeout)
.map(Socks5Listener)
}
/// Initiates a BIND request to the specified proxy using given username
/// and password.
///
/// The proxy will filter incoming connections based on the value of
/// `target`.
pub fn bind_with_password<T, U>(
proxy: T,
target: U,
username: &str,
password: &str,
timeout: Option<Duration>,
) -> io::Result<Socks5Listener>
where
T: ToSocketAddrs,
U: ToTargetAddr,
{
let auth = Authentication::Password { username, password };
Socks5Stream::connect_raw(2, proxy, target, &auth, timeout).map(Socks5Listener)
}
/// The address of the proxy-side TCP listener.
///
/// This should be forwarded to the remote process, which should open a
/// connection to it.
pub fn proxy_addr(&self) -> &TargetAddr {
&self.0.proxy_addr
}
/// Waits for the remote process to connect to the proxy server.
///
/// The value of `proxy_addr` should be forwarded to the remote process
/// before this method is called.
pub fn accept(mut self) -> io::Result<Socks5Stream> {
self.0.proxy_addr = read_response(&mut self.0.socket)?;
Ok(self.0)
}
}
/// A SOCKS5 UDP client.
#[derive(Debug)]
pub struct Socks5Datagram {
socket: UdpSocket,
// keeps the session alive
stream: Socks5Stream,
}
impl Socks5Datagram {
/// Creates a UDP socket bound to the specified address which will have its
/// traffic routed through the specified proxy.
pub fn bind<T, U>(proxy: T, addr: U, timeout: Option<Duration>) -> io::Result<Socks5Datagram>
where
T: ToSocketAddrs,
U: ToSocketAddrs,
{
Self::bind_internal(proxy, addr, &Authentication::None, timeout)
}
/// Creates a UDP socket bound to the specified address which will have its
/// traffic routed through the specified proxy. The given username and password
/// is used to authenticate to the SOCKS proxy.
pub fn bind_with_password<T, U>(
proxy: T,
addr: U,
username: &str,
password: &str,
timeout: Option<Duration>,
) -> io::Result<Socks5Datagram>
where
T: ToSocketAddrs,
U: ToSocketAddrs,
{
let auth = Authentication::Password { username, password };
Self::bind_internal(proxy, addr, &auth, timeout)
}
fn bind_internal<T, U>(
proxy: T,
addr: U,
auth: &Authentication,
timeout: Option<Duration>,
) -> io::Result<Socks5Datagram>
where
T: ToSocketAddrs,
U: ToSocketAddrs,
{
// we don't know what our IP is from the perspective of the proxy, so
// don't try to pass `addr` in here.
let dst = TargetAddr::Ip(SocketAddr::V4(SocketAddrV4::new(
Ipv4Addr::new(0, 0, 0, 0),
0,
)));
let stream = Socks5Stream::connect_raw(3, proxy, dst, auth, timeout)?;
let socket = UdpSocket::bind(addr)?;
socket.connect(&stream.proxy_addr)?;
Ok(Socks5Datagram { socket, stream })
}
/// Like `UdpSocket::send_to`.
///
/// # Note
///
/// The SOCKS protocol inserts a header at the beginning of the message. The
/// header will be 10 bytes for an IPv4 address, 22 bytes for an IPv6
/// address, and 7 bytes plus the length of the domain for a domain address.
pub fn send_to<A>(&self, buf: &[u8], addr: A) -> io::Result<usize>
where
A: ToTargetAddr,
{
let addr = addr.to_target_addr()?;
let mut header = [0; MAX_ADDR_LEN + 3];
// first two bytes are reserved at 0
// third byte is the fragment id at 0
let len = write_addr(&mut header[3..], &addr)?;
self.socket.writev([&header[..len + 3], buf])
}
/// Like `UdpSocket::recv_from`.
pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, TargetAddr)> {
let mut header = [0; MAX_ADDR_LEN + 3];
let len = self.socket.readv([&mut header, buf])?;
let overflow = len.saturating_sub(header.len());
let header_len = cmp::min(header.len(), len);
let mut header = &mut &header[..header_len];
if header.read_u16::<BigEndian>()? != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid reserved bytes",
));
}
if header.read_u8()? != 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid fragment id",
));
}
let addr = read_addr(&mut header)?;
unsafe {
ptr::copy(buf.as_ptr(), buf.as_mut_ptr().add(header.len()), overflow);
}
buf[..header.len()].copy_from_slice(header);
Ok((header.len() + overflow, addr))
}
/// Returns the address of the proxy-side UDP socket through which all
/// messages will be routed.
pub fn proxy_addr(&self) -> &TargetAddr {
&self.stream.proxy_addr
}
/// Returns a shared reference to the inner socket.
pub fn get_ref(&self) -> &UdpSocket {
&self.socket
}
/// Returns a mutable reference to the inner socket.
pub fn get_mut(&mut self) -> &mut UdpSocket {
&mut self.socket
}
}
#[cfg(test)]
mod test {
use std::io::{Read, Write};
use std::net::{TcpStream, ToSocketAddrs, UdpSocket};
use super::*;
const SOCKS_PROXY_NO_AUTH_ONLY: &str = "127.0.0.1:1080";
const SOCKS_PROXY_PASSWD_ONLY: &str = "127.0.0.1:1081";
#[test]
#[ignore]
fn google_no_auth() {
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
let socket = Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, addr, None).unwrap();
google(socket);
}
#[test]
#[ignore]
fn google_with_password() {
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
let socket = Socks5Stream::connect_with_password(
SOCKS_PROXY_PASSWD_ONLY,
addr,
"testuser",
"testpass",
None,
)
.unwrap();
google(socket);
}
fn google(mut socket: Socks5Stream) {
socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
let mut result = vec![];
socket.read_to_end(&mut result).unwrap();
println!("{}", String::from_utf8_lossy(&result));
assert!(result.starts_with(b"HTTP/1.0"));
assert!(result.ends_with(b"</HTML>\r\n") || result.ends_with(b"</html>"));
}
#[test]
#[ignore]
fn google_dns() {
let mut socket =
Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, "google.com:80", None).unwrap();
socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
let mut result = vec![];
socket.read_to_end(&mut result).unwrap();
println!("{}", String::from_utf8_lossy(&result));
assert!(result.starts_with(b"HTTP/1.0"));
assert!(result.ends_with(b"</HTML>\r\n") || result.ends_with(b"</html>"));
}
#[test]
#[ignore]
fn bind_no_auth() {
let addr = find_address();
let listener = Socks5Listener::bind(SOCKS_PROXY_NO_AUTH_ONLY, addr, None).unwrap();
bind(listener);
}
#[test]
#[ignore]
fn bind_with_password_supported_but_no_auth_used() {
let addr = find_address();
let listener = Socks5Listener::bind_with_password(
SOCKS_PROXY_NO_AUTH_ONLY,
addr,
"unused_and_invalid_username",
"unused_and_invalid_password",
None,
)
.unwrap();
bind(listener);
}
#[test]
#[ignore]
fn bind_with_password() {
let addr = find_address();
let listener = Socks5Listener::bind_with_password(
"127.0.0.1:1081",
addr,
"testuser",
"testpass",
None,
)
.unwrap();
bind(listener);
}
fn bind(listener: Socks5Listener) {
let addr = listener.proxy_addr().clone();
let mut end = TcpStream::connect(addr).unwrap();
let mut conn = listener.accept().unwrap();
conn.write_all(b"hello world").unwrap();
drop(conn);
let mut result = vec![];
end.read_to_end(&mut result).unwrap();
assert_eq!(result, b"hello world");
}
// First figure out our local address that we'll be connecting from
fn find_address() -> TargetAddr {
let socket =
Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, "google.com:80", None).unwrap();
socket.proxy_addr().to_owned()
}
#[test]
#[ignore]
fn associate_no_auth() {
let socks =
Socks5Datagram::bind(SOCKS_PROXY_NO_AUTH_ONLY, "127.0.0.1:15410", None).unwrap();
associate(socks, "127.0.0.1:15411");
}
#[test]
#[ignore]
fn associate_with_password() {
let socks = Socks5Datagram::bind_with_password(
SOCKS_PROXY_PASSWD_ONLY,
"127.0.0.1:15414",
"testuser",
"testpass",
None,
)
.unwrap();
associate(socks, "127.0.0.1:15415");
}
fn associate(socks: Socks5Datagram, socket_addr: &str) {
let socket = UdpSocket::bind(socket_addr).unwrap();
socks.send_to(b"hello world!", socket_addr).unwrap();
let mut buf = [0; 13];
let (len, addr) = socket.recv_from(&mut buf).unwrap();
assert_eq!(len, 12);
assert_eq!(&buf[..12], b"hello world!");
socket.send_to(b"hello world!", addr).unwrap();
let len = socks.recv_from(&mut buf).unwrap().0;
assert_eq!(len, 12);
assert_eq!(&buf[..12], b"hello world!");
}
#[test]
#[ignore]
fn associate_long() {
let socks =
Socks5Datagram::bind(SOCKS_PROXY_NO_AUTH_ONLY, "127.0.0.1:15412", None).unwrap();
let socket_addr = "127.0.0.1:15413";
let socket = UdpSocket::bind(socket_addr).unwrap();
let mut msg = vec![];
for i in 0..(MAX_ADDR_LEN + 100) {
msg.push(i as u8);
}
socks.send_to(&msg, socket_addr).unwrap();
let mut buf = vec![0; msg.len() + 1];
let (len, addr) = socket.recv_from(&mut buf).unwrap();
assert_eq!(len, msg.len());
assert_eq!(msg, &buf[..msg.len()]);
socket.send_to(&msg, addr).unwrap();
let mut buf = vec![0; msg.len() + 1];
let len = socks.recv_from(&mut buf).unwrap().0;
assert_eq!(len, msg.len());
assert_eq!(msg, &buf[..msg.len()]);
}
#[test]
#[ignore]
fn incorrect_password() {
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
let err = Socks5Stream::connect_with_password(
SOCKS_PROXY_PASSWD_ONLY,
addr,
"testuser",
"invalid",
None,
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(err.to_string(), "password authentication failed");
}
#[test]
#[ignore]
fn auth_method_not_supported() {
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
let err = Socks5Stream::connect(SOCKS_PROXY_PASSWD_ONLY, addr, None).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::Other);
assert_eq!(err.to_string(), "no acceptable auth methods");
}
#[test]
#[ignore]
fn username_and_password_length() {
let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap();
let err = Socks5Stream::connect_with_password(
SOCKS_PROXY_PASSWD_ONLY,
addr,
&string_of_size(1),
&string_of_size(1),
None,
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(err.to_string(), "password authentication failed");
let err = Socks5Stream::connect_with_password(
SOCKS_PROXY_PASSWD_ONLY,
addr,
&string_of_size(255),
&string_of_size(255),
None,
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::PermissionDenied);
assert_eq!(err.to_string(), "password authentication failed");
let err = Socks5Stream::connect_with_password(
SOCKS_PROXY_PASSWD_ONLY,
addr,
&string_of_size(0),
&string_of_size(255),
None,
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert_eq!(err.to_string(), "invalid username");
let err = Socks5Stream::connect_with_password(
SOCKS_PROXY_PASSWD_ONLY,
addr,
&string_of_size(256),
&string_of_size(255),
None,
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert_eq!(err.to_string(), "invalid username");
let err = Socks5Stream::connect_with_password(
SOCKS_PROXY_PASSWD_ONLY,
addr,
&string_of_size(255),
&string_of_size(0),
None,
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert_eq!(err.to_string(), "invalid password");
let err = Socks5Stream::connect_with_password(
SOCKS_PROXY_PASSWD_ONLY,
addr,
&string_of_size(255),
&string_of_size(256),
None,
)
.unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
assert_eq!(err.to_string(), "invalid password");
}
fn string_of_size(size: usize) -> String {
(0..size).map(|_| 'x').collect()
}
}

133
src/socks/writev.rs Normal file
View File

@ -0,0 +1,133 @@
use std::io;
use std::net::UdpSocket;
pub trait WritevExt {
fn writev(&self, bufs: [&[u8]; 2]) -> io::Result<usize>;
fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result<usize>;
}
#[cfg(unix)]
mod imp {
use libc;
use std::os::unix::io::AsRawFd;
use super::*;
impl WritevExt for UdpSocket {
fn writev(&self, bufs: [&[u8]; 2]) -> io::Result<usize> {
unsafe {
let iovecs = [
libc::iovec {
iov_base: bufs[0].as_ptr() as *const _ as *mut _,
iov_len: bufs[0].len(),
},
libc::iovec {
iov_base: bufs[1].as_ptr() as *const _ as *mut _,
iov_len: bufs[1].len(),
},
];
let r = libc::writev(self.as_raw_fd(), iovecs.as_ptr(), 2);
if r < 0 {
Err(io::Error::last_os_error())
} else {
Ok(r as usize)
}
}
}
fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result<usize> {
unsafe {
let mut iovecs = [
libc::iovec {
iov_base: bufs[0].as_mut_ptr() as *mut _,
iov_len: bufs[0].len(),
},
libc::iovec {
iov_base: bufs[1].as_mut_ptr() as *mut _,
iov_len: bufs[1].len(),
},
];
let r = libc::readv(self.as_raw_fd(), iovecs.as_mut_ptr(), 2);
if r < 0 {
Err(io::Error::last_os_error())
} else {
Ok(r as usize)
}
}
}
}
}
#[cfg(windows)]
mod imp {
use std::os::windows::io::AsRawSocket;
use std::ptr;
use winapi::shared::minwindef;
use winapi::shared::ws2def;
use winapi::um::winsock2;
use super::*;
impl WritevExt for UdpSocket {
fn writev(&self, bufs: [&[u8]; 2]) -> io::Result<usize> {
unsafe {
let mut wsabufs = [
ws2def::WSABUF {
len: bufs[0].len() as winsock2::u_long,
buf: bufs[0].as_ptr() as *const _ as *mut _,
},
ws2def::WSABUF {
len: bufs[1].len() as winsock2::u_long,
buf: bufs[1].as_ptr() as *const _ as *mut _,
},
];
let mut sent = 0;
let r = winsock2::WSASend(
self.as_raw_socket() as usize,
wsabufs.as_mut_ptr(),
bufs.len() as minwindef::DWORD,
&mut sent,
0,
ptr::null_mut(),
None,
);
if r == 0 {
Ok(sent as usize)
} else {
Err(io::Error::last_os_error())
}
}
}
fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result<usize> {
unsafe {
let mut wsabufs = [
ws2def::WSABUF {
len: bufs[0].len() as winsock2::u_long,
buf: bufs[0].as_mut_ptr() as *mut _,
},
ws2def::WSABUF {
len: bufs[1].len() as winsock2::u_long,
buf: bufs[1].as_mut_ptr() as *mut _,
},
];
let mut recved = 0;
let mut flags = 0;
let r = winsock2::WSARecv(
self.as_raw_socket() as usize,
wsabufs.as_mut_ptr(),
bufs.len() as minwindef::DWORD,
&mut recved,
&mut flags,
ptr::null_mut(),
None,
);
if r == 0 {
Ok(recved as usize)
} else {
Err(io::Error::last_os_error())
}
}
}
}
}

View File

@ -1,3 +1,4 @@
use log::error;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
@ -6,17 +7,35 @@ pub struct ClonableStream<T: Read + Write>(Arc<Mutex<T>>);
impl<T: Read + Write> Read for ClonableStream<T> { impl<T: Read + Write> Read for ClonableStream<T> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.lock().unwrap().read(buf) self.0
.lock()
.map_err(|_| {
error!("Unable to acquire lock on ClonableStream read operation");
io::Error::from(io::ErrorKind::BrokenPipe)
})?
.read(buf)
} }
} }
impl<T: Read + Write> Write for ClonableStream<T> { impl<T: Read + Write> Write for ClonableStream<T> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.lock().unwrap().write(buf) self.0
.lock()
.map_err(|_| {
error!("Unable to acquire lock on ClonableStream write operation");
io::Error::from(io::ErrorKind::BrokenPipe)
})?
.write(buf)
} }
fn flush(&mut self) -> io::Result<()> { fn flush(&mut self) -> io::Result<()> {
self.0.lock().unwrap().flush() self.0
.lock()
.map_err(|_| {
error!("Unable to acquire lock on ClonableStream flush operation");
io::Error::from(io::ErrorKind::BrokenPipe)
})?
.flush()
} }
} }

View File

@ -3,18 +3,22 @@
//! This module contains definitions of all the complex data structures that are returned by calls //! This module contains definitions of all the complex data structures that are returned by calls
use std::convert::TryFrom; use std::convert::TryFrom;
use std::fmt::{self, Display, Formatter};
use std::ops::Deref; use std::ops::Deref;
use std::sync::Arc;
use bitcoin::blockdata::block; use bitcoin::blockdata::block;
use bitcoin::consensus::encode::deserialize; use bitcoin::consensus::encode::deserialize;
use bitcoin::hashes::hex::FromHex;
use bitcoin::hashes::{sha256, Hash}; use bitcoin::hashes::{sha256, Hash};
use bitcoin::hex::{DisplayHex, FromHex};
use bitcoin::{Script, Txid}; use bitcoin::{Script, Txid};
use serde::{de, Deserialize, Serialize}; use serde::{de, Deserialize, Serialize};
static JSONRPC_2_0: &str = "2.0"; static JSONRPC_2_0: &str = "2.0";
pub(crate) type Call = (String, Vec<Param>);
#[derive(Serialize, Clone)] #[derive(Serialize, Clone)]
#[serde(untagged)] #[serde(untagged)]
/// A single parameter of a [`Request`](struct.Request.html) /// A single parameter of a [`Request`](struct.Request.html)
@ -65,8 +69,8 @@ impl<'a> Request<'a> {
} }
#[doc(hidden)] #[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub struct Hex32Bytes(#[serde(deserialize_with = "from_hex")] [u8; 32]); pub struct Hex32Bytes(#[serde(deserialize_with = "from_hex", serialize_with = "to_hex")] [u8; 32]);
impl Deref for Hex32Bytes { impl Deref for Hex32Bytes {
type Target = [u8; 32]; type Target = [u8; 32];
@ -82,6 +86,12 @@ impl From<[u8; 32]> for Hex32Bytes {
} }
} }
impl Hex32Bytes {
pub(crate) fn to_hex(self) -> String {
self.0.to_lower_hex_string()
}
}
/// Format used by the Electrum server to identify an address. The reverse sha256 hash of the /// Format used by the Electrum server to identify an address. The reverse sha256 hash of the
/// scriptPubKey. Documented [here](https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes). /// scriptPubKey. Documented [here](https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes).
pub type ScriptHash = Hex32Bytes; pub type ScriptHash = Hex32Bytes;
@ -98,7 +108,7 @@ pub trait ToElectrumScriptHash {
impl ToElectrumScriptHash for Script { impl ToElectrumScriptHash for Script {
fn to_electrum_scripthash(&self) -> ScriptHash { fn to_electrum_scripthash(&self) -> ScriptHash {
let mut result = sha256::Hash::hash(self.as_bytes()).into_inner(); let mut result = sha256::Hash::hash(self.as_bytes()).to_byte_array();
result.reverse(); result.reverse();
result.into() result.into()
@ -114,6 +124,13 @@ where
T::from_hex(&s).map_err(de::Error::custom) T::from_hex(&s).map_err(de::Error::custom)
} }
fn to_hex<S>(bytes: &[u8], serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
serializer.serialize_str(&bytes.to_lower_hex_string())
}
fn from_hex_array<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error> fn from_hex_array<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
where where
T: FromHex + std::fmt::Debug, T: FromHex + std::fmt::Debug,
@ -134,7 +151,7 @@ where
Ok(answer) Ok(answer)
} }
fn from_hex_header<'de, D>(deserializer: D) -> Result<block::BlockHeader, D::Error> fn from_hex_header<'de, D>(deserializer: D) -> Result<block::Header, D::Error>
where where
D: de::Deserializer<'de>, D: de::Deserializer<'de>,
{ {
@ -143,7 +160,7 @@ where
} }
/// Response to a [`script_get_history`](../client/struct.Client.html#method.script_get_history) request. /// 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 { pub struct GetHistoryRes {
/// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of /// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of
/// its inputs are unconfirmed too. /// its inputs are unconfirmed too.
@ -155,7 +172,7 @@ pub struct GetHistoryRes {
} }
/// Response to a [`script_list_unspent`](../client/struct.Client.html#method.script_list_unspent) request. /// 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 { pub struct ListUnspentRes {
/// Confirmation height of the transaction that created this output. /// Confirmation height of the transaction that created this output.
pub height: usize, pub height: usize,
@ -168,7 +185,7 @@ pub struct ListUnspentRes {
} }
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request. /// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
#[derive(Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct ServerFeaturesRes { pub struct ServerFeaturesRes {
/// Server version reported. /// Server version reported.
pub server_version: String, pub server_version: String,
@ -186,7 +203,7 @@ pub struct ServerFeaturesRes {
} }
/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request. /// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
#[derive(Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct GetHeadersRes { pub struct GetHeadersRes {
/// Maximum number of headers returned in a single response. /// Maximum number of headers returned in a single response.
pub max: usize, pub max: usize,
@ -197,20 +214,22 @@ pub struct GetHeadersRes {
pub raw_headers: Vec<u8>, pub raw_headers: Vec<u8>,
/// Array of block headers. /// Array of block headers.
#[serde(skip)] #[serde(skip)]
pub headers: Vec<block::BlockHeader>, pub headers: Vec<block::Header>,
} }
/// Response to a [`script_get_balance`](../client/struct.Client.html#method.script_get_balance) request. /// 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 { pub struct GetBalanceRes {
/// Confirmed balance in Satoshis for the address. /// Confirmed balance in Satoshis for the address.
pub confirmed: u64, pub confirmed: u64,
/// Unconfirmed balance in Satoshis for the address. /// Unconfirmed balance in Satoshis for the address.
pub unconfirmed: u64, ///
/// Some servers (e.g. `electrs`) return this as a negative value.
pub unconfirmed: i64,
} }
/// Response to a [`transaction_get_merkle`](../client/struct.Client.html#method.transaction_get_merkle) request. /// 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 { pub struct GetMerkleRes {
/// Height of the block that confirmed the transaction /// Height of the block that confirmed the transaction
pub block_height: usize, pub block_height: usize,
@ -221,18 +240,29 @@ pub struct GetMerkleRes {
pub merkle: Vec<[u8; 32]>, 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 /// Notification of a new block header
#[derive(Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct HeaderNotification { pub struct HeaderNotification {
/// New block height. /// New block height.
pub height: usize, pub height: usize,
/// Newly added header. /// Newly added header.
#[serde(rename = "hex", deserialize_with = "from_hex_header")] #[serde(rename = "hex", deserialize_with = "from_hex_header")]
pub header: block::BlockHeader, pub header: block::Header,
} }
/// Notification of a new block header with the header encoded as raw bytes /// Notification of a new block header with the header encoded as raw bytes
#[derive(Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct RawHeaderNotification { pub struct RawHeaderNotification {
/// New block height. /// New block height.
pub height: usize, pub height: usize,
@ -253,7 +283,7 @@ impl TryFrom<RawHeaderNotification> for HeaderNotification {
} }
/// Notification of the new status of a script /// Notification of the new status of a script
#[derive(Debug, Deserialize)] #[derive(Clone, Debug, Deserialize)]
pub struct ScriptNotification { pub struct ScriptNotification {
/// Address that generated this notification. /// Address that generated this notification.
pub scripthash: ScriptHash, pub scripthash: ScriptHash,
@ -268,8 +298,8 @@ pub enum Error {
IOError(std::io::Error), IOError(std::io::Error),
/// Wraps `serde_json::error::Error` /// Wraps `serde_json::error::Error`
JSON(serde_json::error::Error), JSON(serde_json::error::Error),
/// Wraps `bitcoin::hashes::hex::Error` /// Wraps `bitcoin::hex::HexToBytesError`
Hex(bitcoin::hashes::hex::Error), Hex(bitcoin::hex::HexToBytesError),
/// Error returned by the Electrum server /// Error returned by the Electrum server
Protocol(serde_json::Value), Protocol(serde_json::Value),
/// Error during the deserialization of a Bitcoin data structure /// Error during the deserialization of a Bitcoin data structure
@ -286,12 +316,19 @@ pub enum Error {
InvalidDNSNameError(String), InvalidDNSNameError(String),
/// Missing domain while it was explicitly asked to validate it /// Missing domain while it was explicitly asked to validate it
MissingDomain, MissingDomain,
/// SSL over a socks5 proxy is currently not supported /// Made one or multiple attempts, always in Error
SSLOverSocks5, AllAttemptsErrored(Vec<Error>),
/// There was an io error reading the socket, to be shared between threads
SharedIOError(Arc<std::io::Error>),
/// Couldn't take a lock on the reader mutex. This means that there's already another reader /// Couldn't take a lock on the reader mutex. This means that there's already another reader
/// thread running /// thread running
CouldntLockReader, CouldntLockReader,
/// Broken IPC communication channel: the other thread probably has exited
Mpsc,
#[cfg(any(feature = "use-rustls", feature = "use-rustls-ring"))]
/// Could not create a rustls client connection
CouldNotCreateConnection(rustls::Error),
#[cfg(feature = "use-openssl")] #[cfg(feature = "use-openssl")]
/// Invalid OpenSSL method used /// Invalid OpenSSL method used
@ -301,11 +338,55 @@ pub enum Error {
SslHandshakeError(openssl::ssl::HandshakeError<std::net::TcpStream>), SslHandshakeError(openssl::ssl::HandshakeError<std::net::TcpStream>),
} }
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Error::IOError(e) => Display::fmt(e, f),
Error::JSON(e) => Display::fmt(e, f),
Error::Hex(e) => Display::fmt(e, f),
Error::Bitcoin(e) => Display::fmt(e, f),
Error::SharedIOError(e) => Display::fmt(e, f),
#[cfg(feature = "use-openssl")]
Error::SslHandshakeError(e) => Display::fmt(e, f),
#[cfg(feature = "use-openssl")]
Error::InvalidSslMethod(e) => Display::fmt(e, f),
#[cfg(any(
feature = "use-rustls",
feature = "use-rustls-ring",
))]
Error::CouldNotCreateConnection(e) => Display::fmt(e, f),
Error::Message(e) => f.write_str(e),
Error::InvalidDNSNameError(domain) => write!(f, "Invalid domain name {} not matching SSL certificate", domain),
Error::AllAttemptsErrored(errors) => {
f.write_str("Made one or multiple attempts, all errored:\n")?;
for err in errors {
writeln!(f, "\t- {}", err)?;
}
Ok(())
}
Error::Protocol(e) => write!(f, "Electrum server error: {}", e.clone().take()),
Error::InvalidResponse(e) => write!(f, "Error during the deserialization of a response from the server: {}", e.clone().take()),
// TODO: Print out addresses once `ScriptHash` will implement `Display`
Error::AlreadySubscribed(_) => write!(f, "Already subscribed to the notifications of an address"),
Error::NotSubscribed(_) => write!(f, "Not subscribed to the notifications of an address"),
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"),
}
}
}
impl std::error::Error for Error {}
macro_rules! impl_error { macro_rules! impl_error {
( $from:ty, $to:ident ) => { ( $from:ty, $to:ident ) => {
impl std::convert::From<$from> for Error { impl std::convert::From<$from> for Error {
fn from(err: $from) -> Self { fn from(err: $from) -> Self {
Error::$to(err) Error::$to(err.into())
} }
} }
}; };
@ -313,5 +394,36 @@ macro_rules! impl_error {
impl_error!(std::io::Error, IOError); impl_error!(std::io::Error, IOError);
impl_error!(serde_json::Error, JSON); 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_error!(bitcoin::consensus::encode::Error, Bitcoin);
impl<T> From<std::sync::PoisonError<T>> for Error {
fn from(_: std::sync::PoisonError<T>) -> Self {
Error::IOError(std::io::Error::from(std::io::ErrorKind::BrokenPipe))
}
}
impl<T> From<std::sync::mpsc::SendError<T>> for Error {
fn from(_: std::sync::mpsc::SendError<T>) -> Self {
Error::Mpsc
}
}
impl From<std::sync::mpsc::RecvError> for Error {
fn from(_: std::sync::mpsc::RecvError) -> Self {
Error::Mpsc
}
}
#[cfg(test)]
mod tests {
use crate::ScriptStatus;
#[test]
fn script_status_roundtrip() {
let script_status: ScriptStatus = [1u8; 32].into();
let script_status_json = serde_json::to_string(&script_status).unwrap();
let script_status_back = serde_json::from_str(&script_status_json).unwrap();
assert_eq!(script_status, script_status_back);
}
}

43
src/utils.rs Normal file
View 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()
}