├── clippy.toml ├── .gitignore ├── examples ├── ssl.rs ├── plaintext.rs └── tor.rs ├── README.md ├── LICENSE ├── src ├── utils.rs ├── stream.rs ├── lib.rs ├── config.rs ├── socks │ ├── writev.rs │ ├── mod.rs │ ├── v4.rs │ └── v5.rs ├── batch.rs ├── types.rs ├── client.rs ├── api.rs └── raw_client.rs ├── Cargo.toml ├── .github └── workflows │ └── cont_integration.yml └── CHANGELOG.md /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv="1.75.0" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | 4 | *.swp 5 | -------------------------------------------------------------------------------- /examples/ssl.rs: -------------------------------------------------------------------------------- 1 | extern crate electrum_client; 2 | 3 | use electrum_client::{Client, ElectrumApi}; 4 | 5 | fn main() { 6 | let client = Client::new("ssl://electrum.blockstream.info:50002").unwrap(); 7 | let res = client.server_features(); 8 | println!("{:#?}", res); 9 | } 10 | -------------------------------------------------------------------------------- /examples/plaintext.rs: -------------------------------------------------------------------------------- 1 | extern crate electrum_client; 2 | 3 | use electrum_client::{Client, ElectrumApi}; 4 | 5 | fn main() { 6 | let client = Client::new("tcp://electrum.blockstream.info:50001").unwrap(); 7 | let res = client.server_features(); 8 | println!("{:#?}", res); 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rust-electrum-client 2 | [![Build Status]][GitHub Workflow] [![Latest Version]][crates.io] [![MSRV Badge]][Rust Blog] 3 | 4 | [Build Status]: https://github.com/bitcoindevkit/rust-electrum-client/actions/workflows/cont_integration.yml/badge.svg 5 | [GitHub Workflow]: https://github.com/bitcoindevkit/rust-electrum-client/actions?query=workflow%3ACI 6 | [Latest Version]: https://img.shields.io/crates/v/electrum-client.svg 7 | [crates.io]: https://crates.io/crates/electrum-client 8 | [MSRV Badge]: https://img.shields.io/badge/rustc-1.75.0%2B-lightgrey.svg 9 | [Rust Blog]: https://blog.rust-lang.org/2023/12/28/Rust-1.75.0.html 10 | 11 | Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers. 12 | 13 | ## Minimum Supported Rust Version (MSRV) 14 | 15 | This library should compile with any combination of features with Rust 1.75.0. 16 | -------------------------------------------------------------------------------- /examples/tor.rs: -------------------------------------------------------------------------------- 1 | extern crate electrum_client; 2 | 3 | use electrum_client::{Client, ConfigBuilder, ElectrumApi, Socks5Config}; 4 | 5 | fn main() { 6 | // NOTE: This assumes Tor is running localy, with an unauthenticated Socks5 listening at 7 | // localhost:9050 8 | let proxy = Socks5Config::new("127.0.0.1:9050"); 9 | let config = ConfigBuilder::new().socks5(Some(proxy)).build(); 10 | 11 | let client = Client::from_config("tcp://explorernuoc63nb.onion:110", config.clone()).unwrap(); 12 | let res = client.server_features(); 13 | println!("{:#?}", res); 14 | 15 | // works both with onion v2/v3 (if your Tor supports them) 16 | let client = Client::from_config( 17 | "tcp://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion:110", 18 | config, 19 | ) 20 | .unwrap(); 21 | let res = client.server_features(); 22 | println!("{:#?}", res); 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Magical Bitcoin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities helping to handle Electrum-related data. 2 | 3 | use crate::types::GetMerkleRes; 4 | use bitcoin::hash_types::TxMerkleNode; 5 | use bitcoin::hashes::sha256d::Hash as Sha256d; 6 | use bitcoin::hashes::{Hash, HashEngine}; 7 | use bitcoin::Txid; 8 | 9 | /// Verifies a Merkle inclusion proof as retrieved via [`transaction_get_merkle`] for a transaction with the 10 | /// given `txid` and `merkle_root` as included in the [`BlockHeader`]. 11 | /// 12 | /// Returns `true` if the transaction is included in the corresponding block, and `false` 13 | /// otherwise. 14 | /// 15 | /// [`transaction_get_merkle`]: crate::ElectrumApi::transaction_get_merkle 16 | /// [`BlockHeader`]: bitcoin::BlockHeader 17 | pub fn validate_merkle_proof( 18 | txid: &Txid, 19 | merkle_root: &TxMerkleNode, 20 | merkle_res: &GetMerkleRes, 21 | ) -> bool { 22 | let mut index = merkle_res.pos; 23 | let mut cur = txid.to_raw_hash(); 24 | for mut bytes in merkle_res.merkle.iter().cloned() { 25 | bytes.reverse(); 26 | let next_hash = Sha256d::from_byte_array(bytes); 27 | 28 | cur = Sha256d::from_engine({ 29 | let mut engine = Sha256d::engine(); 30 | if index % 2 == 0 { 31 | engine.input(cur.as_ref()); 32 | engine.input(next_hash.as_ref()); 33 | } else { 34 | engine.input(next_hash.as_ref()); 35 | engine.input(cur.as_ref()); 36 | }; 37 | engine 38 | }); 39 | index /= 2; 40 | } 41 | 42 | cur == merkle_root.to_raw_hash() 43 | } 44 | -------------------------------------------------------------------------------- /src/stream.rs: -------------------------------------------------------------------------------- 1 | use log::error; 2 | use std::io::{self, Read, Write}; 3 | use std::sync::{Arc, Mutex}; 4 | 5 | #[derive(Debug)] 6 | pub struct ClonableStream(Arc>); 7 | 8 | impl Read for ClonableStream { 9 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 10 | self.0 11 | .lock() 12 | .map_err(|_| { 13 | error!("Unable to acquire lock on ClonableStream read operation"); 14 | io::Error::from(io::ErrorKind::BrokenPipe) 15 | })? 16 | .read(buf) 17 | } 18 | } 19 | 20 | impl Write for ClonableStream { 21 | fn write(&mut self, buf: &[u8]) -> io::Result { 22 | self.0 23 | .lock() 24 | .map_err(|_| { 25 | error!("Unable to acquire lock on ClonableStream write operation"); 26 | io::Error::from(io::ErrorKind::BrokenPipe) 27 | })? 28 | .write(buf) 29 | } 30 | 31 | fn flush(&mut self) -> io::Result<()> { 32 | self.0 33 | .lock() 34 | .map_err(|_| { 35 | error!("Unable to acquire lock on ClonableStream flush operation"); 36 | io::Error::from(io::ErrorKind::BrokenPipe) 37 | })? 38 | .flush() 39 | } 40 | } 41 | 42 | impl From for ClonableStream { 43 | fn from(stream: T) -> Self { 44 | Self(Arc::new(Mutex::new(stream))) 45 | } 46 | } 47 | 48 | impl Clone for ClonableStream { 49 | fn clone(&self) -> Self { 50 | ClonableStream(Arc::clone(&self.0)) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "electrum-client" 3 | version = "0.24.1" 4 | authors = ["Alekos Filini "] 5 | license = "MIT" 6 | homepage = "https://github.com/bitcoindevkit/rust-electrum-client" 7 | repository = "https://github.com/bitcoindevkit/rust-electrum-client" 8 | documentation = "https://docs.rs/electrum-client/" 9 | description = "Bitcoin Electrum client library. Supports plaintext, TLS and Onion servers." 10 | keywords = ["bitcoin", "electrum"] 11 | readme = "README.md" 12 | rust-version = "1.75.0" 13 | edition = "2021" 14 | 15 | # loosely based on https://github.com/evgeniy-scherbina/rust-electrumx-client 16 | 17 | [lib] 18 | name = "electrum_client" 19 | path = "src/lib.rs" 20 | 21 | [dependencies] 22 | log = "^0.4" 23 | bitcoin = { version = "0.32", features = ["serde"] } 24 | serde = { version = "^1.0", features = ["derive"] } 25 | serde_json = { version = "^1.0" } 26 | 27 | # Optional dependencies 28 | openssl = { version = "0.10", optional = true } 29 | rustls = { version = "0.23.21", optional = true, default-features = false } 30 | webpki-roots = { version = "0.25", optional = true } 31 | 32 | byteorder = { version = "1.0", optional = true } 33 | 34 | [target.'cfg(unix)'.dependencies] 35 | libc = { version = "0.2", optional = true } 36 | 37 | [target.'cfg(windows)'.dependencies] 38 | winapi = { version="0.3.9", features=["winsock2"], optional = true } 39 | 40 | [features] 41 | default = ["proxy", "use-rustls"] 42 | minimal = [] 43 | debug-calls = [] 44 | proxy = ["byteorder", "winapi", "libc"] 45 | use-rustls = ["webpki-roots", "rustls/default"] 46 | use-rustls-ring = ["webpki-roots", "rustls/ring", "rustls/logging", "rustls/std", "rustls/tls12"] 47 | use-openssl = ["openssl"] 48 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | 3 | //! This library provides an extendable Bitcoin-Electrum client that supports batch calls, 4 | //! notifications and multiple transport methods. 5 | //! 6 | //! By default this library is compiled with support for SSL servers using [`rustls`](https://docs.rs/rustls) and support for 7 | //! plaintext connections over a socks proxy, useful for Onion servers. Using different features, 8 | //! the SSL implementation can be removed or replaced with [`openssl`](https://docs.rs/openssl). 9 | //! 10 | //! A `minimal` configuration is also provided, which only includes the plaintext TCP client. 11 | //! 12 | //! # Example 13 | //! 14 | //! ```no_run 15 | //! use electrum_client::{Client, ElectrumApi}; 16 | //! 17 | //! let mut client = Client::new("tcp://electrum.blockstream.info:50001")?; 18 | //! let response = client.server_features()?; 19 | //! # Ok::<(), electrum_client::Error>(()) 20 | //! ``` 21 | 22 | pub extern crate bitcoin; 23 | extern crate core; 24 | extern crate log; 25 | #[cfg(feature = "use-openssl")] 26 | extern crate openssl; 27 | #[cfg(all( 28 | any( 29 | feature = "default", 30 | feature = "use-rustls", 31 | feature = "use-rustls-ring" 32 | ), 33 | not(feature = "use-openssl") 34 | ))] 35 | extern crate rustls; 36 | extern crate serde; 37 | extern crate serde_json; 38 | 39 | #[cfg(any( 40 | feature = "default", 41 | feature = "use-rustls", 42 | feature = "use-rustls-ring" 43 | ))] 44 | extern crate webpki_roots; 45 | 46 | #[cfg(any(feature = "default", feature = "proxy"))] 47 | extern crate byteorder; 48 | 49 | #[cfg(all(unix, any(feature = "default", feature = "proxy")))] 50 | extern crate libc; 51 | #[cfg(all(windows, any(feature = "default", feature = "proxy")))] 52 | extern crate winapi; 53 | 54 | #[cfg(any(feature = "default", feature = "proxy"))] 55 | pub mod socks; 56 | 57 | mod api; 58 | mod batch; 59 | 60 | #[cfg(any( 61 | all(feature = "proxy", feature = "use-openssl"), 62 | all(feature = "proxy", feature = "use-rustls"), 63 | all(feature = "proxy", feature = "use-rustls-ring") 64 | ))] 65 | pub mod client; 66 | 67 | mod config; 68 | 69 | pub mod raw_client; 70 | mod stream; 71 | mod types; 72 | pub mod utils; 73 | 74 | pub use api::ElectrumApi; 75 | pub use batch::Batch; 76 | #[cfg(any( 77 | all(feature = "proxy", feature = "use-openssl"), 78 | all(feature = "proxy", feature = "use-rustls"), 79 | all(feature = "proxy", feature = "use-rustls-ring") 80 | ))] 81 | pub use client::*; 82 | pub use config::{Config, ConfigBuilder, Socks5Config}; 83 | pub use types::*; 84 | -------------------------------------------------------------------------------- /.github/workflows/cont_integration.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | test: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | env: 10 | TEST_ELECTRUM_SERVER: electrum.blockstream.info:50001 11 | strategy: 12 | matrix: 13 | rust: 14 | - stable # STABLE 15 | - 1.75.0 # MSRV 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | - name: Cache 20 | uses: actions/cache@v4 21 | with: 22 | path: | 23 | ~/.cargo/registry 24 | ~/.cargo/git 25 | target 26 | key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} 27 | - name: Install rust 28 | uses: dtolnay/rust-toolchain@stable 29 | with: 30 | toolchain: ${{ matrix.rust }} 31 | - name: Test 32 | run: cargo test --verbose --all-features 33 | - name: Setup iptables for the timeout test 34 | run: sudo ip6tables -I INPUT 1 -p tcp -d ::1 --dport 60000 -j DROP 35 | - name: Timeout test 36 | run: cargo test -- --ignored test_local_timeout 37 | - run: cargo check --verbose --features=use-openssl 38 | - run: cargo check --verbose --no-default-features --features=proxy 39 | - run: cargo check --verbose --no-default-features --features=minimal 40 | - run: cargo check --verbose --no-default-features --features=minimal,debug-calls 41 | - run: cargo check --verbose --no-default-features --features=proxy,use-openssl 42 | - run: cargo check --verbose --no-default-features --features=proxy,use-rustls 43 | - run: cargo check --verbose --no-default-features --features=proxy,use-rustls-ring 44 | - run: cargo check --verbose --no-default-features --features=proxy,use-rustls,use-rustls-ring 45 | 46 | fmt: 47 | name: Rust fmt 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | - name: Install Rust toolchain 53 | uses: dtolnay/rust-toolchain@stable 54 | with: 55 | toolchain: stable 56 | components: rustfmt 57 | - name: Check fmt 58 | run: cargo fmt --all -- --config format_code_in_doc_comments=true --check 59 | 60 | clippy_check: 61 | name: Rust clippy 62 | runs-on: ubuntu-latest 63 | steps: 64 | - uses: actions/checkout@v4 65 | - uses: dtolnay/rust-toolchain@v1 66 | with: 67 | toolchain: 1.90.0 68 | components: clippy 69 | - name: Rust Cache 70 | uses: Swatinem/rust-cache@v2.2.1 71 | - run: cargo clippy --all-features --all-targets -- -D warnings 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 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*"`. 4 | 5 | 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. 6 | 7 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 8 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 9 | 10 | ## [Unreleased] 11 | 12 | ## [0.24.1] 13 | - Default to `ring` if multiple `rustls` features are set #183 14 | 15 | ## [0.24.0] 16 | - Use default `CryptoProvider` if available, otherwise install `rustls`'s `CryptoProvider` based on features #171 17 | - Add a new batch method for `blockchain.transaction.get_merkle` #170 18 | 19 | ## [0.23.1] 20 | - Fix batch request to Electrum servers out of order responses #160 21 | - Allow types that references to `ElectrumApi` to also implement it #163 22 | 23 | ## [0.23.0] 24 | 25 | - Raise MSRV to `1.75` and bump `rustls` to `0.23.21` #159 26 | - Enforce min `rustls` version 0.23.19 to support MSRV with fix for RUSTSEC-2024-0399 #158 27 | 28 | ## [0.22.0] 29 | 30 | - Updates the NoCertificateVerification implementation for the rustls::client::danger::ServerCertVerifier to use the rustls::SignatureScheme from CryptoProvider in use #150 31 | - Add `id_from_pos` support #155 32 | 33 | ## [0.21.0] 34 | 35 | - Add use-rustls-ring feature #135 36 | - refactor: make validate_merkle_proof more efficient #134 37 | - chore: set rust edition to 2021, fix clippy, add ci fmt and clippy checks #139 38 | 39 | ## [0.20.0] 40 | 41 | - Upgrade rustls to 0.23 #132 42 | - chore(deps): upgrade rust-bitcoin to 0.32.0 #133 43 | - ci: add test with MSRV 1.63.0 #128 44 | 45 | ## [0.19.0] 46 | 47 | - Add Batch::raw and improve docs #94 48 | - Remove webpki and bump webpki-roots to v0.25 #117 49 | - Upgrade rust-bitcoin to v0.31.0 #121 50 | - Add utility to validate GetMerkleRes #122 51 | - Enforce timeout on initial socks5 proxy connection #125 52 | 53 | ## [0.18.0] 54 | 55 | - Revert "errors if expecting headers notification but not subscribed" #115 56 | 57 | [0.18.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.17.0...0.18.0 58 | [0.19.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.18.0...0.19.0 59 | [0.20.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.19.0...0.20.0 60 | [0.21.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.20.0...0.21.0 61 | [0.22.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.21.0...0.22.0 62 | [0.23.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.22.0...0.23.0 63 | [0.23.1]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.23.0...0.23.1 64 | [0.24.0]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.23.1...0.24.0 65 | [0.24.1]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.24.0...0.24.1 66 | [Unreleased]: https://github.com/bitcoindevkit/rust-electrum-client/compare/0.24.1...HEAD 67 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | /// Configuration for an electrum client 4 | /// 5 | /// Refer to [`Client::from_config`] and [`ClientType::from_config`]. 6 | /// 7 | /// [`Client::from_config`]: crate::Client::from_config 8 | /// [`ClientType::from_config`]: crate::ClientType::from_config 9 | #[derive(Debug, Clone)] 10 | pub struct Config { 11 | /// Proxy socks5 configuration, default None 12 | socks5: Option, 13 | /// timeout in seconds, default None (depends on TcpStream default) 14 | timeout: Option, 15 | /// number of retry if any error, default 1 16 | retry: u8, 17 | /// when ssl, validate the domain, default true 18 | validate_domain: bool, 19 | } 20 | 21 | /// Configuration for Socks5 22 | #[derive(Debug, Clone)] 23 | pub struct Socks5Config { 24 | /// The address of the socks5 service 25 | pub addr: String, 26 | /// Optional credential for the service 27 | pub credentials: Option, 28 | } 29 | 30 | /// Credential for the proxy 31 | #[derive(Debug, Clone)] 32 | pub struct Socks5Credential { 33 | pub username: String, 34 | pub password: String, 35 | } 36 | 37 | /// [Config] Builder 38 | pub struct ConfigBuilder { 39 | config: Config, 40 | } 41 | 42 | impl ConfigBuilder { 43 | /// Create a builder with a default config, equivalent to [ConfigBuilder::default()] 44 | pub fn new() -> Self { 45 | ConfigBuilder { 46 | config: Config::default(), 47 | } 48 | } 49 | 50 | /// Set the socks5 config if Some, it accept an `Option` because it's easier for the caller to use 51 | /// in a method chain 52 | pub fn socks5(mut self, socks5_config: Option) -> Self { 53 | self.config.socks5 = socks5_config; 54 | self 55 | } 56 | 57 | /// Sets the timeout 58 | pub fn timeout(mut self, timeout: Option) -> Self { 59 | self.config.timeout = timeout; 60 | self 61 | } 62 | 63 | /// Sets the retry attempts number 64 | pub fn retry(mut self, retry: u8) -> Self { 65 | self.config.retry = retry; 66 | self 67 | } 68 | 69 | /// Sets if the domain has to be validated 70 | pub fn validate_domain(mut self, validate_domain: bool) -> Self { 71 | self.config.validate_domain = validate_domain; 72 | self 73 | } 74 | 75 | /// Return the config and consume the builder 76 | pub fn build(self) -> Config { 77 | self.config 78 | } 79 | } 80 | 81 | impl Default for ConfigBuilder { 82 | fn default() -> Self { 83 | Self::new() 84 | } 85 | } 86 | 87 | impl Socks5Config { 88 | /// Socks5Config constructor without credentials 89 | pub fn new(addr: impl ToString) -> Self { 90 | let addr = addr.to_string().replacen("socks5://", "", 1); 91 | Socks5Config { 92 | addr, 93 | credentials: None, 94 | } 95 | } 96 | 97 | /// Socks5Config constructor if we have credentials 98 | pub fn with_credentials(addr: impl ToString, username: String, password: String) -> Self { 99 | let mut config = Socks5Config::new(addr); 100 | config.credentials = Some(Socks5Credential { username, password }); 101 | config 102 | } 103 | } 104 | 105 | impl Config { 106 | /// Get the configuration for `socks5` 107 | /// 108 | /// Set this with [`ConfigBuilder::socks5`] 109 | pub fn socks5(&self) -> &Option { 110 | &self.socks5 111 | } 112 | 113 | /// Get the configuration for `retry` 114 | /// 115 | /// Set this with [`ConfigBuilder::retry`] 116 | pub fn retry(&self) -> u8 { 117 | self.retry 118 | } 119 | 120 | /// Get the configuration for `timeout` 121 | /// 122 | /// Set this with [`ConfigBuilder::timeout`] 123 | pub fn timeout(&self) -> Option { 124 | self.timeout 125 | } 126 | 127 | /// Get the configuration for `validate_domain` 128 | /// 129 | /// Set this with [`ConfigBuilder::validate_domain`] 130 | pub fn validate_domain(&self) -> bool { 131 | self.validate_domain 132 | } 133 | 134 | /// Convenience method for calling [`ConfigBuilder::new`] 135 | pub fn builder() -> ConfigBuilder { 136 | ConfigBuilder::new() 137 | } 138 | } 139 | 140 | impl Default for Config { 141 | fn default() -> Self { 142 | Config { 143 | socks5: None, 144 | timeout: None, 145 | retry: 1, 146 | validate_domain: true, 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/socks/writev.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::net::UdpSocket; 3 | 4 | pub trait WritevExt { 5 | fn writev(&self, bufs: [&[u8]; 2]) -> io::Result; 6 | fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result; 7 | } 8 | 9 | #[cfg(unix)] 10 | mod imp { 11 | use libc; 12 | use std::os::unix::io::AsRawFd; 13 | 14 | use super::*; 15 | 16 | impl WritevExt for UdpSocket { 17 | fn writev(&self, bufs: [&[u8]; 2]) -> io::Result { 18 | unsafe { 19 | let iovecs = [ 20 | libc::iovec { 21 | iov_base: bufs[0].as_ptr() as *const _ as *mut _, 22 | iov_len: bufs[0].len(), 23 | }, 24 | libc::iovec { 25 | iov_base: bufs[1].as_ptr() as *const _ as *mut _, 26 | iov_len: bufs[1].len(), 27 | }, 28 | ]; 29 | let r = libc::writev(self.as_raw_fd(), iovecs.as_ptr(), 2); 30 | if r < 0 { 31 | Err(io::Error::last_os_error()) 32 | } else { 33 | Ok(r as usize) 34 | } 35 | } 36 | } 37 | 38 | fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result { 39 | unsafe { 40 | let mut iovecs = [ 41 | libc::iovec { 42 | iov_base: bufs[0].as_mut_ptr() as *mut _, 43 | iov_len: bufs[0].len(), 44 | }, 45 | libc::iovec { 46 | iov_base: bufs[1].as_mut_ptr() as *mut _, 47 | iov_len: bufs[1].len(), 48 | }, 49 | ]; 50 | let r = libc::readv(self.as_raw_fd(), iovecs.as_mut_ptr(), 2); 51 | if r < 0 { 52 | Err(io::Error::last_os_error()) 53 | } else { 54 | Ok(r as usize) 55 | } 56 | } 57 | } 58 | } 59 | } 60 | 61 | #[cfg(windows)] 62 | mod imp { 63 | use std::os::windows::io::AsRawSocket; 64 | use std::ptr; 65 | use winapi::shared::minwindef; 66 | use winapi::shared::ws2def; 67 | use winapi::um::winsock2; 68 | 69 | use super::*; 70 | 71 | impl WritevExt for UdpSocket { 72 | fn writev(&self, bufs: [&[u8]; 2]) -> io::Result { 73 | unsafe { 74 | let mut wsabufs = [ 75 | ws2def::WSABUF { 76 | len: bufs[0].len() as winsock2::u_long, 77 | buf: bufs[0].as_ptr() as *const _ as *mut _, 78 | }, 79 | ws2def::WSABUF { 80 | len: bufs[1].len() as winsock2::u_long, 81 | buf: bufs[1].as_ptr() as *const _ as *mut _, 82 | }, 83 | ]; 84 | let mut sent = 0; 85 | let r = winsock2::WSASend( 86 | self.as_raw_socket() as usize, 87 | wsabufs.as_mut_ptr(), 88 | bufs.len() as minwindef::DWORD, 89 | &mut sent, 90 | 0, 91 | ptr::null_mut(), 92 | None, 93 | ); 94 | if r == 0 { 95 | Ok(sent as usize) 96 | } else { 97 | Err(io::Error::last_os_error()) 98 | } 99 | } 100 | } 101 | 102 | fn readv(&self, bufs: [&mut [u8]; 2]) -> io::Result { 103 | unsafe { 104 | let mut wsabufs = [ 105 | ws2def::WSABUF { 106 | len: bufs[0].len() as winsock2::u_long, 107 | buf: bufs[0].as_mut_ptr() as *mut _, 108 | }, 109 | ws2def::WSABUF { 110 | len: bufs[1].len() as winsock2::u_long, 111 | buf: bufs[1].as_mut_ptr() as *mut _, 112 | }, 113 | ]; 114 | let mut recved = 0; 115 | let mut flags = 0; 116 | let r = winsock2::WSARecv( 117 | self.as_raw_socket() as usize, 118 | wsabufs.as_mut_ptr(), 119 | bufs.len() as minwindef::DWORD, 120 | &mut recved, 121 | &mut flags, 122 | ptr::null_mut(), 123 | None, 124 | ); 125 | if r == 0 { 126 | Ok(recved as usize) 127 | } else { 128 | Err(io::Error::last_os_error()) 129 | } 130 | } 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/batch.rs: -------------------------------------------------------------------------------- 1 | //! Batch utilities 2 | //! 3 | //! This module contains definitions and helper functions used when making batch calls. 4 | 5 | use bitcoin::{Script, Txid}; 6 | 7 | use crate::types::{Call, Param, ToElectrumScriptHash}; 8 | 9 | /// Helper structure that caches all the requests before they are actually sent to the server. 10 | /// 11 | /// Calls on this function are stored and run when [`batch_call`](../client/struct.Client.html#method.batch_call) 12 | /// is run on a [`Client`](../client/struct.Client.html). 13 | /// 14 | /// This structure can be used to make multiple *different* calls in one single run. For batch 15 | /// calls of the same type, there are shorthands methods defined on the 16 | /// [`Client`](../client/struct.Client.html), like 17 | /// [`batch_script_get_balance`](../client/struct.Client.html#method.batch_script_get_balance) to ask the 18 | /// server for the balance of multiple scripts with a single request. 19 | #[derive(Default)] 20 | pub struct Batch { 21 | calls: Vec, 22 | } 23 | 24 | impl Batch { 25 | /// Add a raw request to the batch queue 26 | pub fn raw(&mut self, method: String, params: Vec) { 27 | self.calls.push((method, params)); 28 | } 29 | 30 | /// Add one `blockchain.scripthash.listunspent` request to the batch queue 31 | pub fn script_list_unspent(&mut self, script: &Script) { 32 | let params = vec![Param::String(script.to_electrum_scripthash().to_hex())]; 33 | self.calls 34 | .push((String::from("blockchain.scripthash.listunspent"), params)); 35 | } 36 | 37 | /// Add one `blockchain.scripthash.get_history` request to the batch queue 38 | pub fn script_get_history(&mut self, script: &Script) { 39 | let params = vec![Param::String(script.to_electrum_scripthash().to_hex())]; 40 | self.calls 41 | .push((String::from("blockchain.scripthash.get_history"), params)); 42 | } 43 | 44 | /// Add one `blockchain.scripthash.get_balance` request to the batch queue 45 | pub fn script_get_balance(&mut self, script: &Script) { 46 | let params = vec![Param::String(script.to_electrum_scripthash().to_hex())]; 47 | self.calls 48 | .push((String::from("blockchain.scripthash.get_balance"), params)); 49 | } 50 | 51 | /// Add one `blockchain.scripthash.listunspent` request to the batch queue 52 | pub fn script_subscribe(&mut self, script: &Script) { 53 | let params = vec![Param::String(script.to_electrum_scripthash().to_hex())]; 54 | self.calls 55 | .push((String::from("blockchain.scripthash.subscribe"), params)); 56 | } 57 | 58 | /// Add one `blockchain.transaction.get` request to the batch queue 59 | pub fn transaction_get(&mut self, tx_hash: &Txid) { 60 | let params = vec![Param::String(format!("{:x}", tx_hash))]; 61 | self.calls 62 | .push((String::from("blockchain.transaction.get"), params)); 63 | } 64 | 65 | /// Add one `blockchain.transaction.get_merkle` request to the batch queue 66 | pub fn transaction_get_merkle(&mut self, tx_hash_and_height: &(Txid, usize)) { 67 | let (tx_hash, height) = tx_hash_and_height; 68 | let params = vec![ 69 | Param::String(format!("{:x}", tx_hash)), 70 | Param::Usize(*height), 71 | ]; 72 | self.calls 73 | .push((String::from("blockchain.transaction.get_merkle"), params)); 74 | } 75 | 76 | /// Add one `blockchain.estimatefee` request to the batch queue 77 | pub fn estimate_fee(&mut self, number: usize) { 78 | let params = vec![Param::Usize(number)]; 79 | self.calls 80 | .push((String::from("blockchain.estimatefee"), params)); 81 | } 82 | 83 | /// Add one `blockchain.block.get_header` request to the batch queue 84 | pub fn block_header(&mut self, height: u32) { 85 | let params = vec![Param::U32(height)]; 86 | self.calls 87 | .push((String::from("blockchain.block.header"), params)); 88 | } 89 | 90 | /// Returns an iterator on the batch 91 | pub fn iter(&self) -> BatchIter<'_> { 92 | BatchIter { 93 | batch: self, 94 | index: 0, 95 | } 96 | } 97 | } 98 | 99 | impl std::iter::IntoIterator for Batch { 100 | type Item = (String, Vec); 101 | type IntoIter = std::vec::IntoIter; 102 | 103 | fn into_iter(self) -> Self::IntoIter { 104 | self.calls.into_iter() 105 | } 106 | } 107 | 108 | pub struct BatchIter<'a> { 109 | batch: &'a Batch, 110 | index: usize, 111 | } 112 | 113 | impl<'a> std::iter::Iterator for BatchIter<'a> { 114 | type Item = &'a (String, Vec); 115 | 116 | fn next(&mut self) -> Option { 117 | let val = self.batch.calls.get(self.index); 118 | self.index += 1; 119 | val 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/socks/mod.rs: -------------------------------------------------------------------------------- 1 | //! SOCKS proxy clients 2 | 3 | use std::io; 4 | use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, ToSocketAddrs}; 5 | use std::vec; 6 | 7 | pub use self::v4::{Socks4Listener, Socks4Stream}; 8 | pub use self::v5::{Socks5Datagram, Socks5Listener, Socks5Stream}; 9 | 10 | mod v4; 11 | mod v5; 12 | mod writev; 13 | 14 | /// A description of a connection target. 15 | #[derive(Debug, Clone)] 16 | pub enum TargetAddr { 17 | /// Connect to an IP address. 18 | Ip(SocketAddr), 19 | /// Connect to a fully qualified domain name. 20 | /// 21 | /// The domain name will be passed along to the proxy server and DNS lookup 22 | /// will happen there. 23 | Domain(String, u16), 24 | } 25 | 26 | impl ToSocketAddrs for TargetAddr { 27 | type Iter = Iter; 28 | 29 | fn to_socket_addrs(&self) -> io::Result { 30 | let inner = match *self { 31 | TargetAddr::Ip(addr) => IterInner::Ip(Some(addr)), 32 | TargetAddr::Domain(ref domain, port) => { 33 | let it = (&**domain, port).to_socket_addrs()?; 34 | IterInner::Domain(it) 35 | } 36 | }; 37 | Ok(Iter(inner)) 38 | } 39 | } 40 | 41 | enum IterInner { 42 | Ip(Option), 43 | Domain(vec::IntoIter), 44 | } 45 | 46 | /// An iterator over `SocketAddr`s associated with a `TargetAddr`. 47 | pub struct Iter(IterInner); 48 | 49 | impl Iterator for Iter { 50 | type Item = SocketAddr; 51 | 52 | fn next(&mut self) -> Option { 53 | match self.0 { 54 | IterInner::Ip(ref mut addr) => addr.take(), 55 | IterInner::Domain(ref mut it) => it.next(), 56 | } 57 | } 58 | } 59 | 60 | /// A trait for objects that can be converted to `TargetAddr`. 61 | pub trait ToTargetAddr { 62 | /// Converts the value of `self` to a `TargetAddr`. 63 | fn to_target_addr(&self) -> io::Result; 64 | } 65 | 66 | impl ToTargetAddr for TargetAddr { 67 | fn to_target_addr(&self) -> io::Result { 68 | Ok(self.clone()) 69 | } 70 | } 71 | 72 | impl ToTargetAddr for SocketAddr { 73 | fn to_target_addr(&self) -> io::Result { 74 | Ok(TargetAddr::Ip(*self)) 75 | } 76 | } 77 | 78 | impl ToTargetAddr for SocketAddrV4 { 79 | fn to_target_addr(&self) -> io::Result { 80 | SocketAddr::V4(*self).to_target_addr() 81 | } 82 | } 83 | 84 | impl ToTargetAddr for SocketAddrV6 { 85 | fn to_target_addr(&self) -> io::Result { 86 | SocketAddr::V6(*self).to_target_addr() 87 | } 88 | } 89 | 90 | impl ToTargetAddr for (Ipv4Addr, u16) { 91 | fn to_target_addr(&self) -> io::Result { 92 | SocketAddrV4::new(self.0, self.1).to_target_addr() 93 | } 94 | } 95 | 96 | impl ToTargetAddr for (Ipv6Addr, u16) { 97 | fn to_target_addr(&self) -> io::Result { 98 | SocketAddrV6::new(self.0, self.1, 0, 0).to_target_addr() 99 | } 100 | } 101 | 102 | impl ToTargetAddr for (&str, u16) { 103 | fn to_target_addr(&self) -> io::Result { 104 | // try to parse as an IP first 105 | if let Ok(addr) = self.0.parse::() { 106 | return (addr, self.1).to_target_addr(); 107 | } 108 | 109 | if let Ok(addr) = self.0.parse::() { 110 | return (addr, self.1).to_target_addr(); 111 | } 112 | 113 | Ok(TargetAddr::Domain(self.0.to_owned(), self.1)) 114 | } 115 | } 116 | 117 | impl ToTargetAddr for &str { 118 | fn to_target_addr(&self) -> io::Result { 119 | // try to parse as an IP first 120 | if let Ok(addr) = self.parse::() { 121 | return addr.to_target_addr(); 122 | } 123 | 124 | if let Ok(addr) = self.parse::() { 125 | return addr.to_target_addr(); 126 | } 127 | 128 | // split the string by ':' and convert the second part to u16 129 | let mut parts_iter = self.rsplitn(2, ':'); 130 | let port_str = match parts_iter.next() { 131 | Some(s) => s, 132 | None => { 133 | return Err(io::Error::new( 134 | io::ErrorKind::InvalidInput, 135 | "invalid socket address", 136 | )) 137 | } 138 | }; 139 | 140 | let host = match parts_iter.next() { 141 | Some(s) => s, 142 | None => { 143 | return Err(io::Error::new( 144 | io::ErrorKind::InvalidInput, 145 | "invalid socket address", 146 | )) 147 | } 148 | }; 149 | 150 | let port: u16 = match port_str.parse() { 151 | Ok(p) => p, 152 | Err(_) => { 153 | return Err(io::Error::new( 154 | io::ErrorKind::InvalidInput, 155 | "invalid port value", 156 | )) 157 | } 158 | }; 159 | 160 | (host, port).to_target_addr() 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/socks/v4.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 2 | use std::io::{self, Read, Write}; 3 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, SocketAddrV6, TcpStream, ToSocketAddrs}; 4 | 5 | use super::{TargetAddr, ToTargetAddr}; 6 | 7 | fn read_response(socket: &mut TcpStream) -> io::Result { 8 | let mut response = [0u8; 8]; 9 | socket.read_exact(&mut response)?; 10 | let mut response = &response[..]; 11 | 12 | if response.read_u8()? != 0 { 13 | return Err(io::Error::new( 14 | io::ErrorKind::InvalidData, 15 | "invalid response version", 16 | )); 17 | } 18 | 19 | match response.read_u8()? { 20 | 90 => {} 21 | 91 => return Err(io::Error::other("request rejected or failed")), 22 | 92 => { 23 | return Err(io::Error::new( 24 | io::ErrorKind::PermissionDenied, 25 | "request rejected because SOCKS server cannot connect to \ 26 | idnetd on the client", 27 | )) 28 | } 29 | 93 => { 30 | return Err(io::Error::new( 31 | io::ErrorKind::PermissionDenied, 32 | "request rejected because the client program and identd \ 33 | report different user-ids", 34 | )) 35 | } 36 | _ => { 37 | return Err(io::Error::new( 38 | io::ErrorKind::InvalidData, 39 | "invalid response code", 40 | )) 41 | } 42 | } 43 | 44 | let port = response.read_u16::()?; 45 | let ip = Ipv4Addr::from(response.read_u32::()?); 46 | 47 | Ok(SocketAddrV4::new(ip, port)) 48 | } 49 | 50 | /// A SOCKS4 client. 51 | #[derive(Debug)] 52 | pub struct Socks4Stream { 53 | socket: TcpStream, 54 | proxy_addr: SocketAddrV4, 55 | } 56 | 57 | impl Socks4Stream { 58 | /// Connects to a target server through a SOCKS4 proxy. 59 | /// 60 | /// # Note 61 | /// 62 | /// If `target` is a `TargetAddr::Domain`, the domain name will be forwarded 63 | /// to the proxy server using the SOCKS4A protocol extension. If the proxy 64 | /// server does not support SOCKS4A, consider performing the DNS lookup 65 | /// locally and passing a `TargetAddr::Ip`. 66 | pub fn connect(proxy: T, target: U, userid: &str) -> io::Result 67 | where 68 | T: ToSocketAddrs, 69 | U: ToTargetAddr, 70 | { 71 | Self::connect_raw(1, proxy, target, userid) 72 | } 73 | 74 | fn connect_raw(command: u8, proxy: T, target: U, userid: &str) -> io::Result 75 | where 76 | T: ToSocketAddrs, 77 | U: ToTargetAddr, 78 | { 79 | let mut socket = TcpStream::connect(proxy)?; 80 | 81 | let target = target.to_target_addr()?; 82 | 83 | let mut packet = vec![]; 84 | let _ = packet.write_u8(4); // version 85 | let _ = packet.write_u8(command); // command code 86 | match target.to_target_addr()? { 87 | TargetAddr::Ip(addr) => { 88 | let addr = match addr { 89 | SocketAddr::V4(addr) => addr, 90 | SocketAddr::V6(_) => { 91 | return Err(io::Error::new( 92 | io::ErrorKind::InvalidInput, 93 | "SOCKS4 does not support IPv6", 94 | )); 95 | } 96 | }; 97 | let _ = packet.write_u16::(addr.port()); 98 | let _ = packet.write_u32::((*addr.ip()).into()); 99 | let _ = packet.write_all(userid.as_bytes()); 100 | let _ = packet.write_u8(0); 101 | } 102 | TargetAddr::Domain(ref host, port) => { 103 | let _ = packet.write_u16::(port); 104 | let _ = packet.write_u32::(Ipv4Addr::new(0, 0, 0, 1).into()); 105 | let _ = packet.write_all(userid.as_bytes()); 106 | let _ = packet.write_u8(0); 107 | packet.extend(host.as_bytes()); 108 | let _ = packet.write_u8(0); 109 | } 110 | } 111 | 112 | socket.write_all(&packet)?; 113 | let proxy_addr = read_response(&mut socket)?; 114 | 115 | Ok(Socks4Stream { socket, proxy_addr }) 116 | } 117 | 118 | /// Returns the proxy-side address of the connection between the proxy and 119 | /// target server. 120 | pub fn proxy_addr(&self) -> SocketAddrV4 { 121 | self.proxy_addr 122 | } 123 | 124 | /// Returns a shared reference to the inner `TcpStream`. 125 | pub fn get_ref(&self) -> &TcpStream { 126 | &self.socket 127 | } 128 | 129 | /// Returns a mutable reference to the inner `TcpStream`. 130 | pub fn get_mut(&mut self) -> &mut TcpStream { 131 | &mut self.socket 132 | } 133 | 134 | /// Consumes the `Socks4Stream`, returning the inner `TcpStream`. 135 | pub fn into_inner(self) -> TcpStream { 136 | self.socket 137 | } 138 | } 139 | 140 | impl Read for Socks4Stream { 141 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 142 | self.socket.read(buf) 143 | } 144 | } 145 | 146 | impl Read for &Socks4Stream { 147 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 148 | (&self.socket).read(buf) 149 | } 150 | } 151 | 152 | impl Write for Socks4Stream { 153 | fn write(&mut self, buf: &[u8]) -> io::Result { 154 | self.socket.write(buf) 155 | } 156 | 157 | fn flush(&mut self) -> io::Result<()> { 158 | self.socket.flush() 159 | } 160 | } 161 | 162 | impl Write for &Socks4Stream { 163 | fn write(&mut self, buf: &[u8]) -> io::Result { 164 | (&self.socket).write(buf) 165 | } 166 | 167 | fn flush(&mut self) -> io::Result<()> { 168 | (&self.socket).flush() 169 | } 170 | } 171 | 172 | /// A SOCKS4 BIND client. 173 | #[derive(Debug)] 174 | pub struct Socks4Listener(Socks4Stream); 175 | 176 | impl Socks4Listener { 177 | /// Initiates a BIND request to the specified proxy. 178 | /// 179 | /// The proxy will filter incoming connections based on the value of 180 | /// `target`. 181 | pub fn bind(proxy: T, target: U, userid: &str) -> io::Result 182 | where 183 | T: ToSocketAddrs, 184 | U: ToTargetAddr, 185 | { 186 | Socks4Stream::connect_raw(2, proxy, target, userid).map(Socks4Listener) 187 | } 188 | 189 | /// The address of the proxy-side TCP listener. 190 | /// 191 | /// This should be forwarded to the remote process, which should open a 192 | /// connection to it. 193 | pub fn proxy_addr(&self) -> io::Result { 194 | if self.0.proxy_addr.ip().octets() != [0, 0, 0, 0] { 195 | Ok(SocketAddr::V4(self.0.proxy_addr())) 196 | } else { 197 | let port = self.0.proxy_addr.port(); 198 | let peer = match self.0.socket.peer_addr()? { 199 | SocketAddr::V4(addr) => SocketAddr::V4(SocketAddrV4::new(*addr.ip(), port)), 200 | SocketAddr::V6(addr) => SocketAddr::V6(SocketAddrV6::new(*addr.ip(), port, 0, 0)), 201 | }; 202 | Ok(peer) 203 | } 204 | } 205 | 206 | /// Waits for the remote process to connect to the proxy server. 207 | /// 208 | /// The value of `proxy_addr` should be forwarded to the remote process 209 | /// before this method is called. 210 | pub fn accept(mut self) -> io::Result { 211 | self.0.proxy_addr = read_response(&mut self.0.socket)?; 212 | Ok(self.0) 213 | } 214 | } 215 | 216 | #[cfg(test)] 217 | mod test { 218 | use std::io::{Read, Write}; 219 | use std::net::{SocketAddr, SocketAddrV4, TcpStream, ToSocketAddrs}; 220 | 221 | use super::*; 222 | 223 | fn google_ip() -> SocketAddrV4 { 224 | "google.com:80" 225 | .to_socket_addrs() 226 | .unwrap() 227 | .filter_map(|a| match a { 228 | SocketAddr::V4(a) => Some(a), 229 | SocketAddr::V6(_) => None, 230 | }) 231 | .next() 232 | .unwrap() 233 | } 234 | 235 | #[test] 236 | #[ignore] 237 | fn google() { 238 | let mut socket = Socks4Stream::connect("127.0.0.1:1080", google_ip(), "").unwrap(); 239 | 240 | socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); 241 | let mut result = vec![]; 242 | socket.read_to_end(&mut result).unwrap(); 243 | 244 | println!("{}", String::from_utf8_lossy(&result)); 245 | assert!(result.starts_with(b"HTTP/1.0")); 246 | assert!(result.ends_with(b"\r\n") || result.ends_with(b"")); 247 | } 248 | 249 | #[test] 250 | #[ignore] // dante doesn't support SOCKS4A 251 | fn google_dns() { 252 | let mut socket = Socks4Stream::connect("127.0.0.1:8080", "google.com:80", "").unwrap(); 253 | 254 | socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); 255 | let mut result = vec![]; 256 | socket.read_to_end(&mut result).unwrap(); 257 | 258 | println!("{}", String::from_utf8_lossy(&result)); 259 | assert!(result.starts_with(b"HTTP/1.0")); 260 | assert!(result.ends_with(b"\r\n") || result.ends_with(b"")); 261 | } 262 | 263 | #[test] 264 | #[ignore] 265 | fn bind() { 266 | // First figure out our local address that we'll be connecting from 267 | let socket = Socks4Stream::connect("127.0.0.1:1080", google_ip(), "").unwrap(); 268 | let addr = socket.proxy_addr(); 269 | 270 | let listener = Socks4Listener::bind("127.0.0.1:1080", addr, "").unwrap(); 271 | let addr = listener.proxy_addr().unwrap(); 272 | let mut end = TcpStream::connect(addr).unwrap(); 273 | let mut conn = listener.accept().unwrap(); 274 | conn.write_all(b"hello world").unwrap(); 275 | drop(conn); 276 | let mut result = vec![]; 277 | end.read_to_end(&mut result).unwrap(); 278 | assert_eq!(result, b"hello world"); 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | //! Return types 2 | //! 3 | //! This module contains definitions of all the complex data structures that are returned by calls 4 | 5 | use std::convert::TryFrom; 6 | use std::fmt::{self, Display, Formatter}; 7 | use std::ops::Deref; 8 | use std::sync::Arc; 9 | 10 | use bitcoin::blockdata::block; 11 | use bitcoin::consensus::encode::deserialize; 12 | use bitcoin::hashes::{sha256, Hash}; 13 | use bitcoin::hex::{DisplayHex, FromHex}; 14 | use bitcoin::{Script, Txid}; 15 | 16 | use serde::{de, Deserialize, Serialize}; 17 | 18 | static JSONRPC_2_0: &str = "2.0"; 19 | 20 | pub(crate) type Call = (String, Vec); 21 | 22 | #[derive(Serialize, Clone)] 23 | #[serde(untagged)] 24 | /// A single parameter of a [`Request`](struct.Request.html) 25 | pub enum Param { 26 | /// Integer parameter 27 | U32(u32), 28 | /// Integer parameter 29 | Usize(usize), 30 | /// String parameter 31 | String(String), 32 | /// Boolean parameter 33 | Bool(bool), 34 | /// Bytes array parameter 35 | Bytes(Vec), 36 | } 37 | 38 | #[derive(Serialize, Clone)] 39 | /// A request that can be sent to the server 40 | pub struct Request<'a> { 41 | jsonrpc: &'static str, 42 | 43 | /// The JSON-RPC request id 44 | pub id: usize, 45 | /// The request method 46 | pub method: &'a str, 47 | /// The request parameters 48 | pub params: Vec, 49 | } 50 | 51 | impl<'a> Request<'a> { 52 | /// Creates a new request with a default id 53 | fn new(method: &'a str, params: Vec) -> Self { 54 | Self { 55 | id: 0, 56 | jsonrpc: JSONRPC_2_0, 57 | method, 58 | params, 59 | } 60 | } 61 | 62 | /// Creates a new request with a user-specified id 63 | pub fn new_id(id: usize, method: &'a str, params: Vec) -> Self { 64 | let mut instance = Self::new(method, params); 65 | instance.id = id; 66 | 67 | instance 68 | } 69 | } 70 | 71 | #[doc(hidden)] 72 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] 73 | pub struct Hex32Bytes(#[serde(deserialize_with = "from_hex", serialize_with = "to_hex")] [u8; 32]); 74 | 75 | impl Deref for Hex32Bytes { 76 | type Target = [u8; 32]; 77 | 78 | fn deref(&self) -> &Self::Target { 79 | &self.0 80 | } 81 | } 82 | 83 | impl From<[u8; 32]> for Hex32Bytes { 84 | fn from(other: [u8; 32]) -> Hex32Bytes { 85 | Hex32Bytes(other) 86 | } 87 | } 88 | 89 | impl Hex32Bytes { 90 | pub(crate) fn to_hex(self) -> String { 91 | self.0.to_lower_hex_string() 92 | } 93 | } 94 | 95 | /// Format used by the Electrum server to identify an address. The reverse sha256 hash of the 96 | /// scriptPubKey. Documented [here](https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes). 97 | pub type ScriptHash = Hex32Bytes; 98 | 99 | /// Binary blob that condenses all the activity of an address. Used to detect changes without 100 | /// having to compare potentially long lists of transactions. 101 | pub type ScriptStatus = Hex32Bytes; 102 | 103 | /// Trait used to convert a struct into the Electrum representation of an address 104 | pub trait ToElectrumScriptHash { 105 | /// Transforms the current struct into a `ScriptHash` 106 | fn to_electrum_scripthash(&self) -> ScriptHash; 107 | } 108 | 109 | impl ToElectrumScriptHash for Script { 110 | fn to_electrum_scripthash(&self) -> ScriptHash { 111 | let mut result = sha256::Hash::hash(self.as_bytes()).to_byte_array(); 112 | result.reverse(); 113 | 114 | result.into() 115 | } 116 | } 117 | 118 | fn from_hex<'de, T, D>(deserializer: D) -> Result 119 | where 120 | T: FromHex, 121 | D: de::Deserializer<'de>, 122 | { 123 | let s = String::deserialize(deserializer)?; 124 | T::from_hex(&s).map_err(de::Error::custom) 125 | } 126 | 127 | fn to_hex(bytes: &[u8], serializer: S) -> std::result::Result 128 | where 129 | S: serde::ser::Serializer, 130 | { 131 | serializer.serialize_str(&bytes.to_lower_hex_string()) 132 | } 133 | 134 | fn from_hex_array<'de, T, D>(deserializer: D) -> Result, D::Error> 135 | where 136 | T: FromHex + std::fmt::Debug, 137 | D: de::Deserializer<'de>, 138 | { 139 | let arr = Vec::::deserialize(deserializer)?; 140 | 141 | let results: Vec> = arr 142 | .into_iter() 143 | .map(|s| T::from_hex(&s).map_err(de::Error::custom)) 144 | .collect(); 145 | 146 | let mut answer = Vec::new(); 147 | for x in results.into_iter() { 148 | answer.push(x?); 149 | } 150 | 151 | Ok(answer) 152 | } 153 | 154 | fn from_hex_header<'de, D>(deserializer: D) -> Result 155 | where 156 | D: de::Deserializer<'de>, 157 | { 158 | let vec: Vec = from_hex(deserializer)?; 159 | deserialize(&vec).map_err(de::Error::custom) 160 | } 161 | 162 | /// Response to a [`script_get_history`](../client/struct.Client.html#method.script_get_history) request. 163 | #[derive(Clone, Debug, Deserialize)] 164 | pub struct GetHistoryRes { 165 | /// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of 166 | /// its inputs are unconfirmed too. 167 | pub height: i32, 168 | /// Txid of the transaction. 169 | pub tx_hash: Txid, 170 | /// Fee of the transaction. 171 | pub fee: Option, 172 | } 173 | 174 | /// Response to a [`script_list_unspent`](../client/struct.Client.html#method.script_list_unspent) request. 175 | #[derive(Clone, Debug, Deserialize)] 176 | pub struct ListUnspentRes { 177 | /// Confirmation height of the transaction that created this output. 178 | pub height: usize, 179 | /// Txid of the transaction 180 | pub tx_hash: Txid, 181 | /// Index of the output in the transaction. 182 | pub tx_pos: usize, 183 | /// Value of the output. 184 | pub value: u64, 185 | } 186 | 187 | /// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request. 188 | #[derive(Clone, Debug, Deserialize)] 189 | pub struct ServerFeaturesRes { 190 | /// Server version reported. 191 | pub server_version: String, 192 | /// Hash of the genesis block. 193 | #[serde(deserialize_with = "from_hex")] 194 | pub genesis_hash: [u8; 32], 195 | /// Minimum supported version of the protocol. 196 | pub protocol_min: String, 197 | /// Maximum supported version of the protocol. 198 | pub protocol_max: String, 199 | /// Hash function used to create the [`ScriptHash`](type.ScriptHash.html). 200 | pub hash_function: Option, 201 | /// Pruned height of the server. 202 | pub pruning: Option, 203 | } 204 | 205 | /// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request. 206 | #[derive(Clone, Debug, Deserialize)] 207 | pub struct GetHeadersRes { 208 | /// Maximum number of headers returned in a single response. 209 | pub max: usize, 210 | /// Number of headers in this response. 211 | pub count: usize, 212 | /// Raw headers concatenated. Normally cleared before returning. 213 | #[serde(rename(deserialize = "hex"), deserialize_with = "from_hex")] 214 | pub raw_headers: Vec, 215 | /// Array of block headers. 216 | #[serde(skip)] 217 | pub headers: Vec, 218 | } 219 | 220 | /// Response to a [`script_get_balance`](../client/struct.Client.html#method.script_get_balance) request. 221 | #[derive(Clone, Debug, Deserialize)] 222 | pub struct GetBalanceRes { 223 | /// Confirmed balance in Satoshis for the address. 224 | pub confirmed: u64, 225 | /// Unconfirmed balance in Satoshis for the address. 226 | /// 227 | /// Some servers (e.g. `electrs`) return this as a negative value. 228 | pub unconfirmed: i64, 229 | } 230 | 231 | /// Response to a [`transaction_get_merkle`](../client/struct.Client.html#method.transaction_get_merkle) request. 232 | #[derive(Clone, Debug, Deserialize)] 233 | pub struct GetMerkleRes { 234 | /// Height of the block that confirmed the transaction 235 | pub block_height: usize, 236 | /// Position in the block of the transaction. 237 | pub pos: usize, 238 | /// The merkle path of the transaction. 239 | #[serde(deserialize_with = "from_hex_array")] 240 | pub merkle: Vec<[u8; 32]>, 241 | } 242 | 243 | /// Response to a [`txid_from_pos_with_merkle`](../client/struct.Client.html#method.txid_from_pos_with_merkle) 244 | /// request. 245 | #[derive(Clone, Debug, Deserialize)] 246 | pub struct TxidFromPosRes { 247 | /// Txid of the transaction. 248 | pub tx_hash: Txid, 249 | /// The merkle path of the transaction. 250 | #[serde(deserialize_with = "from_hex_array")] 251 | pub merkle: Vec<[u8; 32]>, 252 | } 253 | 254 | /// Notification of a new block header 255 | #[derive(Clone, Debug, Deserialize)] 256 | pub struct HeaderNotification { 257 | /// New block height. 258 | pub height: usize, 259 | /// Newly added header. 260 | #[serde(rename = "hex", deserialize_with = "from_hex_header")] 261 | pub header: block::Header, 262 | } 263 | 264 | /// Notification of a new block header with the header encoded as raw bytes 265 | #[derive(Clone, Debug, Deserialize)] 266 | pub struct RawHeaderNotification { 267 | /// New block height. 268 | pub height: usize, 269 | /// Newly added header. 270 | #[serde(rename = "hex", deserialize_with = "from_hex")] 271 | pub header: Vec, 272 | } 273 | 274 | impl TryFrom for HeaderNotification { 275 | type Error = Error; 276 | 277 | fn try_from(raw: RawHeaderNotification) -> Result { 278 | Ok(HeaderNotification { 279 | height: raw.height, 280 | header: deserialize(&raw.header)?, 281 | }) 282 | } 283 | } 284 | 285 | /// Notification of the new status of a script 286 | #[derive(Clone, Debug, Deserialize)] 287 | pub struct ScriptNotification { 288 | /// Address that generated this notification. 289 | pub scripthash: ScriptHash, 290 | /// The new status of the address. 291 | pub status: ScriptStatus, 292 | } 293 | 294 | /// Errors 295 | #[derive(Debug)] 296 | pub enum Error { 297 | /// Wraps `std::io::Error` 298 | IOError(std::io::Error), 299 | /// Wraps `serde_json::error::Error` 300 | JSON(serde_json::error::Error), 301 | /// Wraps `bitcoin::hex::HexToBytesError` 302 | Hex(bitcoin::hex::HexToBytesError), 303 | /// Error returned by the Electrum server 304 | Protocol(serde_json::Value), 305 | /// Error during the deserialization of a Bitcoin data structure 306 | Bitcoin(bitcoin::consensus::encode::Error), 307 | /// Already subscribed to the notifications of an address 308 | AlreadySubscribed(ScriptHash), 309 | /// Not subscribed to the notifications of an address 310 | NotSubscribed(ScriptHash), 311 | /// Error during the deserialization of a response from the server 312 | InvalidResponse(serde_json::Value), 313 | /// Generic error with a message 314 | Message(String), 315 | /// Invalid domain name for an SSL certificate 316 | InvalidDNSNameError(String), 317 | /// Missing domain while it was explicitly asked to validate it 318 | MissingDomain, 319 | /// Made one or multiple attempts, always in Error 320 | AllAttemptsErrored(Vec), 321 | /// There was an io error reading the socket, to be shared between threads 322 | SharedIOError(Arc), 323 | 324 | /// Couldn't take a lock on the reader mutex. This means that there's already another reader 325 | /// thread running 326 | CouldntLockReader, 327 | /// Broken IPC communication channel: the other thread probably has exited 328 | Mpsc, 329 | #[cfg(any(feature = "use-rustls", feature = "use-rustls-ring"))] 330 | /// Could not create a rustls client connection 331 | CouldNotCreateConnection(rustls::Error), 332 | 333 | #[cfg(feature = "use-openssl")] 334 | /// Invalid OpenSSL method used 335 | InvalidSslMethod(openssl::error::ErrorStack), 336 | #[cfg(feature = "use-openssl")] 337 | /// SSL Handshake failed with the server 338 | SslHandshakeError(openssl::ssl::HandshakeError), 339 | } 340 | 341 | impl Display for Error { 342 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 343 | match self { 344 | Error::IOError(e) => Display::fmt(e, f), 345 | Error::JSON(e) => Display::fmt(e, f), 346 | Error::Hex(e) => Display::fmt(e, f), 347 | Error::Bitcoin(e) => Display::fmt(e, f), 348 | Error::SharedIOError(e) => Display::fmt(e, f), 349 | #[cfg(feature = "use-openssl")] 350 | Error::SslHandshakeError(e) => Display::fmt(e, f), 351 | #[cfg(feature = "use-openssl")] 352 | Error::InvalidSslMethod(e) => Display::fmt(e, f), 353 | #[cfg(any( 354 | feature = "use-rustls", 355 | feature = "use-rustls-ring", 356 | ))] 357 | Error::CouldNotCreateConnection(e) => Display::fmt(e, f), 358 | 359 | Error::Message(e) => f.write_str(e), 360 | Error::InvalidDNSNameError(domain) => write!(f, "Invalid domain name {} not matching SSL certificate", domain), 361 | Error::AllAttemptsErrored(errors) => { 362 | f.write_str("Made one or multiple attempts, all errored:\n")?; 363 | for err in errors { 364 | writeln!(f, "\t- {}", err)?; 365 | } 366 | Ok(()) 367 | } 368 | 369 | Error::Protocol(e) => write!(f, "Electrum server error: {}", e.clone().take()), 370 | Error::InvalidResponse(e) => write!(f, "Error during the deserialization of a response from the server: {}", e.clone().take()), 371 | 372 | // TODO: Print out addresses once `ScriptHash` will implement `Display` 373 | Error::AlreadySubscribed(_) => write!(f, "Already subscribed to the notifications of an address"), 374 | Error::NotSubscribed(_) => write!(f, "Not subscribed to the notifications of an address"), 375 | 376 | Error::MissingDomain => f.write_str("Missing domain while it was explicitly asked to validate it"), 377 | 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"), 378 | Error::Mpsc => f.write_str("Broken IPC communication channel: the other thread probably has exited"), 379 | } 380 | } 381 | } 382 | 383 | impl std::error::Error for Error {} 384 | 385 | macro_rules! impl_error { 386 | ( $from:ty, $to:ident ) => { 387 | impl std::convert::From<$from> for Error { 388 | fn from(err: $from) -> Self { 389 | Error::$to(err.into()) 390 | } 391 | } 392 | }; 393 | } 394 | 395 | impl_error!(std::io::Error, IOError); 396 | impl_error!(serde_json::Error, JSON); 397 | impl_error!(bitcoin::hex::HexToBytesError, Hex); 398 | impl_error!(bitcoin::consensus::encode::Error, Bitcoin); 399 | 400 | impl From> for Error { 401 | fn from(_: std::sync::PoisonError) -> Self { 402 | Error::IOError(std::io::Error::from(std::io::ErrorKind::BrokenPipe)) 403 | } 404 | } 405 | 406 | impl From> for Error { 407 | fn from(_: std::sync::mpsc::SendError) -> Self { 408 | Error::Mpsc 409 | } 410 | } 411 | 412 | impl From for Error { 413 | fn from(_: std::sync::mpsc::RecvError) -> Self { 414 | Error::Mpsc 415 | } 416 | } 417 | 418 | #[cfg(test)] 419 | mod tests { 420 | use crate::ScriptStatus; 421 | 422 | #[test] 423 | fn script_status_roundtrip() { 424 | let script_status: ScriptStatus = [1u8; 32].into(); 425 | let script_status_json = serde_json::to_string(&script_status).unwrap(); 426 | let script_status_back = serde_json::from_str(&script_status_json).unwrap(); 427 | assert_eq!(script_status, script_status_back); 428 | } 429 | } 430 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | //! Electrum Client 2 | 3 | use std::{borrow::Borrow, sync::RwLock}; 4 | 5 | use log::{info, warn}; 6 | 7 | use bitcoin::{Script, Txid}; 8 | 9 | use crate::api::ElectrumApi; 10 | use crate::batch::Batch; 11 | use crate::config::Config; 12 | use crate::raw_client::*; 13 | use crate::types::*; 14 | use std::convert::TryFrom; 15 | 16 | /// Generalized Electrum client that supports multiple backends. This wraps 17 | /// [`RawClient`](client/struct.RawClient.html) and provides a more user-friendly 18 | /// constructor that can choose the right backend based on the url prefix. 19 | /// 20 | /// **This is available only with the `default` features, or if `proxy` and one ssl implementation are enabled** 21 | pub enum ClientType { 22 | #[allow(missing_docs)] 23 | TCP(RawClient), 24 | #[allow(missing_docs)] 25 | SSL(RawClient), 26 | #[allow(missing_docs)] 27 | Socks5(RawClient), 28 | } 29 | 30 | /// Generalized Electrum client that supports multiple backends. Can re-instantiate client_type if connections 31 | /// drops 32 | pub struct Client { 33 | client_type: RwLock, 34 | config: Config, 35 | url: String, 36 | } 37 | 38 | macro_rules! impl_inner_call { 39 | ( $self:expr, $name:ident $(, $args:expr)* ) => { 40 | { 41 | let mut errors = vec![]; 42 | loop { 43 | let read_client = $self.client_type.read().unwrap(); 44 | let res = match &*read_client { 45 | ClientType::TCP(inner) => inner.$name( $($args, )* ), 46 | ClientType::SSL(inner) => inner.$name( $($args, )* ), 47 | ClientType::Socks5(inner) => inner.$name( $($args, )* ), 48 | }; 49 | drop(read_client); 50 | match res { 51 | Ok(val) => return Ok(val), 52 | Err(Error::Protocol(_) | Error::AlreadySubscribed(_)) => { 53 | return res; 54 | }, 55 | Err(e) => { 56 | let failed_attempts = errors.len() + 1; 57 | 58 | if retries_exhausted(failed_attempts, $self.config.retry()) { 59 | warn!("call '{}' failed after {} attempts", stringify!($name), failed_attempts); 60 | return Err(Error::AllAttemptsErrored(errors)); 61 | } 62 | 63 | warn!("call '{}' failed with {}, retry: {}/{}", stringify!($name), e, failed_attempts, $self.config.retry()); 64 | 65 | errors.push(e); 66 | 67 | // Only one thread will try to recreate the client getting the write lock, 68 | // other eventual threads will get Err and will block at the beginning of 69 | // previous loop when trying to read() 70 | if let Ok(mut write_client) = $self.client_type.try_write() { 71 | loop { 72 | std::thread::sleep(std::time::Duration::from_secs((1 << errors.len()).min(30) as u64)); 73 | match ClientType::from_config(&$self.url, &$self.config) { 74 | Ok(new_client) => { 75 | info!("Succesfully created new client"); 76 | *write_client = new_client; 77 | break; 78 | }, 79 | Err(e) => { 80 | let failed_attempts = errors.len() + 1; 81 | 82 | if retries_exhausted(failed_attempts, $self.config.retry()) { 83 | warn!("re-creating client failed after {} attempts", failed_attempts); 84 | return Err(Error::AllAttemptsErrored(errors)); 85 | } 86 | 87 | warn!("re-creating client failed with {}, retry: {}/{}", e, failed_attempts, $self.config.retry()); 88 | 89 | errors.push(e); 90 | } 91 | } 92 | } 93 | } 94 | }, 95 | } 96 | }} 97 | } 98 | } 99 | 100 | fn retries_exhausted(failed_attempts: usize, configured_retries: u8) -> bool { 101 | match u8::try_from(failed_attempts) { 102 | Ok(failed_attempts) => failed_attempts > configured_retries, 103 | Err(_) => true, // if the usize doesn't fit into a u8, we definitely exhausted our retries 104 | } 105 | } 106 | 107 | impl ClientType { 108 | /// Constructor that supports multiple backends and allows configuration through 109 | /// the [Config] 110 | pub fn from_config(url: &str, config: &Config) -> Result { 111 | if url.starts_with("ssl://") { 112 | let url = url.replacen("ssl://", "", 1); 113 | let client = match config.socks5() { 114 | Some(socks5) => RawClient::new_proxy_ssl( 115 | url.as_str(), 116 | config.validate_domain(), 117 | socks5, 118 | config.timeout(), 119 | )?, 120 | None => { 121 | RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())? 122 | } 123 | }; 124 | 125 | Ok(ClientType::SSL(client)) 126 | } else { 127 | let url = url.replacen("tcp://", "", 1); 128 | 129 | Ok(match config.socks5().as_ref() { 130 | None => ClientType::TCP(RawClient::new(url.as_str(), config.timeout())?), 131 | Some(socks5) => ClientType::Socks5(RawClient::new_proxy( 132 | url.as_str(), 133 | socks5, 134 | config.timeout(), 135 | )?), 136 | }) 137 | } 138 | } 139 | } 140 | 141 | impl Client { 142 | /// Default constructor supporting multiple backends by providing a prefix 143 | /// 144 | /// Supported prefixes are: 145 | /// - tcp:// for a TCP plaintext client. 146 | /// - ssl:// for an SSL-encrypted client. The server certificate will be verified. 147 | /// 148 | /// If no prefix is specified, then `tcp://` is assumed. 149 | /// 150 | /// See [Client::from_config] for more configuration options 151 | pub fn new(url: &str) -> Result { 152 | Self::from_config(url, Config::default()) 153 | } 154 | 155 | /// Generic constructor that supports multiple backends and allows configuration through 156 | /// the [Config] 157 | pub fn from_config(url: &str, config: Config) -> Result { 158 | let client_type = RwLock::new(ClientType::from_config(url, &config)?); 159 | 160 | Ok(Client { 161 | client_type, 162 | config, 163 | url: url.to_string(), 164 | }) 165 | } 166 | } 167 | 168 | impl ElectrumApi for Client { 169 | #[inline] 170 | fn raw_call( 171 | &self, 172 | method_name: &str, 173 | params: impl IntoIterator, 174 | ) -> Result { 175 | // We can't passthrough this method to the inner client because it would require the 176 | // `params` argument to also be `Copy` (because it's used multiple times for multiple 177 | // retries). To avoid adding this extra trait bound we instead re-direct this call to the internal 178 | // `RawClient::internal_raw_call_with_vec` method. 179 | 180 | let vec = params.into_iter().collect::>(); 181 | impl_inner_call!(self, internal_raw_call_with_vec, method_name, vec.clone()); 182 | } 183 | 184 | #[inline] 185 | fn batch_call(&self, batch: &Batch) -> Result, Error> { 186 | impl_inner_call!(self, batch_call, batch) 187 | } 188 | 189 | #[inline] 190 | fn block_headers_subscribe_raw(&self) -> Result { 191 | impl_inner_call!(self, block_headers_subscribe_raw) 192 | } 193 | 194 | #[inline] 195 | fn block_headers_pop_raw(&self) -> Result, Error> { 196 | impl_inner_call!(self, block_headers_pop_raw) 197 | } 198 | 199 | #[inline] 200 | fn block_header_raw(&self, height: usize) -> Result, Error> { 201 | impl_inner_call!(self, block_header_raw, height) 202 | } 203 | 204 | #[inline] 205 | fn block_headers(&self, start_height: usize, count: usize) -> Result { 206 | impl_inner_call!(self, block_headers, start_height, count) 207 | } 208 | 209 | #[inline] 210 | fn estimate_fee(&self, number: usize) -> Result { 211 | impl_inner_call!(self, estimate_fee, number) 212 | } 213 | 214 | #[inline] 215 | fn relay_fee(&self) -> Result { 216 | impl_inner_call!(self, relay_fee) 217 | } 218 | 219 | #[inline] 220 | fn script_subscribe(&self, script: &Script) -> Result, Error> { 221 | impl_inner_call!(self, script_subscribe, script) 222 | } 223 | 224 | #[inline] 225 | fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result>, Error> 226 | where 227 | I: IntoIterator + Clone, 228 | I::Item: Borrow<&'s Script>, 229 | { 230 | impl_inner_call!(self, batch_script_subscribe, scripts.clone()) 231 | } 232 | 233 | #[inline] 234 | fn script_unsubscribe(&self, script: &Script) -> Result { 235 | impl_inner_call!(self, script_unsubscribe, script) 236 | } 237 | 238 | #[inline] 239 | fn script_pop(&self, script: &Script) -> Result, Error> { 240 | impl_inner_call!(self, script_pop, script) 241 | } 242 | 243 | #[inline] 244 | fn script_get_balance(&self, script: &Script) -> Result { 245 | impl_inner_call!(self, script_get_balance, script) 246 | } 247 | 248 | #[inline] 249 | fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result, Error> 250 | where 251 | I: IntoIterator + Clone, 252 | I::Item: Borrow<&'s Script>, 253 | { 254 | impl_inner_call!(self, batch_script_get_balance, scripts.clone()) 255 | } 256 | 257 | #[inline] 258 | fn script_get_history(&self, script: &Script) -> Result, Error> { 259 | impl_inner_call!(self, script_get_history, script) 260 | } 261 | 262 | #[inline] 263 | fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result>, Error> 264 | where 265 | I: IntoIterator + Clone, 266 | I::Item: Borrow<&'s Script>, 267 | { 268 | impl_inner_call!(self, batch_script_get_history, scripts.clone()) 269 | } 270 | 271 | #[inline] 272 | fn script_list_unspent(&self, script: &Script) -> Result, Error> { 273 | impl_inner_call!(self, script_list_unspent, script) 274 | } 275 | 276 | #[inline] 277 | fn batch_script_list_unspent<'s, I>( 278 | &self, 279 | scripts: I, 280 | ) -> Result>, Error> 281 | where 282 | I: IntoIterator + Clone, 283 | I::Item: Borrow<&'s Script>, 284 | { 285 | impl_inner_call!(self, batch_script_list_unspent, scripts.clone()) 286 | } 287 | 288 | #[inline] 289 | fn transaction_get_raw(&self, txid: &Txid) -> Result, Error> { 290 | impl_inner_call!(self, transaction_get_raw, txid) 291 | } 292 | 293 | #[inline] 294 | fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result>, Error> 295 | where 296 | I: IntoIterator + Clone, 297 | I::Item: Borrow<&'t Txid>, 298 | { 299 | impl_inner_call!(self, batch_transaction_get_raw, txids.clone()) 300 | } 301 | 302 | #[inline] 303 | fn batch_block_header_raw<'s, I>(&self, heights: I) -> Result>, Error> 304 | where 305 | I: IntoIterator + Clone, 306 | I::Item: Borrow, 307 | { 308 | impl_inner_call!(self, batch_block_header_raw, heights.clone()) 309 | } 310 | 311 | #[inline] 312 | fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result, Error> 313 | where 314 | I: IntoIterator + Clone, 315 | I::Item: Borrow, 316 | { 317 | impl_inner_call!(self, batch_estimate_fee, numbers.clone()) 318 | } 319 | 320 | #[inline] 321 | fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result { 322 | impl_inner_call!(self, transaction_broadcast_raw, raw_tx) 323 | } 324 | 325 | #[inline] 326 | fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result { 327 | impl_inner_call!(self, transaction_get_merkle, txid, height) 328 | } 329 | 330 | #[inline] 331 | fn batch_transaction_get_merkle( 332 | &self, 333 | txids_and_heights: I, 334 | ) -> Result, Error> 335 | where 336 | I: IntoIterator + Clone, 337 | I::Item: Borrow<(Txid, usize)>, 338 | { 339 | impl_inner_call!( 340 | self, 341 | batch_transaction_get_merkle, 342 | txids_and_heights.clone() 343 | ) 344 | } 345 | 346 | #[inline] 347 | fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result { 348 | impl_inner_call!(self, txid_from_pos, height, tx_pos) 349 | } 350 | 351 | #[inline] 352 | fn txid_from_pos_with_merkle( 353 | &self, 354 | height: usize, 355 | tx_pos: usize, 356 | ) -> Result { 357 | impl_inner_call!(self, txid_from_pos_with_merkle, height, tx_pos) 358 | } 359 | 360 | #[inline] 361 | fn server_features(&self) -> Result { 362 | impl_inner_call!(self, server_features) 363 | } 364 | 365 | #[inline] 366 | fn ping(&self) -> Result<(), Error> { 367 | impl_inner_call!(self, ping) 368 | } 369 | 370 | #[inline] 371 | #[cfg(feature = "debug-calls")] 372 | fn calls_made(&self) -> Result { 373 | impl_inner_call!(self, calls_made) 374 | } 375 | } 376 | 377 | #[cfg(test)] 378 | mod tests { 379 | use super::*; 380 | 381 | #[test] 382 | fn more_failed_attempts_than_retries_means_exhausted() { 383 | let exhausted = retries_exhausted(10, 5); 384 | 385 | assert!(exhausted) 386 | } 387 | 388 | #[test] 389 | fn failed_attempts_bigger_than_u8_means_exhausted() { 390 | let failed_attempts = u8::MAX as usize + 1; 391 | 392 | let exhausted = retries_exhausted(failed_attempts, u8::MAX); 393 | 394 | assert!(exhausted) 395 | } 396 | 397 | #[test] 398 | fn less_failed_attempts_means_not_exhausted() { 399 | let exhausted = retries_exhausted(2, 5); 400 | 401 | assert!(!exhausted) 402 | } 403 | 404 | #[test] 405 | fn attempts_equals_retries_means_not_exhausted_yet() { 406 | let exhausted = retries_exhausted(2, 2); 407 | 408 | assert!(!exhausted) 409 | } 410 | 411 | #[test] 412 | #[ignore] 413 | fn test_local_timeout() { 414 | // This test assumes a couple things: 415 | // - that `localhost` is resolved to two IP addresses, `127.0.0.1` and `::1` (with the v6 416 | // one having higher priority) 417 | // - that the system silently drops packets to `[::1]:60000` or a different port if 418 | // specified through `TEST_ELECTRUM_TIMEOUT_PORT` 419 | // 420 | // this can be setup with: ip6tables -I INPUT 1 -p tcp -d ::1 --dport 60000 -j DROP 421 | // and removed with: ip6tables -D INPUT -p tcp -d ::1 --dport 60000 -j DROP 422 | // 423 | // The test tries to create a client to `localhost` and expects it to succeed, but only 424 | // after at least 2 seconds have passed which is roughly the timeout time for the first 425 | // try. 426 | 427 | use std::net::TcpListener; 428 | use std::sync::mpsc::channel; 429 | use std::time::{Duration, Instant}; 430 | 431 | let endpoint = 432 | std::env::var("TEST_ELECTRUM_TIMEOUT_PORT").unwrap_or("localhost:60000".into()); 433 | let (sender, receiver) = channel(); 434 | 435 | std::thread::spawn(move || { 436 | let listener = TcpListener::bind("127.0.0.1:60000").unwrap(); 437 | sender.send(()).unwrap(); 438 | 439 | for _stream in listener.incoming() { 440 | std::thread::sleep(Duration::from_secs(60)) 441 | } 442 | }); 443 | 444 | receiver 445 | .recv_timeout(Duration::from_secs(5)) 446 | .expect("Can't start local listener"); 447 | 448 | let now = Instant::now(); 449 | let client = Client::from_config( 450 | &endpoint, 451 | crate::config::ConfigBuilder::new() 452 | .timeout(Some(Duration::from_secs(5))) 453 | .build(), 454 | ); 455 | let elapsed = now.elapsed(); 456 | 457 | assert!(client.is_ok()); 458 | assert!(elapsed > Duration::from_secs(2)); 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | //! Electrum APIs 2 | 3 | use std::borrow::Borrow; 4 | use std::convert::TryInto; 5 | use std::ops::Deref; 6 | 7 | use bitcoin::consensus::encode::{deserialize, serialize}; 8 | use bitcoin::{block, Script, Transaction, Txid}; 9 | 10 | use crate::batch::Batch; 11 | use crate::types::*; 12 | 13 | impl ElectrumApi for E 14 | where 15 | E::Target: ElectrumApi, 16 | { 17 | fn raw_call( 18 | &self, 19 | method_name: &str, 20 | params: impl IntoIterator, 21 | ) -> Result { 22 | (**self).raw_call(method_name, params) 23 | } 24 | 25 | fn batch_call(&self, batch: &Batch) -> Result, Error> { 26 | (**self).batch_call(batch) 27 | } 28 | 29 | fn block_headers_subscribe_raw(&self) -> Result { 30 | (**self).block_headers_subscribe_raw() 31 | } 32 | 33 | fn block_headers_pop_raw(&self) -> Result, Error> { 34 | (**self).block_headers_pop_raw() 35 | } 36 | 37 | fn block_header_raw(&self, height: usize) -> Result, Error> { 38 | (**self).block_header_raw(height) 39 | } 40 | 41 | fn block_headers(&self, start_height: usize, count: usize) -> Result { 42 | (**self).block_headers(start_height, count) 43 | } 44 | 45 | fn estimate_fee(&self, number: usize) -> Result { 46 | (**self).estimate_fee(number) 47 | } 48 | 49 | fn relay_fee(&self) -> Result { 50 | (**self).relay_fee() 51 | } 52 | 53 | fn script_subscribe(&self, script: &Script) -> Result, Error> { 54 | (**self).script_subscribe(script) 55 | } 56 | 57 | fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result>, Error> 58 | where 59 | I: IntoIterator + Clone, 60 | I::Item: Borrow<&'s Script>, 61 | { 62 | (**self).batch_script_subscribe(scripts) 63 | } 64 | 65 | fn script_unsubscribe(&self, script: &Script) -> Result { 66 | (**self).script_unsubscribe(script) 67 | } 68 | 69 | fn script_pop(&self, script: &Script) -> Result, Error> { 70 | (**self).script_pop(script) 71 | } 72 | 73 | fn script_get_balance(&self, script: &Script) -> Result { 74 | (**self).script_get_balance(script) 75 | } 76 | 77 | fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result, Error> 78 | where 79 | I: IntoIterator + Clone, 80 | I::Item: Borrow<&'s Script>, 81 | { 82 | (**self).batch_script_get_balance(scripts) 83 | } 84 | 85 | fn script_get_history(&self, script: &Script) -> Result, Error> { 86 | (**self).script_get_history(script) 87 | } 88 | 89 | fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result>, Error> 90 | where 91 | I: IntoIterator + Clone, 92 | I::Item: Borrow<&'s Script>, 93 | { 94 | (**self).batch_script_get_history(scripts) 95 | } 96 | 97 | fn script_list_unspent(&self, script: &Script) -> Result, Error> { 98 | (**self).script_list_unspent(script) 99 | } 100 | 101 | fn batch_script_list_unspent<'s, I>( 102 | &self, 103 | scripts: I, 104 | ) -> Result>, Error> 105 | where 106 | I: IntoIterator + Clone, 107 | I::Item: Borrow<&'s Script>, 108 | { 109 | (**self).batch_script_list_unspent(scripts) 110 | } 111 | 112 | fn transaction_get_raw(&self, txid: &Txid) -> Result, Error> { 113 | (**self).transaction_get_raw(txid) 114 | } 115 | 116 | fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result>, Error> 117 | where 118 | I: IntoIterator + Clone, 119 | I::Item: Borrow<&'t Txid>, 120 | { 121 | (**self).batch_transaction_get_raw(txids) 122 | } 123 | 124 | fn batch_block_header_raw(&self, heights: I) -> Result>, Error> 125 | where 126 | I: IntoIterator + Clone, 127 | I::Item: Borrow, 128 | { 129 | (**self).batch_block_header_raw(heights) 130 | } 131 | 132 | fn batch_estimate_fee(&self, numbers: I) -> Result, Error> 133 | where 134 | I: IntoIterator + Clone, 135 | I::Item: Borrow, 136 | { 137 | (**self).batch_estimate_fee(numbers) 138 | } 139 | 140 | fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result { 141 | (**self).transaction_broadcast_raw(raw_tx) 142 | } 143 | 144 | fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result { 145 | (**self).transaction_get_merkle(txid, height) 146 | } 147 | 148 | fn batch_transaction_get_merkle( 149 | &self, 150 | txids_and_heights: I, 151 | ) -> Result, Error> 152 | where 153 | I: IntoIterator + Clone, 154 | I::Item: Borrow<(Txid, usize)>, 155 | { 156 | (**self).batch_transaction_get_merkle(txids_and_heights) 157 | } 158 | 159 | fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result { 160 | (**self).txid_from_pos(height, tx_pos) 161 | } 162 | 163 | fn txid_from_pos_with_merkle( 164 | &self, 165 | height: usize, 166 | tx_pos: usize, 167 | ) -> Result { 168 | (**self).txid_from_pos_with_merkle(height, tx_pos) 169 | } 170 | 171 | fn server_features(&self) -> Result { 172 | (**self).server_features() 173 | } 174 | 175 | fn ping(&self) -> Result<(), Error> { 176 | (**self).ping() 177 | } 178 | 179 | #[cfg(feature = "debug-calls")] 180 | fn calls_made(&self) -> Result { 181 | (**self).calls_made() 182 | } 183 | } 184 | 185 | /// API calls exposed by an Electrum client 186 | pub trait ElectrumApi { 187 | /// Gets the block header for height `height`. 188 | fn block_header(&self, height: usize) -> Result { 189 | Ok(deserialize(&self.block_header_raw(height)?)?) 190 | } 191 | 192 | /// Subscribes to notifications for new block headers, by sending a `blockchain.headers.subscribe` call. 193 | fn block_headers_subscribe(&self) -> Result { 194 | self.block_headers_subscribe_raw()?.try_into() 195 | } 196 | 197 | /// Tries to pop one queued notification for a new block header that we might have received. 198 | /// Returns `None` if there are no items in the queue. 199 | fn block_headers_pop(&self) -> Result, Error> { 200 | self.block_headers_pop_raw()? 201 | .map(|raw| raw.try_into()) 202 | .transpose() 203 | } 204 | 205 | /// Gets the transaction with `txid`. Returns an error if not found. 206 | fn transaction_get(&self, txid: &Txid) -> Result { 207 | Ok(deserialize(&self.transaction_get_raw(txid)?)?) 208 | } 209 | 210 | /// Batch version of [`transaction_get`](#method.transaction_get). 211 | /// 212 | /// Takes a list of `txids` and returns a list of transactions. 213 | fn batch_transaction_get<'t, I>(&self, txids: I) -> Result, Error> 214 | where 215 | I: IntoIterator + Clone, 216 | I::Item: Borrow<&'t Txid>, 217 | { 218 | self.batch_transaction_get_raw(txids)? 219 | .iter() 220 | .map(|s| Ok(deserialize(s)?)) 221 | .collect() 222 | } 223 | 224 | /// Batch version of [`block_header`](#method.block_header). 225 | /// 226 | /// Takes a list of `heights` of blocks and returns a list of headers. 227 | fn batch_block_header(&self, heights: I) -> Result, Error> 228 | where 229 | I: IntoIterator + Clone, 230 | I::Item: Borrow, 231 | { 232 | self.batch_block_header_raw(heights)? 233 | .iter() 234 | .map(|s| Ok(deserialize(s)?)) 235 | .collect() 236 | } 237 | 238 | /// Broadcasts a transaction to the network. 239 | fn transaction_broadcast(&self, tx: &Transaction) -> Result { 240 | let buffer: Vec = serialize(tx); 241 | self.transaction_broadcast_raw(&buffer) 242 | } 243 | 244 | /// Executes the requested API call returning the raw answer. 245 | fn raw_call( 246 | &self, 247 | method_name: &str, 248 | params: impl IntoIterator, 249 | ) -> Result; 250 | 251 | /// Execute a queue of calls stored in a [`Batch`](../batch/struct.Batch.html) struct. Returns 252 | /// `Ok()` **only if** all of the calls are successful. The order of the JSON `Value`s returned 253 | /// reflects the order in which the calls were made on the `Batch` struct. 254 | fn batch_call(&self, batch: &Batch) -> Result, Error>; 255 | 256 | /// Subscribes to notifications for new block headers, by sending a `blockchain.headers.subscribe` call and 257 | /// returns the current tip as raw bytes instead of deserializing them. 258 | fn block_headers_subscribe_raw(&self) -> Result; 259 | 260 | /// Tries to pop one queued notification for a new block header that we might have received. 261 | /// Returns a the header in raw bytes if a notification is found in the queue, None otherwise. 262 | fn block_headers_pop_raw(&self) -> Result, Error>; 263 | 264 | /// Gets the raw bytes of block header for height `height`. 265 | fn block_header_raw(&self, height: usize) -> Result, Error>; 266 | 267 | /// Tries to fetch `count` block headers starting from `start_height`. 268 | fn block_headers(&self, start_height: usize, count: usize) -> Result; 269 | 270 | /// Estimates the fee required in **Bitcoin per kilobyte** to confirm a transaction in `number` blocks. 271 | fn estimate_fee(&self, number: usize) -> Result; 272 | 273 | /// Returns the minimum accepted fee by the server's node in **Bitcoin, not Satoshi**. 274 | fn relay_fee(&self) -> Result; 275 | 276 | /// Subscribes to notifications for activity on a specific *scriptPubKey*. 277 | /// 278 | /// Returns a [`ScriptStatus`](../types/type.ScriptStatus.html) when successful that represents 279 | /// the current status for the requested script. 280 | /// 281 | /// Returns [`Error::AlreadySubscribed`](../types/enum.Error.html#variant.AlreadySubscribed) if 282 | /// already subscribed to the script. 283 | fn script_subscribe(&self, script: &Script) -> Result, Error>; 284 | 285 | /// Batch version of [`script_subscribe`](#method.script_subscribe). 286 | /// 287 | /// Takes a list of scripts and returns a list of script status responses. 288 | /// 289 | /// Note you should pass a reference to a collection because otherwise an expensive clone is made 290 | fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result>, Error> 291 | where 292 | I: IntoIterator + Clone, 293 | I::Item: Borrow<&'s Script>; 294 | 295 | /// Subscribes to notifications for activity on a specific *scriptPubKey*. 296 | /// 297 | /// Returns a `bool` with the server response when successful. 298 | /// 299 | /// Returns [`Error::NotSubscribed`](../types/enum.Error.html#variant.NotSubscribed) if 300 | /// not subscribed to the script. 301 | fn script_unsubscribe(&self, script: &Script) -> Result; 302 | 303 | /// Tries to pop one queued notification for a the requested script. Returns `None` if there are no items in the queue. 304 | fn script_pop(&self, script: &Script) -> Result, Error>; 305 | 306 | /// Returns the balance for a *scriptPubKey*. 307 | fn script_get_balance(&self, script: &Script) -> Result; 308 | 309 | /// Batch version of [`script_get_balance`](#method.script_get_balance). 310 | /// 311 | /// Takes a list of scripts and returns a list of balance responses. 312 | fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result, Error> 313 | where 314 | I: IntoIterator + Clone, 315 | I::Item: Borrow<&'s Script>; 316 | 317 | /// Returns the history for a *scriptPubKey* 318 | fn script_get_history(&self, script: &Script) -> Result, Error>; 319 | 320 | /// Batch version of [`script_get_history`](#method.script_get_history). 321 | /// 322 | /// Takes a list of scripts and returns a list of history responses. 323 | fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result>, Error> 324 | where 325 | I: IntoIterator + Clone, 326 | I::Item: Borrow<&'s Script>; 327 | 328 | /// Returns the list of unspent outputs for a *scriptPubKey* 329 | fn script_list_unspent(&self, script: &Script) -> Result, Error>; 330 | 331 | /// Batch version of [`script_list_unspent`](#method.script_list_unspent). 332 | /// 333 | /// Takes a list of scripts and returns a list of a list of utxos. 334 | fn batch_script_list_unspent<'s, I>( 335 | &self, 336 | scripts: I, 337 | ) -> Result>, Error> 338 | where 339 | I: IntoIterator + Clone, 340 | I::Item: Borrow<&'s Script>; 341 | 342 | /// Gets the raw bytes of a transaction with `txid`. Returns an error if not found. 343 | fn transaction_get_raw(&self, txid: &Txid) -> Result, Error>; 344 | 345 | /// Batch version of [`transaction_get_raw`](#method.transaction_get_raw). 346 | /// 347 | /// Takes a list of `txids` and returns a list of transactions raw bytes. 348 | fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result>, Error> 349 | where 350 | I: IntoIterator + Clone, 351 | I::Item: Borrow<&'t Txid>; 352 | 353 | /// Batch version of [`block_header_raw`](#method.block_header_raw). 354 | /// 355 | /// Takes a list of `heights` of blocks and returns a list of block header raw bytes. 356 | fn batch_block_header_raw(&self, heights: I) -> Result>, Error> 357 | where 358 | I: IntoIterator + Clone, 359 | I::Item: Borrow; 360 | 361 | /// Batch version of [`estimate_fee`](#method.estimate_fee). 362 | /// 363 | /// Takes a list of `numbers` of blocks and returns a list of fee required in 364 | /// **Satoshis per kilobyte** to confirm a transaction in the given number of blocks. 365 | fn batch_estimate_fee(&self, numbers: I) -> Result, Error> 366 | where 367 | I: IntoIterator + Clone, 368 | I::Item: Borrow; 369 | 370 | /// Broadcasts the raw bytes of a transaction to the network. 371 | fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result; 372 | 373 | /// Returns the merkle path for the transaction `txid` confirmed in the block at `height`. 374 | fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result; 375 | 376 | /// Batch version of [`transaction_get_merkle`](#method.transaction_get_merkle). 377 | /// 378 | /// Take a list of `(txid, height)`, for transactions with `txid` confirmed in the block at `height`. 379 | fn batch_transaction_get_merkle( 380 | &self, 381 | txids_and_heights: I, 382 | ) -> Result, Error> 383 | where 384 | I: IntoIterator + Clone, 385 | I::Item: Borrow<(Txid, usize)>; 386 | 387 | /// Returns a transaction hash, given a block `height` and a `tx_pos` in the block. 388 | fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result; 389 | 390 | /// Returns a transaction hash and a merkle path, given a block `height` and a `tx_pos` in the 391 | /// block. 392 | fn txid_from_pos_with_merkle( 393 | &self, 394 | height: usize, 395 | tx_pos: usize, 396 | ) -> Result; 397 | 398 | /// Returns the capabilities of the server. 399 | fn server_features(&self) -> Result; 400 | 401 | /// Pings the server. This method can also be used as a "dummy" call to trigger the processing 402 | /// of incoming block header or script notifications. 403 | fn ping(&self) -> Result<(), Error>; 404 | 405 | #[cfg(feature = "debug-calls")] 406 | /// Returns the number of network calls made since the creation of the client. 407 | fn calls_made(&self) -> Result; 408 | } 409 | 410 | #[cfg(test)] 411 | mod test { 412 | use std::{borrow::Cow, sync::Arc}; 413 | 414 | use super::ElectrumApi; 415 | 416 | #[derive(Debug, Clone)] 417 | struct FakeApi; 418 | 419 | impl ElectrumApi for FakeApi { 420 | fn raw_call( 421 | &self, 422 | _: &str, 423 | _: impl IntoIterator, 424 | ) -> Result { 425 | unreachable!() 426 | } 427 | 428 | fn batch_call(&self, _: &crate::Batch) -> Result, super::Error> { 429 | unreachable!() 430 | } 431 | 432 | fn block_headers_subscribe_raw( 433 | &self, 434 | ) -> Result { 435 | unreachable!() 436 | } 437 | 438 | fn block_headers_pop_raw( 439 | &self, 440 | ) -> Result, super::Error> { 441 | unreachable!() 442 | } 443 | 444 | fn block_header_raw(&self, _: usize) -> Result, super::Error> { 445 | unreachable!() 446 | } 447 | 448 | fn block_headers(&self, _: usize, _: usize) -> Result { 449 | unreachable!() 450 | } 451 | 452 | fn estimate_fee(&self, _: usize) -> Result { 453 | unreachable!() 454 | } 455 | 456 | fn relay_fee(&self) -> Result { 457 | unreachable!() 458 | } 459 | 460 | fn script_subscribe( 461 | &self, 462 | _: &bitcoin::Script, 463 | ) -> Result, super::Error> { 464 | unreachable!() 465 | } 466 | 467 | fn batch_script_subscribe<'s, I>( 468 | &self, 469 | _: I, 470 | ) -> Result>, super::Error> 471 | where 472 | I: IntoIterator + Clone, 473 | I::Item: std::borrow::Borrow<&'s bitcoin::Script>, 474 | { 475 | unreachable!() 476 | } 477 | 478 | fn script_unsubscribe(&self, _: &bitcoin::Script) -> Result { 479 | unreachable!() 480 | } 481 | 482 | fn script_pop( 483 | &self, 484 | _: &bitcoin::Script, 485 | ) -> Result, super::Error> { 486 | unreachable!() 487 | } 488 | 489 | fn script_get_balance( 490 | &self, 491 | _: &bitcoin::Script, 492 | ) -> Result { 493 | unreachable!() 494 | } 495 | 496 | fn batch_script_get_balance<'s, I>( 497 | &self, 498 | _: I, 499 | ) -> Result, super::Error> 500 | where 501 | I: IntoIterator + Clone, 502 | I::Item: std::borrow::Borrow<&'s bitcoin::Script>, 503 | { 504 | unreachable!() 505 | } 506 | 507 | fn script_get_history( 508 | &self, 509 | _: &bitcoin::Script, 510 | ) -> Result, super::Error> { 511 | unreachable!() 512 | } 513 | 514 | fn batch_script_get_history<'s, I>( 515 | &self, 516 | _: I, 517 | ) -> Result>, super::Error> 518 | where 519 | I: IntoIterator + Clone, 520 | I::Item: std::borrow::Borrow<&'s bitcoin::Script>, 521 | { 522 | unreachable!() 523 | } 524 | 525 | fn script_list_unspent( 526 | &self, 527 | _: &bitcoin::Script, 528 | ) -> Result, super::Error> { 529 | unreachable!() 530 | } 531 | 532 | fn batch_script_list_unspent<'s, I>( 533 | &self, 534 | _: I, 535 | ) -> Result>, super::Error> 536 | where 537 | I: IntoIterator + Clone, 538 | I::Item: std::borrow::Borrow<&'s bitcoin::Script>, 539 | { 540 | unreachable!() 541 | } 542 | 543 | fn transaction_get_raw(&self, _: &bitcoin::Txid) -> Result, super::Error> { 544 | unreachable!() 545 | } 546 | 547 | fn batch_transaction_get_raw<'t, I>(&self, _: I) -> Result>, super::Error> 548 | where 549 | I: IntoIterator + Clone, 550 | I::Item: std::borrow::Borrow<&'t bitcoin::Txid>, 551 | { 552 | unreachable!() 553 | } 554 | 555 | fn batch_block_header_raw(&self, _: I) -> Result>, super::Error> 556 | where 557 | I: IntoIterator + Clone, 558 | I::Item: std::borrow::Borrow, 559 | { 560 | unreachable!() 561 | } 562 | 563 | fn batch_estimate_fee(&self, _: I) -> Result, super::Error> 564 | where 565 | I: IntoIterator + Clone, 566 | I::Item: std::borrow::Borrow, 567 | { 568 | unreachable!() 569 | } 570 | 571 | fn transaction_broadcast_raw(&self, _: &[u8]) -> Result { 572 | unreachable!() 573 | } 574 | 575 | fn transaction_get_merkle( 576 | &self, 577 | _: &bitcoin::Txid, 578 | _: usize, 579 | ) -> Result { 580 | unreachable!() 581 | } 582 | 583 | fn batch_transaction_get_merkle( 584 | &self, 585 | _: I, 586 | ) -> Result, crate::Error> 587 | where 588 | I: IntoIterator + Clone, 589 | I::Item: std::borrow::Borrow<(bitcoin::Txid, usize)>, 590 | { 591 | unreachable!() 592 | } 593 | 594 | fn txid_from_pos(&self, _: usize, _: usize) -> Result { 595 | unreachable!() 596 | } 597 | 598 | fn txid_from_pos_with_merkle( 599 | &self, 600 | _: usize, 601 | _: usize, 602 | ) -> Result { 603 | unreachable!() 604 | } 605 | 606 | fn server_features(&self) -> Result { 607 | unreachable!() 608 | } 609 | 610 | fn ping(&self) -> Result<(), super::Error> { 611 | unreachable!() 612 | } 613 | 614 | #[cfg(feature = "debug-calls")] 615 | fn calls_made(&self) -> Result { 616 | unreachable!() 617 | } 618 | } 619 | 620 | fn is_impl() {} 621 | 622 | #[test] 623 | fn deref() { 624 | is_impl::(); 625 | is_impl::<&FakeApi>(); 626 | is_impl::>(); 627 | is_impl::>(); 628 | is_impl::>(); 629 | } 630 | } 631 | -------------------------------------------------------------------------------- /src/socks/v5.rs: -------------------------------------------------------------------------------- 1 | use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; 2 | use std::cmp; 3 | use std::io::{self, Read, Write}; 4 | use std::net::{ 5 | Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6, TcpStream, ToSocketAddrs, UdpSocket, 6 | }; 7 | use std::ptr; 8 | use std::time::Duration; 9 | 10 | use super::writev::WritevExt; 11 | use super::{TargetAddr, ToTargetAddr}; 12 | 13 | const MAX_ADDR_LEN: usize = 260; 14 | 15 | fn read_addr(socket: &mut R) -> io::Result { 16 | match socket.read_u8()? { 17 | 1 => { 18 | let ip = Ipv4Addr::from(socket.read_u32::()?); 19 | let port = socket.read_u16::()?; 20 | Ok(TargetAddr::Ip(SocketAddr::V4(SocketAddrV4::new(ip, port)))) 21 | } 22 | 3 => { 23 | let len = socket.read_u8()?; 24 | let mut domain = vec![0; len as usize]; 25 | socket.read_exact(&mut domain)?; 26 | let domain = String::from_utf8(domain) 27 | .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; 28 | let port = socket.read_u16::()?; 29 | Ok(TargetAddr::Domain(domain, port)) 30 | } 31 | 4 => { 32 | let mut ip = [0; 16]; 33 | socket.read_exact(&mut ip)?; 34 | let ip = Ipv6Addr::from(ip); 35 | let port = socket.read_u16::()?; 36 | Ok(TargetAddr::Ip(SocketAddr::V6(SocketAddrV6::new( 37 | ip, port, 0, 0, 38 | )))) 39 | } 40 | _ => Err(io::Error::other("unsupported address type")), 41 | } 42 | } 43 | 44 | fn read_response(socket: &mut TcpStream) -> io::Result { 45 | if socket.read_u8()? != 5 { 46 | return Err(io::Error::new( 47 | io::ErrorKind::InvalidData, 48 | "invalid response version", 49 | )); 50 | } 51 | 52 | match socket.read_u8()? { 53 | 0 => {} 54 | 1 => return Err(io::Error::other("general SOCKS server failure")), 55 | 2 => return Err(io::Error::other("connection not allowed by ruleset")), 56 | 3 => return Err(io::Error::other("network unreachable")), 57 | 4 => return Err(io::Error::other("host unreachable")), 58 | 5 => return Err(io::Error::other("connection refused")), 59 | 6 => return Err(io::Error::other("TTL expired")), 60 | 7 => return Err(io::Error::other("command not supported")), 61 | 8 => return Err(io::Error::other("address kind not supported")), 62 | _ => return Err(io::Error::other("unknown error")), 63 | } 64 | 65 | if socket.read_u8()? != 0 { 66 | return Err(io::Error::new( 67 | io::ErrorKind::InvalidData, 68 | "invalid reserved byte", 69 | )); 70 | } 71 | 72 | read_addr(socket) 73 | } 74 | 75 | fn write_addr(mut packet: &mut [u8], target: &TargetAddr) -> io::Result { 76 | let start_len = packet.len(); 77 | match *target { 78 | TargetAddr::Ip(SocketAddr::V4(addr)) => { 79 | packet.write_u8(1).unwrap(); 80 | packet.write_u32::((*addr.ip()).into()).unwrap(); 81 | packet.write_u16::(addr.port()).unwrap(); 82 | } 83 | TargetAddr::Ip(SocketAddr::V6(addr)) => { 84 | packet.write_u8(4).unwrap(); 85 | packet.write_all(&addr.ip().octets()).unwrap(); 86 | packet.write_u16::(addr.port()).unwrap(); 87 | } 88 | TargetAddr::Domain(ref domain, port) => { 89 | packet.write_u8(3).unwrap(); 90 | if domain.len() > u8::MAX as usize { 91 | return Err(io::Error::new( 92 | io::ErrorKind::InvalidInput, 93 | "domain name too long", 94 | )); 95 | } 96 | packet.write_u8(domain.len() as u8).unwrap(); 97 | packet.write_all(domain.as_bytes()).unwrap(); 98 | packet.write_u16::(port).unwrap(); 99 | } 100 | } 101 | 102 | Ok(start_len - packet.len()) 103 | } 104 | 105 | /// Authentication methods 106 | #[derive(Debug)] 107 | enum Authentication<'a> { 108 | Password { 109 | username: &'a str, 110 | password: &'a str, 111 | }, 112 | None, 113 | } 114 | 115 | impl Authentication<'_> { 116 | fn id(&self) -> u8 { 117 | match *self { 118 | Authentication::Password { .. } => 2, 119 | Authentication::None => 0, 120 | } 121 | } 122 | 123 | fn is_no_auth(&self) -> bool { 124 | matches!(*self, Authentication::None) 125 | } 126 | } 127 | 128 | /// A SOCKS5 client. 129 | #[derive(Debug)] 130 | pub struct Socks5Stream { 131 | socket: TcpStream, 132 | proxy_addr: TargetAddr, 133 | } 134 | 135 | impl Socks5Stream { 136 | /// Connects to a target server through a SOCKS5 proxy. 137 | pub fn connect(proxy: T, target: U, timeout: Option) -> io::Result 138 | where 139 | T: ToSocketAddrs, 140 | U: ToTargetAddr, 141 | { 142 | Self::connect_raw(1, proxy, target, &Authentication::None, timeout) 143 | } 144 | 145 | /// Connects to a target server through a SOCKS5 proxy using given 146 | /// username and password. 147 | pub fn connect_with_password( 148 | proxy: T, 149 | target: U, 150 | username: &str, 151 | password: &str, 152 | timeout: Option, 153 | ) -> io::Result 154 | where 155 | T: ToSocketAddrs, 156 | U: ToTargetAddr, 157 | { 158 | let auth = Authentication::Password { username, password }; 159 | Self::connect_raw(1, proxy, target, &auth, timeout) 160 | } 161 | 162 | fn connect_raw( 163 | command: u8, 164 | proxy: T, 165 | target: U, 166 | auth: &Authentication, 167 | timeout: Option, 168 | ) -> io::Result 169 | where 170 | T: ToSocketAddrs, 171 | U: ToTargetAddr, 172 | { 173 | let mut socket = if let Some(timeout) = timeout { 174 | let addr = proxy.to_socket_addrs()?.next().unwrap(); 175 | TcpStream::connect_timeout(&addr, timeout)? 176 | } else { 177 | TcpStream::connect(proxy)? 178 | }; 179 | 180 | socket.set_read_timeout(timeout)?; 181 | socket.set_write_timeout(timeout)?; 182 | 183 | let target = target.to_target_addr()?; 184 | 185 | let packet_len = if auth.is_no_auth() { 3 } else { 4 }; 186 | let packet = [ 187 | 5, // protocol version 188 | if auth.is_no_auth() { 1 } else { 2 }, // method count 189 | auth.id(), // method 190 | 0, // no auth (always offered) 191 | ]; 192 | socket.write_all(&packet[..packet_len])?; 193 | 194 | let mut buf = [0; 2]; 195 | socket.read_exact(&mut buf)?; 196 | let response_version = buf[0]; 197 | let selected_method = buf[1]; 198 | 199 | if response_version != 5 { 200 | return Err(io::Error::new( 201 | io::ErrorKind::InvalidData, 202 | "invalid response version", 203 | )); 204 | } 205 | 206 | if selected_method == 0xff { 207 | return Err(io::Error::other("no acceptable auth methods")); 208 | } 209 | 210 | if selected_method != auth.id() && selected_method != Authentication::None.id() { 211 | return Err(io::Error::other("unknown auth method")); 212 | } 213 | 214 | match *auth { 215 | Authentication::Password { username, password } if selected_method == auth.id() => { 216 | Self::password_authentication(&mut socket, username, password)? 217 | } 218 | _ => (), 219 | } 220 | 221 | let mut packet = [0; MAX_ADDR_LEN + 3]; 222 | packet[0] = 5; // protocol version 223 | packet[1] = command; // command 224 | packet[2] = 0; // reserved 225 | let len = write_addr(&mut packet[3..], &target)?; 226 | socket.write_all(&packet[..len + 3])?; 227 | 228 | let proxy_addr = read_response(&mut socket)?; 229 | 230 | Ok(Socks5Stream { socket, proxy_addr }) 231 | } 232 | 233 | fn password_authentication( 234 | socket: &mut TcpStream, 235 | username: &str, 236 | password: &str, 237 | ) -> io::Result<()> { 238 | if username.is_empty() || username.len() > 255 { 239 | return Err(io::Error::new( 240 | io::ErrorKind::InvalidInput, 241 | "invalid username", 242 | )); 243 | }; 244 | if password.is_empty() || password.len() > 255 { 245 | return Err(io::Error::new( 246 | io::ErrorKind::InvalidInput, 247 | "invalid password", 248 | )); 249 | } 250 | 251 | let mut packet = [0; 515]; 252 | let packet_size = 3 + username.len() + password.len(); 253 | packet[0] = 1; // version 254 | packet[1] = username.len() as u8; 255 | packet[2..2 + username.len()].copy_from_slice(username.as_bytes()); 256 | packet[2 + username.len()] = password.len() as u8; 257 | packet[3 + username.len()..packet_size].copy_from_slice(password.as_bytes()); 258 | socket.write_all(&packet[..packet_size])?; 259 | 260 | let mut buf = [0; 2]; 261 | socket.read_exact(&mut buf)?; 262 | if buf[0] != 1 { 263 | return Err(io::Error::new( 264 | io::ErrorKind::InvalidData, 265 | "invalid response version", 266 | )); 267 | } 268 | if buf[1] != 0 { 269 | return Err(io::Error::new( 270 | io::ErrorKind::PermissionDenied, 271 | "password authentication failed", 272 | )); 273 | } 274 | 275 | Ok(()) 276 | } 277 | 278 | /// Returns the proxy-side address of the connection between the proxy and 279 | /// target server. 280 | pub fn proxy_addr(&self) -> &TargetAddr { 281 | &self.proxy_addr 282 | } 283 | 284 | /// Returns a shared reference to the inner `TcpStream`. 285 | pub fn get_ref(&self) -> &TcpStream { 286 | &self.socket 287 | } 288 | 289 | /// Returns a mutable reference to the inner `TcpStream`. 290 | pub fn get_mut(&mut self) -> &mut TcpStream { 291 | &mut self.socket 292 | } 293 | 294 | /// Consumes the `Socks5Stream`, returning the inner `TcpStream`. 295 | pub fn into_inner(self) -> TcpStream { 296 | self.socket 297 | } 298 | } 299 | 300 | impl Read for Socks5Stream { 301 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 302 | self.socket.read(buf) 303 | } 304 | } 305 | 306 | impl Read for &Socks5Stream { 307 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 308 | (&self.socket).read(buf) 309 | } 310 | } 311 | 312 | impl Write for Socks5Stream { 313 | fn write(&mut self, buf: &[u8]) -> io::Result { 314 | self.socket.write(buf) 315 | } 316 | 317 | fn flush(&mut self) -> io::Result<()> { 318 | self.socket.flush() 319 | } 320 | } 321 | 322 | impl Write for &Socks5Stream { 323 | fn write(&mut self, buf: &[u8]) -> io::Result { 324 | (&self.socket).write(buf) 325 | } 326 | 327 | fn flush(&mut self) -> io::Result<()> { 328 | (&self.socket).flush() 329 | } 330 | } 331 | 332 | /// A SOCKS5 BIND client. 333 | #[derive(Debug)] 334 | pub struct Socks5Listener(Socks5Stream); 335 | 336 | impl Socks5Listener { 337 | /// Initiates a BIND request to the specified proxy. 338 | /// 339 | /// The proxy will filter incoming connections based on the value of 340 | /// `target`. 341 | pub fn bind(proxy: T, target: U, timeout: Option) -> io::Result 342 | where 343 | T: ToSocketAddrs, 344 | U: ToTargetAddr, 345 | { 346 | Socks5Stream::connect_raw(2, proxy, target, &Authentication::None, timeout) 347 | .map(Socks5Listener) 348 | } 349 | /// Initiates a BIND request to the specified proxy using given username 350 | /// and password. 351 | /// 352 | /// The proxy will filter incoming connections based on the value of 353 | /// `target`. 354 | pub fn bind_with_password( 355 | proxy: T, 356 | target: U, 357 | username: &str, 358 | password: &str, 359 | timeout: Option, 360 | ) -> io::Result 361 | where 362 | T: ToSocketAddrs, 363 | U: ToTargetAddr, 364 | { 365 | let auth = Authentication::Password { username, password }; 366 | Socks5Stream::connect_raw(2, proxy, target, &auth, timeout).map(Socks5Listener) 367 | } 368 | 369 | /// The address of the proxy-side TCP listener. 370 | /// 371 | /// This should be forwarded to the remote process, which should open a 372 | /// connection to it. 373 | pub fn proxy_addr(&self) -> &TargetAddr { 374 | &self.0.proxy_addr 375 | } 376 | 377 | /// Waits for the remote process to connect to the proxy server. 378 | /// 379 | /// The value of `proxy_addr` should be forwarded to the remote process 380 | /// before this method is called. 381 | pub fn accept(mut self) -> io::Result { 382 | self.0.proxy_addr = read_response(&mut self.0.socket)?; 383 | Ok(self.0) 384 | } 385 | } 386 | 387 | /// A SOCKS5 UDP client. 388 | #[derive(Debug)] 389 | pub struct Socks5Datagram { 390 | socket: UdpSocket, 391 | // keeps the session alive 392 | stream: Socks5Stream, 393 | } 394 | 395 | impl Socks5Datagram { 396 | /// Creates a UDP socket bound to the specified address which will have its 397 | /// traffic routed through the specified proxy. 398 | pub fn bind(proxy: T, addr: U, timeout: Option) -> io::Result 399 | where 400 | T: ToSocketAddrs, 401 | U: ToSocketAddrs, 402 | { 403 | Self::bind_internal(proxy, addr, &Authentication::None, timeout) 404 | } 405 | /// Creates a UDP socket bound to the specified address which will have its 406 | /// traffic routed through the specified proxy. The given username and password 407 | /// is used to authenticate to the SOCKS proxy. 408 | pub fn bind_with_password( 409 | proxy: T, 410 | addr: U, 411 | username: &str, 412 | password: &str, 413 | timeout: Option, 414 | ) -> io::Result 415 | where 416 | T: ToSocketAddrs, 417 | U: ToSocketAddrs, 418 | { 419 | let auth = Authentication::Password { username, password }; 420 | Self::bind_internal(proxy, addr, &auth, timeout) 421 | } 422 | 423 | fn bind_internal( 424 | proxy: T, 425 | addr: U, 426 | auth: &Authentication, 427 | timeout: Option, 428 | ) -> io::Result 429 | where 430 | T: ToSocketAddrs, 431 | U: ToSocketAddrs, 432 | { 433 | // we don't know what our IP is from the perspective of the proxy, so 434 | // don't try to pass `addr` in here. 435 | let dst = TargetAddr::Ip(SocketAddr::V4(SocketAddrV4::new( 436 | Ipv4Addr::new(0, 0, 0, 0), 437 | 0, 438 | ))); 439 | let stream = Socks5Stream::connect_raw(3, proxy, dst, auth, timeout)?; 440 | 441 | let socket = UdpSocket::bind(addr)?; 442 | socket.connect(&stream.proxy_addr)?; 443 | 444 | Ok(Socks5Datagram { socket, stream }) 445 | } 446 | 447 | /// Like `UdpSocket::send_to`. 448 | /// 449 | /// # Note 450 | /// 451 | /// The SOCKS protocol inserts a header at the beginning of the message. The 452 | /// header will be 10 bytes for an IPv4 address, 22 bytes for an IPv6 453 | /// address, and 7 bytes plus the length of the domain for a domain address. 454 | pub fn send_to(&self, buf: &[u8], addr: A) -> io::Result 455 | where 456 | A: ToTargetAddr, 457 | { 458 | let addr = addr.to_target_addr()?; 459 | 460 | let mut header = [0; MAX_ADDR_LEN + 3]; 461 | // first two bytes are reserved at 0 462 | // third byte is the fragment id at 0 463 | let len = write_addr(&mut header[3..], &addr)?; 464 | 465 | self.socket.writev([&header[..len + 3], buf]) 466 | } 467 | 468 | /// Like `UdpSocket::recv_from`. 469 | pub fn recv_from(&self, buf: &mut [u8]) -> io::Result<(usize, TargetAddr)> { 470 | let mut header = [0; MAX_ADDR_LEN + 3]; 471 | let len = self.socket.readv([&mut header, buf])?; 472 | 473 | let overflow = len.saturating_sub(header.len()); 474 | 475 | let header_len = cmp::min(header.len(), len); 476 | let mut header = &mut &header[..header_len]; 477 | 478 | if header.read_u16::()? != 0 { 479 | return Err(io::Error::new( 480 | io::ErrorKind::InvalidData, 481 | "invalid reserved bytes", 482 | )); 483 | } 484 | if header.read_u8()? != 0 { 485 | return Err(io::Error::new( 486 | io::ErrorKind::InvalidData, 487 | "invalid fragment id", 488 | )); 489 | } 490 | let addr = read_addr(&mut header)?; 491 | 492 | unsafe { 493 | ptr::copy(buf.as_ptr(), buf.as_mut_ptr().add(header.len()), overflow); 494 | } 495 | buf[..header.len()].copy_from_slice(header); 496 | 497 | Ok((header.len() + overflow, addr)) 498 | } 499 | 500 | /// Returns the address of the proxy-side UDP socket through which all 501 | /// messages will be routed. 502 | pub fn proxy_addr(&self) -> &TargetAddr { 503 | &self.stream.proxy_addr 504 | } 505 | 506 | /// Returns a shared reference to the inner socket. 507 | pub fn get_ref(&self) -> &UdpSocket { 508 | &self.socket 509 | } 510 | 511 | /// Returns a mutable reference to the inner socket. 512 | pub fn get_mut(&mut self) -> &mut UdpSocket { 513 | &mut self.socket 514 | } 515 | } 516 | 517 | #[cfg(test)] 518 | mod test { 519 | use std::io::{Read, Write}; 520 | use std::net::{TcpStream, ToSocketAddrs, UdpSocket}; 521 | 522 | use super::*; 523 | 524 | const SOCKS_PROXY_NO_AUTH_ONLY: &str = "127.0.0.1:1080"; 525 | const SOCKS_PROXY_PASSWD_ONLY: &str = "127.0.0.1:1081"; 526 | 527 | #[test] 528 | #[ignore] 529 | fn google_no_auth() { 530 | let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap(); 531 | let socket = Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, addr, None).unwrap(); 532 | google(socket); 533 | } 534 | 535 | #[test] 536 | #[ignore] 537 | fn google_with_password() { 538 | let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap(); 539 | let socket = Socks5Stream::connect_with_password( 540 | SOCKS_PROXY_PASSWD_ONLY, 541 | addr, 542 | "testuser", 543 | "testpass", 544 | None, 545 | ) 546 | .unwrap(); 547 | google(socket); 548 | } 549 | 550 | fn google(mut socket: Socks5Stream) { 551 | socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); 552 | let mut result = vec![]; 553 | socket.read_to_end(&mut result).unwrap(); 554 | 555 | println!("{}", String::from_utf8_lossy(&result)); 556 | assert!(result.starts_with(b"HTTP/1.0")); 557 | assert!(result.ends_with(b"\r\n") || result.ends_with(b"")); 558 | } 559 | 560 | #[test] 561 | #[ignore] 562 | fn google_dns() { 563 | let mut socket = 564 | Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, "google.com:80", None).unwrap(); 565 | 566 | socket.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap(); 567 | let mut result = vec![]; 568 | socket.read_to_end(&mut result).unwrap(); 569 | 570 | println!("{}", String::from_utf8_lossy(&result)); 571 | assert!(result.starts_with(b"HTTP/1.0")); 572 | assert!(result.ends_with(b"\r\n") || result.ends_with(b"")); 573 | } 574 | 575 | #[test] 576 | #[ignore] 577 | fn bind_no_auth() { 578 | let addr = find_address(); 579 | let listener = Socks5Listener::bind(SOCKS_PROXY_NO_AUTH_ONLY, addr, None).unwrap(); 580 | bind(listener); 581 | } 582 | 583 | #[test] 584 | #[ignore] 585 | fn bind_with_password_supported_but_no_auth_used() { 586 | let addr = find_address(); 587 | let listener = Socks5Listener::bind_with_password( 588 | SOCKS_PROXY_NO_AUTH_ONLY, 589 | addr, 590 | "unused_and_invalid_username", 591 | "unused_and_invalid_password", 592 | None, 593 | ) 594 | .unwrap(); 595 | bind(listener); 596 | } 597 | 598 | #[test] 599 | #[ignore] 600 | fn bind_with_password() { 601 | let addr = find_address(); 602 | let listener = Socks5Listener::bind_with_password( 603 | "127.0.0.1:1081", 604 | addr, 605 | "testuser", 606 | "testpass", 607 | None, 608 | ) 609 | .unwrap(); 610 | bind(listener); 611 | } 612 | 613 | fn bind(listener: Socks5Listener) { 614 | let addr = listener.proxy_addr().clone(); 615 | let mut end = TcpStream::connect(addr).unwrap(); 616 | let mut conn = listener.accept().unwrap(); 617 | conn.write_all(b"hello world").unwrap(); 618 | drop(conn); 619 | let mut result = vec![]; 620 | end.read_to_end(&mut result).unwrap(); 621 | assert_eq!(result, b"hello world"); 622 | } 623 | 624 | // First figure out our local address that we'll be connecting from 625 | fn find_address() -> TargetAddr { 626 | let socket = 627 | Socks5Stream::connect(SOCKS_PROXY_NO_AUTH_ONLY, "google.com:80", None).unwrap(); 628 | socket.proxy_addr().to_owned() 629 | } 630 | 631 | #[test] 632 | #[ignore] 633 | fn associate_no_auth() { 634 | let socks = 635 | Socks5Datagram::bind(SOCKS_PROXY_NO_AUTH_ONLY, "127.0.0.1:15410", None).unwrap(); 636 | associate(socks, "127.0.0.1:15411"); 637 | } 638 | 639 | #[test] 640 | #[ignore] 641 | fn associate_with_password() { 642 | let socks = Socks5Datagram::bind_with_password( 643 | SOCKS_PROXY_PASSWD_ONLY, 644 | "127.0.0.1:15414", 645 | "testuser", 646 | "testpass", 647 | None, 648 | ) 649 | .unwrap(); 650 | associate(socks, "127.0.0.1:15415"); 651 | } 652 | 653 | fn associate(socks: Socks5Datagram, socket_addr: &str) { 654 | let socket = UdpSocket::bind(socket_addr).unwrap(); 655 | 656 | socks.send_to(b"hello world!", socket_addr).unwrap(); 657 | let mut buf = [0; 13]; 658 | let (len, addr) = socket.recv_from(&mut buf).unwrap(); 659 | assert_eq!(len, 12); 660 | assert_eq!(&buf[..12], b"hello world!"); 661 | 662 | socket.send_to(b"hello world!", addr).unwrap(); 663 | 664 | let len = socks.recv_from(&mut buf).unwrap().0; 665 | assert_eq!(len, 12); 666 | assert_eq!(&buf[..12], b"hello world!"); 667 | } 668 | 669 | #[test] 670 | #[ignore] 671 | fn associate_long() { 672 | let socks = 673 | Socks5Datagram::bind(SOCKS_PROXY_NO_AUTH_ONLY, "127.0.0.1:15412", None).unwrap(); 674 | let socket_addr = "127.0.0.1:15413"; 675 | let socket = UdpSocket::bind(socket_addr).unwrap(); 676 | 677 | let mut msg = vec![]; 678 | for i in 0..(MAX_ADDR_LEN + 100) { 679 | msg.push(i as u8); 680 | } 681 | 682 | socks.send_to(&msg, socket_addr).unwrap(); 683 | let mut buf = vec![0; msg.len() + 1]; 684 | let (len, addr) = socket.recv_from(&mut buf).unwrap(); 685 | assert_eq!(len, msg.len()); 686 | assert_eq!(msg, &buf[..msg.len()]); 687 | 688 | socket.send_to(&msg, addr).unwrap(); 689 | 690 | let mut buf = vec![0; msg.len() + 1]; 691 | let len = socks.recv_from(&mut buf).unwrap().0; 692 | assert_eq!(len, msg.len()); 693 | assert_eq!(msg, &buf[..msg.len()]); 694 | } 695 | 696 | #[test] 697 | #[ignore] 698 | fn incorrect_password() { 699 | let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap(); 700 | let err = Socks5Stream::connect_with_password( 701 | SOCKS_PROXY_PASSWD_ONLY, 702 | addr, 703 | "testuser", 704 | "invalid", 705 | None, 706 | ) 707 | .unwrap_err(); 708 | 709 | assert_eq!(err.kind(), io::ErrorKind::PermissionDenied); 710 | assert_eq!(err.to_string(), "password authentication failed"); 711 | } 712 | 713 | #[test] 714 | #[ignore] 715 | fn auth_method_not_supported() { 716 | let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap(); 717 | let err = Socks5Stream::connect(SOCKS_PROXY_PASSWD_ONLY, addr, None).unwrap_err(); 718 | 719 | assert_eq!(err.kind(), io::ErrorKind::Other); 720 | assert_eq!(err.to_string(), "no acceptable auth methods"); 721 | } 722 | 723 | #[test] 724 | #[ignore] 725 | fn username_and_password_length() { 726 | let addr = "google.com:80".to_socket_addrs().unwrap().next().unwrap(); 727 | 728 | let err = Socks5Stream::connect_with_password( 729 | SOCKS_PROXY_PASSWD_ONLY, 730 | addr, 731 | &string_of_size(1), 732 | &string_of_size(1), 733 | None, 734 | ) 735 | .unwrap_err(); 736 | assert_eq!(err.kind(), io::ErrorKind::PermissionDenied); 737 | assert_eq!(err.to_string(), "password authentication failed"); 738 | 739 | let err = Socks5Stream::connect_with_password( 740 | SOCKS_PROXY_PASSWD_ONLY, 741 | addr, 742 | &string_of_size(255), 743 | &string_of_size(255), 744 | None, 745 | ) 746 | .unwrap_err(); 747 | assert_eq!(err.kind(), io::ErrorKind::PermissionDenied); 748 | assert_eq!(err.to_string(), "password authentication failed"); 749 | 750 | let err = Socks5Stream::connect_with_password( 751 | SOCKS_PROXY_PASSWD_ONLY, 752 | addr, 753 | &string_of_size(0), 754 | &string_of_size(255), 755 | None, 756 | ) 757 | .unwrap_err(); 758 | assert_eq!(err.kind(), io::ErrorKind::InvalidInput); 759 | assert_eq!(err.to_string(), "invalid username"); 760 | 761 | let err = Socks5Stream::connect_with_password( 762 | SOCKS_PROXY_PASSWD_ONLY, 763 | addr, 764 | &string_of_size(256), 765 | &string_of_size(255), 766 | None, 767 | ) 768 | .unwrap_err(); 769 | assert_eq!(err.kind(), io::ErrorKind::InvalidInput); 770 | assert_eq!(err.to_string(), "invalid username"); 771 | 772 | let err = Socks5Stream::connect_with_password( 773 | SOCKS_PROXY_PASSWD_ONLY, 774 | addr, 775 | &string_of_size(255), 776 | &string_of_size(0), 777 | None, 778 | ) 779 | .unwrap_err(); 780 | assert_eq!(err.kind(), io::ErrorKind::InvalidInput); 781 | assert_eq!(err.to_string(), "invalid password"); 782 | 783 | let err = Socks5Stream::connect_with_password( 784 | SOCKS_PROXY_PASSWD_ONLY, 785 | addr, 786 | &string_of_size(255), 787 | &string_of_size(256), 788 | None, 789 | ) 790 | .unwrap_err(); 791 | assert_eq!(err.kind(), io::ErrorKind::InvalidInput); 792 | assert_eq!(err.to_string(), "invalid password"); 793 | } 794 | 795 | fn string_of_size(size: usize) -> String { 796 | (0..size).map(|_| 'x').collect() 797 | } 798 | } 799 | -------------------------------------------------------------------------------- /src/raw_client.rs: -------------------------------------------------------------------------------- 1 | //! Raw client 2 | //! 3 | //! This module contains the definition of the raw client that wraps the transport method 4 | 5 | use std::borrow::Borrow; 6 | use std::collections::{BTreeMap, HashMap, VecDeque}; 7 | use std::io::{BufRead, BufReader, Read, Write}; 8 | use std::mem::drop; 9 | use std::net::{TcpStream, ToSocketAddrs}; 10 | use std::sync::atomic::{AtomicUsize, Ordering}; 11 | use std::sync::mpsc::{channel, Receiver, Sender}; 12 | use std::sync::{Arc, Mutex, TryLockError}; 13 | use std::time::Duration; 14 | 15 | #[allow(unused_imports)] 16 | use log::{debug, error, info, trace, warn}; 17 | 18 | use bitcoin::consensus::encode::deserialize; 19 | use bitcoin::hex::{DisplayHex, FromHex}; 20 | use bitcoin::{Script, Txid}; 21 | 22 | #[cfg(feature = "use-openssl")] 23 | use openssl::ssl::{SslConnector, SslMethod, SslStream, SslVerifyMode}; 24 | 25 | #[cfg(all( 26 | any( 27 | feature = "default", 28 | feature = "use-rustls", 29 | feature = "use-rustls-ring" 30 | ), 31 | not(feature = "use-openssl") 32 | ))] 33 | use rustls::{ 34 | pki_types::ServerName, 35 | pki_types::{Der, TrustAnchor}, 36 | ClientConfig, ClientConnection, RootCertStore, StreamOwned, 37 | }; 38 | 39 | #[cfg(any(feature = "default", feature = "proxy"))] 40 | use crate::socks::{Socks5Stream, TargetAddr, ToTargetAddr}; 41 | 42 | use crate::stream::ClonableStream; 43 | 44 | use crate::api::ElectrumApi; 45 | use crate::batch::Batch; 46 | use crate::types::*; 47 | 48 | macro_rules! impl_batch_call { 49 | ( $self:expr, $data:expr, $call:ident ) => {{ 50 | impl_batch_call!($self, $data, $call, ) 51 | }}; 52 | 53 | ( $self:expr, $data:expr, $call:ident, apply_deref ) => {{ 54 | impl_batch_call!($self, $data, $call, *) 55 | }}; 56 | 57 | ( $self:expr, $data:expr, $call:ident, $($apply_deref:tt)? ) => {{ 58 | let mut batch = Batch::default(); 59 | for i in $data { 60 | batch.$call($($apply_deref)* i.borrow()); 61 | } 62 | 63 | let resp = $self.batch_call(&batch)?; 64 | let mut answer = Vec::new(); 65 | 66 | for x in resp { 67 | answer.push(serde_json::from_value(x)?); 68 | } 69 | 70 | Ok(answer) 71 | }}; 72 | } 73 | 74 | /// A trait for [`ToSocketAddrs`](https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html) that 75 | /// can also be turned into a domain. Used when an SSL client needs to validate the server's 76 | /// certificate. 77 | pub trait ToSocketAddrsDomain: ToSocketAddrs { 78 | /// Returns the domain, if present 79 | fn domain(&self) -> Option<&str> { 80 | None 81 | } 82 | } 83 | 84 | impl ToSocketAddrsDomain for &str { 85 | fn domain(&self) -> Option<&str> { 86 | self.split(':').next() 87 | } 88 | } 89 | 90 | impl ToSocketAddrsDomain for (&str, u16) { 91 | fn domain(&self) -> Option<&str> { 92 | self.0.domain() 93 | } 94 | } 95 | 96 | #[cfg(any(feature = "default", feature = "proxy"))] 97 | impl ToSocketAddrsDomain for TargetAddr { 98 | fn domain(&self) -> Option<&str> { 99 | match self { 100 | TargetAddr::Ip(_) => None, 101 | TargetAddr::Domain(domain, _) => Some(domain.as_str()), 102 | } 103 | } 104 | } 105 | 106 | macro_rules! impl_to_socket_addrs_domain { 107 | ( $ty:ty ) => { 108 | impl ToSocketAddrsDomain for $ty {} 109 | }; 110 | } 111 | 112 | impl_to_socket_addrs_domain!(std::net::SocketAddr); 113 | impl_to_socket_addrs_domain!(std::net::SocketAddrV4); 114 | impl_to_socket_addrs_domain!(std::net::SocketAddrV6); 115 | impl_to_socket_addrs_domain!((std::net::IpAddr, u16)); 116 | impl_to_socket_addrs_domain!((std::net::Ipv4Addr, u16)); 117 | impl_to_socket_addrs_domain!((std::net::Ipv6Addr, u16)); 118 | 119 | /// Instance of an Electrum client 120 | /// 121 | /// A `Client` maintains a constant connection with an Electrum server and exposes methods to 122 | /// interact with it. It can also subscribe and receive notifictations from the server about new 123 | /// blocks or activity on a specific *scriptPubKey*. 124 | /// 125 | /// The `Client` is modeled in such a way that allows the external caller to have full control over 126 | /// its functionality: no threads or tasks are spawned internally to monitor the state of the 127 | /// connection. 128 | /// 129 | /// More transport methods can be used by manually creating an instance of this struct with an 130 | /// arbitray `S` type. 131 | #[derive(Debug)] 132 | pub struct RawClient 133 | where 134 | S: Read + Write, 135 | { 136 | stream: Mutex>, 137 | buf_reader: Mutex>>, 138 | 139 | last_id: AtomicUsize, 140 | waiting_map: Mutex>>, 141 | 142 | headers: Mutex>, 143 | script_notifications: Mutex>>, 144 | 145 | #[cfg(feature = "debug-calls")] 146 | calls: AtomicUsize, 147 | } 148 | 149 | impl From for RawClient 150 | where 151 | S: Read + Write, 152 | { 153 | fn from(stream: S) -> Self { 154 | let stream: ClonableStream<_> = stream.into(); 155 | 156 | Self { 157 | buf_reader: Mutex::new(BufReader::new(stream.clone())), 158 | stream: Mutex::new(stream), 159 | 160 | last_id: AtomicUsize::new(0), 161 | waiting_map: Mutex::new(HashMap::new()), 162 | 163 | headers: Mutex::new(VecDeque::new()), 164 | script_notifications: Mutex::new(HashMap::new()), 165 | 166 | #[cfg(feature = "debug-calls")] 167 | calls: AtomicUsize::new(0), 168 | } 169 | } 170 | } 171 | 172 | /// Transport type used to establish a plaintext TCP connection with the server 173 | pub type ElectrumPlaintextStream = TcpStream; 174 | impl RawClient { 175 | /// Creates a new plaintext client and tries to connect to `socket_addr`. 176 | pub fn new( 177 | socket_addrs: A, 178 | timeout: Option, 179 | ) -> Result { 180 | let stream = match timeout { 181 | Some(timeout) => { 182 | let stream = connect_with_total_timeout(socket_addrs, timeout)?; 183 | stream.set_read_timeout(Some(timeout))?; 184 | stream.set_write_timeout(Some(timeout))?; 185 | stream 186 | } 187 | None => TcpStream::connect(socket_addrs)?, 188 | }; 189 | 190 | Ok(stream.into()) 191 | } 192 | } 193 | 194 | fn connect_with_total_timeout( 195 | socket_addrs: A, 196 | mut timeout: Duration, 197 | ) -> Result { 198 | // Use the same algorithm as curl: 1/2 on the first host, 1/4 on the second one, etc. 199 | // https://curl.se/mail/lib-2014-11/0164.html 200 | 201 | let mut errors = Vec::new(); 202 | 203 | let addrs = socket_addrs 204 | .to_socket_addrs()? 205 | .enumerate() 206 | .collect::>(); 207 | for (index, addr) in &addrs { 208 | if *index < addrs.len() - 1 { 209 | timeout = timeout.div_f32(2.0); 210 | } 211 | 212 | info!( 213 | "Trying to connect to {} (attempt {}/{}) with timeout {:?}", 214 | addr, 215 | index + 1, 216 | addrs.len(), 217 | timeout 218 | ); 219 | match TcpStream::connect_timeout(addr, timeout) { 220 | Ok(socket) => return Ok(socket), 221 | Err(e) => { 222 | warn!("Connection error: {:?}", e); 223 | errors.push(e.into()); 224 | } 225 | } 226 | } 227 | 228 | Err(Error::AllAttemptsErrored(errors)) 229 | } 230 | 231 | #[cfg(feature = "use-openssl")] 232 | /// Transport type used to establish an OpenSSL TLS encrypted/authenticated connection with the server 233 | pub type ElectrumSslStream = SslStream; 234 | #[cfg(feature = "use-openssl")] 235 | impl RawClient { 236 | /// Creates a new SSL client and tries to connect to `socket_addr`. Optionally, if 237 | /// `validate_domain` is `true`, validate the server's certificate. 238 | pub fn new_ssl( 239 | socket_addrs: A, 240 | validate_domain: bool, 241 | timeout: Option, 242 | ) -> Result { 243 | debug!( 244 | "new_ssl socket_addrs.domain():{:?} validate_domain:{} timeout:{:?}", 245 | socket_addrs.domain(), 246 | validate_domain, 247 | timeout 248 | ); 249 | if validate_domain { 250 | socket_addrs.domain().ok_or(Error::MissingDomain)?; 251 | } 252 | match timeout { 253 | Some(timeout) => { 254 | let stream = connect_with_total_timeout(socket_addrs.clone(), timeout)?; 255 | stream.set_read_timeout(Some(timeout))?; 256 | stream.set_write_timeout(Some(timeout))?; 257 | Self::new_ssl_from_stream(socket_addrs, validate_domain, stream) 258 | } 259 | None => { 260 | let stream = TcpStream::connect(socket_addrs.clone())?; 261 | Self::new_ssl_from_stream(socket_addrs, validate_domain, stream) 262 | } 263 | } 264 | } 265 | 266 | /// Create a new SSL client using an existing TcpStream 267 | pub fn new_ssl_from_stream( 268 | socket_addrs: A, 269 | validate_domain: bool, 270 | stream: TcpStream, 271 | ) -> Result { 272 | let mut builder = 273 | SslConnector::builder(SslMethod::tls()).map_err(Error::InvalidSslMethod)?; 274 | // TODO: support for certificate pinning 275 | if validate_domain { 276 | socket_addrs.domain().ok_or(Error::MissingDomain)?; 277 | } else { 278 | builder.set_verify(SslVerifyMode::NONE); 279 | } 280 | let connector = builder.build(); 281 | 282 | let domain = socket_addrs.domain().unwrap_or("NONE").to_string(); 283 | 284 | let stream = connector 285 | .connect(&domain, stream) 286 | .map_err(Error::SslHandshakeError)?; 287 | 288 | Ok(stream.into()) 289 | } 290 | } 291 | 292 | #[cfg(all( 293 | any( 294 | feature = "default", 295 | feature = "use-rustls", 296 | feature = "use-rustls-ring" 297 | ), 298 | not(feature = "use-openssl") 299 | ))] 300 | mod danger { 301 | use crate::raw_client::ServerName; 302 | use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified}; 303 | use rustls::crypto::CryptoProvider; 304 | use rustls::pki_types::{CertificateDer, UnixTime}; 305 | use rustls::DigitallySignedStruct; 306 | 307 | #[derive(Debug)] 308 | pub struct NoCertificateVerification(CryptoProvider); 309 | 310 | impl NoCertificateVerification { 311 | pub fn new(provider: CryptoProvider) -> Self { 312 | Self(provider) 313 | } 314 | } 315 | 316 | impl rustls::client::danger::ServerCertVerifier for NoCertificateVerification { 317 | fn verify_server_cert( 318 | &self, 319 | _end_entity: &CertificateDer<'_>, 320 | _intermediates: &[CertificateDer<'_>], 321 | _server_name: &ServerName<'_>, 322 | _ocsp: &[u8], 323 | _now: UnixTime, 324 | ) -> Result { 325 | Ok(ServerCertVerified::assertion()) 326 | } 327 | 328 | fn verify_tls12_signature( 329 | &self, 330 | _message: &[u8], 331 | _cert: &CertificateDer<'_>, 332 | _dss: &DigitallySignedStruct, 333 | ) -> Result { 334 | Ok(HandshakeSignatureValid::assertion()) 335 | } 336 | 337 | fn verify_tls13_signature( 338 | &self, 339 | _message: &[u8], 340 | _cert: &CertificateDer<'_>, 341 | _dss: &DigitallySignedStruct, 342 | ) -> Result { 343 | Ok(HandshakeSignatureValid::assertion()) 344 | } 345 | 346 | fn supported_verify_schemes(&self) -> Vec { 347 | self.0.signature_verification_algorithms.supported_schemes() 348 | } 349 | } 350 | } 351 | 352 | #[cfg(all( 353 | any( 354 | feature = "default", 355 | feature = "use-rustls", 356 | feature = "use-rustls-ring" 357 | ), 358 | not(feature = "use-openssl") 359 | ))] 360 | /// Transport type used to establish a Rustls TLS encrypted/authenticated connection with the server 361 | pub type ElectrumSslStream = StreamOwned; 362 | #[cfg(all( 363 | any( 364 | feature = "default", 365 | feature = "use-rustls", 366 | feature = "use-rustls-ring" 367 | ), 368 | not(feature = "use-openssl") 369 | ))] 370 | impl RawClient { 371 | /// Creates a new SSL client and tries to connect to `socket_addr`. Optionally, if 372 | /// `validate_domain` is `true`, validate the server's certificate. 373 | pub fn new_ssl( 374 | socket_addrs: A, 375 | validate_domain: bool, 376 | timeout: Option, 377 | ) -> Result { 378 | debug!( 379 | "new_ssl socket_addrs.domain():{:?} validate_domain:{} timeout:{:?}", 380 | socket_addrs.domain(), 381 | validate_domain, 382 | timeout 383 | ); 384 | if validate_domain { 385 | socket_addrs.domain().ok_or(Error::MissingDomain)?; 386 | } 387 | match timeout { 388 | Some(timeout) => { 389 | let stream = connect_with_total_timeout(socket_addrs.clone(), timeout)?; 390 | stream.set_read_timeout(Some(timeout))?; 391 | stream.set_write_timeout(Some(timeout))?; 392 | Self::new_ssl_from_stream(socket_addrs, validate_domain, stream) 393 | } 394 | None => { 395 | let stream = TcpStream::connect(socket_addrs.clone())?; 396 | Self::new_ssl_from_stream(socket_addrs, validate_domain, stream) 397 | } 398 | } 399 | } 400 | 401 | /// Create a new SSL client using an existing TcpStream 402 | pub fn new_ssl_from_stream( 403 | socket_addr: A, 404 | validate_domain: bool, 405 | tcp_stream: TcpStream, 406 | ) -> Result { 407 | use std::convert::TryFrom; 408 | 409 | if rustls::crypto::CryptoProvider::get_default().is_none() { 410 | // We install a crypto provider depending on the set feature. 411 | #[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))] 412 | rustls::crypto::CryptoProvider::install_default( 413 | rustls::crypto::aws_lc_rs::default_provider(), 414 | ) 415 | .map_err(|_| { 416 | Error::CouldNotCreateConnection(rustls::Error::General( 417 | "Failed to install CryptoProvider".to_string(), 418 | )) 419 | })?; 420 | 421 | #[cfg(feature = "use-rustls-ring")] 422 | rustls::crypto::CryptoProvider::install_default( 423 | rustls::crypto::ring::default_provider(), 424 | ) 425 | .map_err(|_| { 426 | Error::CouldNotCreateConnection(rustls::Error::General( 427 | "Failed to install CryptoProvider".to_string(), 428 | )) 429 | })?; 430 | } 431 | 432 | let builder = ClientConfig::builder(); 433 | 434 | let config = if validate_domain { 435 | socket_addr.domain().ok_or(Error::MissingDomain)?; 436 | 437 | let store = webpki_roots::TLS_SERVER_ROOTS 438 | .iter() 439 | .map(|t| TrustAnchor { 440 | subject: Der::from_slice(t.subject), 441 | subject_public_key_info: Der::from_slice(t.spki), 442 | name_constraints: t.name_constraints.map(Der::from_slice), 443 | }) 444 | .collect::(); 445 | 446 | // TODO: cert pinning 447 | builder.with_root_certificates(store).with_no_client_auth() 448 | } else { 449 | builder 450 | .dangerous() 451 | .with_custom_certificate_verifier(std::sync::Arc::new( 452 | #[cfg(all(feature = "use-rustls", not(feature = "use-rustls-ring")))] 453 | danger::NoCertificateVerification::new(rustls::crypto::aws_lc_rs::default_provider()), 454 | #[cfg(feature = "use-rustls-ring")] 455 | danger::NoCertificateVerification::new(rustls::crypto::ring::default_provider()), 456 | )) 457 | .with_no_client_auth() 458 | }; 459 | 460 | let domain = socket_addr.domain().unwrap_or("NONE").to_string(); 461 | let session = ClientConnection::new( 462 | std::sync::Arc::new(config), 463 | ServerName::try_from(domain.clone()) 464 | .map_err(|_| Error::InvalidDNSNameError(domain.clone()))?, 465 | ) 466 | .map_err(Error::CouldNotCreateConnection)?; 467 | let stream = StreamOwned::new(session, tcp_stream); 468 | 469 | Ok(stream.into()) 470 | } 471 | } 472 | 473 | #[cfg(any(feature = "default", feature = "proxy"))] 474 | /// Transport type used to establish a connection to a server through a socks proxy 475 | pub type ElectrumProxyStream = Socks5Stream; 476 | #[cfg(any(feature = "default", feature = "proxy"))] 477 | impl RawClient { 478 | /// Creates a new socks client and tries to connect to `target_addr` using `proxy_addr` as a 479 | /// socks proxy server. The DNS resolution of `target_addr`, if required, is done 480 | /// through the proxy. This allows to specify, for instance, `.onion` addresses. 481 | pub fn new_proxy( 482 | target_addr: T, 483 | proxy: &crate::Socks5Config, 484 | timeout: Option, 485 | ) -> Result { 486 | let mut stream = match proxy.credentials.as_ref() { 487 | Some(cred) => Socks5Stream::connect_with_password( 488 | &proxy.addr, 489 | target_addr, 490 | &cred.username, 491 | &cred.password, 492 | timeout, 493 | )?, 494 | None => Socks5Stream::connect(&proxy.addr, target_addr, timeout)?, 495 | }; 496 | stream.get_mut().set_read_timeout(timeout)?; 497 | stream.get_mut().set_write_timeout(timeout)?; 498 | 499 | Ok(stream.into()) 500 | } 501 | 502 | #[cfg(any( 503 | feature = "use-openssl", 504 | feature = "use-rustls", 505 | feature = "use-rustls-ring" 506 | ))] 507 | /// Creates a new TLS client that connects to `target_addr` using `proxy_addr` as a socks proxy 508 | /// server. The DNS resolution of `target_addr`, if required, is done through the proxy. This 509 | /// allows to specify, for instance, `.onion` addresses. 510 | pub fn new_proxy_ssl( 511 | target_addr: T, 512 | validate_domain: bool, 513 | proxy: &crate::Socks5Config, 514 | timeout: Option, 515 | ) -> Result, Error> { 516 | let target = target_addr.to_target_addr()?; 517 | 518 | let mut stream = match proxy.credentials.as_ref() { 519 | Some(cred) => Socks5Stream::connect_with_password( 520 | &proxy.addr, 521 | target_addr, 522 | &cred.username, 523 | &cred.password, 524 | timeout, 525 | )?, 526 | None => Socks5Stream::connect(&proxy.addr, target.clone(), timeout)?, 527 | }; 528 | stream.get_mut().set_read_timeout(timeout)?; 529 | stream.get_mut().set_write_timeout(timeout)?; 530 | 531 | RawClient::new_ssl_from_stream(target, validate_domain, stream.into_inner()) 532 | } 533 | } 534 | 535 | #[derive(Debug)] 536 | enum ChannelMessage { 537 | Response(serde_json::Value), 538 | WakeUp, 539 | Error(Arc), 540 | } 541 | 542 | impl RawClient { 543 | // TODO: to enable this we have to find a way to allow concurrent read and writes to the 544 | // underlying transport struct. This can be done pretty easily for TcpStream because it can be 545 | // split into a "read" and a "write" object, but it's not as trivial for other types. Without 546 | // such thing, this causes a deadlock, because the reader thread takes a lock on the 547 | // `ClonableStream` before other threads can send a request to the server. They will block 548 | // waiting for the reader to release the mutex, but this will never happen because the server 549 | // didn't receive any request, so it has nothing to send back. 550 | // pub fn reader_thread(&self) -> Result<(), Error> { 551 | // self._reader_thread(None).map(|_| ()) 552 | // } 553 | 554 | fn _reader_thread(&self, until_message: Option) -> Result { 555 | let mut raw_resp = String::new(); 556 | let resp = match self.buf_reader.try_lock() { 557 | Ok(mut reader) => { 558 | trace!( 559 | "Starting reader thread with `until_message` = {:?}", 560 | until_message 561 | ); 562 | 563 | if let Some(until_message) = until_message { 564 | // If we are trying to start a reader thread but the corresponding sender is 565 | // missing from the map, exit immediately. We might have already received a 566 | // response for that id, but we don't know it yet. Exiting here forces the 567 | // calling code to fallback to the sender-receiver method, and it should find 568 | // a message there waiting for it. 569 | if self.waiting_map.lock()?.get(&until_message).is_none() { 570 | return Err(Error::CouldntLockReader); 571 | } 572 | } 573 | 574 | // Loop over every message 575 | loop { 576 | raw_resp.clear(); 577 | 578 | if let Err(e) = reader.read_line(&mut raw_resp) { 579 | let error = Arc::new(e); 580 | for (_, s) in self.waiting_map.lock().unwrap().drain() { 581 | s.send(ChannelMessage::Error(error.clone()))?; 582 | } 583 | return Err(Error::SharedIOError(error)); 584 | } 585 | trace!("<== {}", raw_resp); 586 | 587 | let resp: serde_json::Value = serde_json::from_str(&raw_resp)?; 588 | 589 | // Normally there is and id, but it's missing for spontaneous notifications 590 | // from the server 591 | let resp_id = resp["id"] 592 | .as_str() 593 | .and_then(|s| s.parse().ok()) 594 | .or_else(|| resp["id"].as_u64().map(|i| i as usize)); 595 | match resp_id { 596 | Some(resp_id) if until_message == Some(resp_id) => { 597 | // We have a valid id and it's exactly the one we were waiting for! 598 | trace!( 599 | "Reader thread {} received a response for its request", 600 | resp_id 601 | ); 602 | 603 | // Remove ourselves from the "waiting map" 604 | let mut map = self.waiting_map.lock()?; 605 | map.remove(&resp_id); 606 | 607 | // If the map is not empty, we select a random thread to become the 608 | // new reader thread. 609 | if let Some(err) = map.values().find_map(|sender| { 610 | sender 611 | .send(ChannelMessage::WakeUp) 612 | .map_err(|err| { 613 | warn!("Unable to wake up a thread, trying some other"); 614 | err 615 | }) 616 | .err() 617 | }) { 618 | error!("All the threads has failed, giving up"); 619 | return Err(err)?; 620 | } 621 | 622 | break Ok(resp); 623 | } 624 | Some(resp_id) => { 625 | // We have an id, but it's not our response. Notify the thread and 626 | // move on 627 | trace!("Reader thread received response for {}", resp_id); 628 | 629 | if let Some(sender) = self.waiting_map.lock()?.remove(&resp_id) { 630 | sender.send(ChannelMessage::Response(resp))?; 631 | } else { 632 | warn!("Missing listener for {}", resp_id); 633 | } 634 | } 635 | None => { 636 | // No id, that's probably a notification. 637 | let mut resp = resp; 638 | 639 | if let Some(method) = resp["method"].take().as_str() { 640 | self.handle_notification(method, resp["params"].take())?; 641 | } else { 642 | warn!("Unexpected response: {:?}", resp); 643 | } 644 | } 645 | } 646 | } 647 | } 648 | Err(TryLockError::WouldBlock) => { 649 | // If we "WouldBlock" here it means that there's already a reader thread 650 | // running somewhere. 651 | Err(Error::CouldntLockReader) 652 | } 653 | Err(TryLockError::Poisoned(e)) => Err(e)?, 654 | }; 655 | 656 | let resp = resp?; 657 | if let Some(err) = resp.get("error") { 658 | Err(Error::Protocol(err.clone())) 659 | } else { 660 | Ok(resp) 661 | } 662 | } 663 | 664 | fn call(&self, req: Request) -> Result { 665 | // Add our listener to the map before we send the request, to make sure we don't get a 666 | // reply before the receiver is added 667 | let (sender, receiver) = channel(); 668 | self.waiting_map.lock()?.insert(req.id, sender); 669 | 670 | let mut raw = serde_json::to_vec(&req)?; 671 | trace!("==> {}", String::from_utf8_lossy(&raw)); 672 | 673 | raw.extend_from_slice(b"\n"); 674 | let mut stream = self.stream.lock()?; 675 | stream.write_all(&raw)?; 676 | stream.flush()?; 677 | drop(stream); // release the lock 678 | 679 | self.increment_calls(); 680 | 681 | let mut resp = match self.recv(&receiver, req.id) { 682 | Ok(resp) => resp, 683 | e @ Err(_) => { 684 | // In case of error our sender could still be left in the map, depending on where 685 | // the error happened. Just in case, try to remove it here 686 | self.waiting_map.lock()?.remove(&req.id); 687 | return e; 688 | } 689 | }; 690 | Ok(resp["result"].take()) 691 | } 692 | 693 | fn recv( 694 | &self, 695 | receiver: &Receiver, 696 | req_id: usize, 697 | ) -> Result { 698 | loop { 699 | // Try to take the lock on the reader. If we manage to do so, we'll become the reader 700 | // thread until we get our reponse 701 | match self._reader_thread(Some(req_id)) { 702 | Ok(response) => break Ok(response), 703 | Err(Error::CouldntLockReader) => { 704 | match receiver.recv()? { 705 | // Received our response, returning it 706 | ChannelMessage::Response(received) => break Ok(received), 707 | ChannelMessage::WakeUp => { 708 | // We have been woken up, this means that we should try becoming the 709 | // reader thread ourselves 710 | trace!("WakeUp for {}", req_id); 711 | 712 | continue; 713 | } 714 | ChannelMessage::Error(e) => { 715 | warn!("Received ChannelMessage::Error"); 716 | 717 | break Err(Error::SharedIOError(e)); 718 | } 719 | } 720 | } 721 | e @ Err(_) => break e, 722 | } 723 | } 724 | } 725 | 726 | fn handle_notification(&self, method: &str, result: serde_json::Value) -> Result<(), Error> { 727 | match method { 728 | "blockchain.headers.subscribe" => self.headers.lock()?.append( 729 | &mut serde_json::from_value::>(result)? 730 | .into_iter() 731 | .collect(), 732 | ), 733 | "blockchain.scripthash.subscribe" => { 734 | let unserialized: ScriptNotification = serde_json::from_value(result)?; 735 | let mut script_notifications = self.script_notifications.lock()?; 736 | 737 | let queue = script_notifications 738 | .get_mut(&unserialized.scripthash) 739 | .ok_or(Error::NotSubscribed(unserialized.scripthash))?; 740 | 741 | queue.push_back(unserialized.status); 742 | } 743 | _ => info!("received unknown notification for method `{}`", method), 744 | } 745 | 746 | Ok(()) 747 | } 748 | 749 | pub(crate) fn internal_raw_call_with_vec( 750 | &self, 751 | method_name: &str, 752 | params: Vec, 753 | ) -> Result { 754 | let req = Request::new_id( 755 | self.last_id.fetch_add(1, Ordering::SeqCst), 756 | method_name, 757 | params, 758 | ); 759 | let result = self.call(req)?; 760 | 761 | Ok(result) 762 | } 763 | 764 | #[inline] 765 | #[cfg(feature = "debug-calls")] 766 | fn increment_calls(&self) { 767 | self.calls.fetch_add(1, Ordering::SeqCst); 768 | } 769 | 770 | #[inline] 771 | #[cfg(not(feature = "debug-calls"))] 772 | fn increment_calls(&self) {} 773 | } 774 | 775 | impl ElectrumApi for RawClient { 776 | fn raw_call( 777 | &self, 778 | method_name: &str, 779 | params: impl IntoIterator, 780 | ) -> Result { 781 | self.internal_raw_call_with_vec(method_name, params.into_iter().collect()) 782 | } 783 | 784 | fn batch_call(&self, batch: &Batch) -> Result, Error> { 785 | let mut raw = Vec::new(); 786 | 787 | let mut missing_responses = Vec::new(); 788 | let mut answers = BTreeMap::new(); 789 | 790 | // Add our listener to the map before we send the request 791 | 792 | for (method, params) in batch.iter() { 793 | let req = Request::new_id( 794 | self.last_id.fetch_add(1, Ordering::SeqCst), 795 | method, 796 | params.to_vec(), 797 | ); 798 | // Add distinct channel to each request so when we remove our request id (and sender) from the waiting_map 799 | // we can be sure that the response gets sent to the correct channel in self.recv 800 | let (sender, receiver) = channel(); 801 | missing_responses.push((req.id, receiver)); 802 | 803 | self.waiting_map.lock()?.insert(req.id, sender); 804 | 805 | raw.append(&mut serde_json::to_vec(&req)?); 806 | raw.extend_from_slice(b"\n"); 807 | } 808 | 809 | if missing_responses.is_empty() { 810 | return Ok(vec![]); 811 | } 812 | 813 | trace!("==> {}", String::from_utf8_lossy(&raw)); 814 | 815 | let mut stream = self.stream.lock()?; 816 | stream.write_all(&raw)?; 817 | stream.flush()?; 818 | drop(stream); // release the lock 819 | 820 | self.increment_calls(); 821 | 822 | for (req_id, receiver) in missing_responses.iter() { 823 | match self.recv(receiver, *req_id) { 824 | Ok(mut resp) => answers.insert(req_id, resp["result"].take()), 825 | Err(e) => { 826 | // In case of error our sender could still be left in the map, depending on where 827 | // the error happened. Just in case, try to remove it here 828 | warn!("got error for req_id {}: {:?}", req_id, e); 829 | warn!("removing all waiting req of this batch"); 830 | let mut guard = self.waiting_map.lock()?; 831 | for (req_id, _) in missing_responses.iter() { 832 | guard.remove(req_id); 833 | } 834 | return Err(e); 835 | } 836 | }; 837 | } 838 | 839 | Ok(answers.into_values().collect()) 840 | } 841 | 842 | fn block_headers_subscribe_raw(&self) -> Result { 843 | let req = Request::new_id( 844 | self.last_id.fetch_add(1, Ordering::SeqCst), 845 | "blockchain.headers.subscribe", 846 | vec![], 847 | ); 848 | let value = self.call(req)?; 849 | 850 | Ok(serde_json::from_value(value)?) 851 | } 852 | 853 | fn block_headers_pop_raw(&self) -> Result, Error> { 854 | Ok(self.headers.lock()?.pop_front()) 855 | } 856 | 857 | fn block_header_raw(&self, height: usize) -> Result, Error> { 858 | let req = Request::new_id( 859 | self.last_id.fetch_add(1, Ordering::SeqCst), 860 | "blockchain.block.header", 861 | vec![Param::Usize(height)], 862 | ); 863 | let result = self.call(req)?; 864 | 865 | Ok(Vec::::from_hex( 866 | result 867 | .as_str() 868 | .ok_or_else(|| Error::InvalidResponse(result.clone()))?, 869 | )?) 870 | } 871 | 872 | fn block_headers(&self, start_height: usize, count: usize) -> Result { 873 | let req = Request::new_id( 874 | self.last_id.fetch_add(1, Ordering::SeqCst), 875 | "blockchain.block.headers", 876 | vec![Param::Usize(start_height), Param::Usize(count)], 877 | ); 878 | let result = self.call(req)?; 879 | 880 | let mut deserialized: GetHeadersRes = serde_json::from_value(result)?; 881 | for i in 0..deserialized.count { 882 | let (start, end) = (i * 80, (i + 1) * 80); 883 | deserialized 884 | .headers 885 | .push(deserialize(&deserialized.raw_headers[start..end])?); 886 | } 887 | deserialized.raw_headers.clear(); 888 | 889 | Ok(deserialized) 890 | } 891 | 892 | fn estimate_fee(&self, number: usize) -> Result { 893 | let req = Request::new_id( 894 | self.last_id.fetch_add(1, Ordering::SeqCst), 895 | "blockchain.estimatefee", 896 | vec![Param::Usize(number)], 897 | ); 898 | let result = self.call(req)?; 899 | 900 | result 901 | .as_f64() 902 | .ok_or_else(|| Error::InvalidResponse(result.clone())) 903 | } 904 | 905 | fn relay_fee(&self) -> Result { 906 | let req = Request::new_id( 907 | self.last_id.fetch_add(1, Ordering::SeqCst), 908 | "blockchain.relayfee", 909 | vec![], 910 | ); 911 | let result = self.call(req)?; 912 | 913 | result 914 | .as_f64() 915 | .ok_or_else(|| Error::InvalidResponse(result.clone())) 916 | } 917 | 918 | fn script_subscribe(&self, script: &Script) -> Result, Error> { 919 | let script_hash = script.to_electrum_scripthash(); 920 | let mut script_notifications = self.script_notifications.lock()?; 921 | 922 | if script_notifications.contains_key(&script_hash) { 923 | return Err(Error::AlreadySubscribed(script_hash)); 924 | } 925 | 926 | script_notifications.insert(script_hash, VecDeque::new()); 927 | drop(script_notifications); 928 | 929 | let req = Request::new_id( 930 | self.last_id.fetch_add(1, Ordering::SeqCst), 931 | "blockchain.scripthash.subscribe", 932 | vec![Param::String(script_hash.to_hex())], 933 | ); 934 | let value = self.call(req)?; 935 | 936 | Ok(serde_json::from_value(value)?) 937 | } 938 | 939 | fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result>, Error> 940 | where 941 | I: IntoIterator + Clone, 942 | I::Item: Borrow<&'s Script>, 943 | { 944 | { 945 | let mut script_notifications = self.script_notifications.lock()?; 946 | 947 | for script in scripts.clone() { 948 | let script_hash = script.borrow().to_electrum_scripthash(); 949 | if script_notifications.contains_key(&script_hash) { 950 | return Err(Error::AlreadySubscribed(script_hash)); 951 | } 952 | script_notifications.insert(script_hash, VecDeque::new()); 953 | } 954 | } 955 | impl_batch_call!(self, scripts, script_subscribe) 956 | } 957 | 958 | fn script_unsubscribe(&self, script: &Script) -> Result { 959 | let script_hash = script.to_electrum_scripthash(); 960 | let mut script_notifications = self.script_notifications.lock()?; 961 | 962 | if !script_notifications.contains_key(&script_hash) { 963 | return Err(Error::NotSubscribed(script_hash)); 964 | } 965 | 966 | let req = Request::new_id( 967 | self.last_id.fetch_add(1, Ordering::SeqCst), 968 | "blockchain.scripthash.unsubscribe", 969 | vec![Param::String(script_hash.to_hex())], 970 | ); 971 | let value = self.call(req)?; 972 | let answer = serde_json::from_value(value)?; 973 | 974 | script_notifications.remove(&script_hash); 975 | 976 | Ok(answer) 977 | } 978 | 979 | fn script_pop(&self, script: &Script) -> Result, Error> { 980 | let script_hash = script.to_electrum_scripthash(); 981 | 982 | match self.script_notifications.lock()?.get_mut(&script_hash) { 983 | None => Err(Error::NotSubscribed(script_hash)), 984 | Some(queue) => Ok(queue.pop_front()), 985 | } 986 | } 987 | 988 | fn script_get_balance(&self, script: &Script) -> Result { 989 | let params = vec![Param::String(script.to_electrum_scripthash().to_hex())]; 990 | let req = Request::new_id( 991 | self.last_id.fetch_add(1, Ordering::SeqCst), 992 | "blockchain.scripthash.get_balance", 993 | params, 994 | ); 995 | let result = self.call(req)?; 996 | 997 | Ok(serde_json::from_value(result)?) 998 | } 999 | fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result, Error> 1000 | where 1001 | I: IntoIterator + Clone, 1002 | I::Item: Borrow<&'s Script>, 1003 | { 1004 | impl_batch_call!(self, scripts, script_get_balance) 1005 | } 1006 | 1007 | fn script_get_history(&self, script: &Script) -> Result, Error> { 1008 | let params = vec![Param::String(script.to_electrum_scripthash().to_hex())]; 1009 | let req = Request::new_id( 1010 | self.last_id.fetch_add(1, Ordering::SeqCst), 1011 | "blockchain.scripthash.get_history", 1012 | params, 1013 | ); 1014 | let result = self.call(req)?; 1015 | 1016 | Ok(serde_json::from_value(result)?) 1017 | } 1018 | fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result>, Error> 1019 | where 1020 | I: IntoIterator + Clone, 1021 | I::Item: Borrow<&'s Script>, 1022 | { 1023 | impl_batch_call!(self, scripts, script_get_history) 1024 | } 1025 | 1026 | fn script_list_unspent(&self, script: &Script) -> Result, Error> { 1027 | let params = vec![Param::String(script.to_electrum_scripthash().to_hex())]; 1028 | let req = Request::new_id( 1029 | self.last_id.fetch_add(1, Ordering::SeqCst), 1030 | "blockchain.scripthash.listunspent", 1031 | params, 1032 | ); 1033 | let result = self.call(req)?; 1034 | let mut result: Vec = serde_json::from_value(result)?; 1035 | 1036 | // This should not be necessary, since the protocol documentation says that the txs should 1037 | // be "in blockchain order" (https://electrumx.readthedocs.io/en/latest/protocol-methods.html#blockchain-scripthash-listunspent). 1038 | // However, elects seems to be ignoring this at the moment, so we'll sort again here just 1039 | // to make sure the result is consistent. 1040 | result.sort_unstable_by_key(|k| (k.height, k.tx_pos)); 1041 | Ok(result) 1042 | } 1043 | 1044 | fn batch_script_list_unspent<'s, I>( 1045 | &self, 1046 | scripts: I, 1047 | ) -> Result>, Error> 1048 | where 1049 | I: IntoIterator + Clone, 1050 | I::Item: Borrow<&'s Script>, 1051 | { 1052 | impl_batch_call!(self, scripts, script_list_unspent) 1053 | } 1054 | 1055 | fn transaction_get_raw(&self, txid: &Txid) -> Result, Error> { 1056 | let params = vec![Param::String(format!("{:x}", txid))]; 1057 | let req = Request::new_id( 1058 | self.last_id.fetch_add(1, Ordering::SeqCst), 1059 | "blockchain.transaction.get", 1060 | params, 1061 | ); 1062 | let result = self.call(req)?; 1063 | 1064 | Ok(Vec::::from_hex( 1065 | result 1066 | .as_str() 1067 | .ok_or_else(|| Error::InvalidResponse(result.clone()))?, 1068 | )?) 1069 | } 1070 | 1071 | fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result>, Error> 1072 | where 1073 | I: IntoIterator + Clone, 1074 | I::Item: Borrow<&'t Txid>, 1075 | { 1076 | let txs_string: Result, Error> = impl_batch_call!(self, txids, transaction_get); 1077 | txs_string? 1078 | .iter() 1079 | .map(|s| Ok(Vec::::from_hex(s)?)) 1080 | .collect() 1081 | } 1082 | 1083 | fn batch_block_header_raw<'s, I>(&self, heights: I) -> Result>, Error> 1084 | where 1085 | I: IntoIterator + Clone, 1086 | I::Item: Borrow, 1087 | { 1088 | let headers_string: Result, Error> = 1089 | impl_batch_call!(self, heights, block_header, apply_deref); 1090 | headers_string? 1091 | .iter() 1092 | .map(|s| Ok(Vec::::from_hex(s)?)) 1093 | .collect() 1094 | } 1095 | 1096 | fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result, Error> 1097 | where 1098 | I: IntoIterator + Clone, 1099 | I::Item: Borrow, 1100 | { 1101 | impl_batch_call!(self, numbers, estimate_fee, apply_deref) 1102 | } 1103 | 1104 | fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result { 1105 | let params = vec![Param::String(raw_tx.to_lower_hex_string())]; 1106 | let req = Request::new_id( 1107 | self.last_id.fetch_add(1, Ordering::SeqCst), 1108 | "blockchain.transaction.broadcast", 1109 | params, 1110 | ); 1111 | let result = self.call(req)?; 1112 | 1113 | Ok(serde_json::from_value(result)?) 1114 | } 1115 | 1116 | fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result { 1117 | let params = vec![Param::String(format!("{:x}", txid)), Param::Usize(height)]; 1118 | let req = Request::new_id( 1119 | self.last_id.fetch_add(1, Ordering::SeqCst), 1120 | "blockchain.transaction.get_merkle", 1121 | params, 1122 | ); 1123 | let result = self.call(req)?; 1124 | 1125 | Ok(serde_json::from_value(result)?) 1126 | } 1127 | 1128 | fn batch_transaction_get_merkle( 1129 | &self, 1130 | txids_and_heights: I, 1131 | ) -> Result, Error> 1132 | where 1133 | I: IntoIterator + Clone, 1134 | I::Item: Borrow<(Txid, usize)>, 1135 | { 1136 | impl_batch_call!(self, txids_and_heights, transaction_get_merkle) 1137 | } 1138 | 1139 | fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result { 1140 | let params = vec![Param::Usize(height), Param::Usize(tx_pos)]; 1141 | let req = Request::new_id( 1142 | self.last_id.fetch_add(1, Ordering::SeqCst), 1143 | "blockchain.transaction.id_from_pos", 1144 | params, 1145 | ); 1146 | let result = self.call(req)?; 1147 | 1148 | Ok(serde_json::from_value(result)?) 1149 | } 1150 | 1151 | fn txid_from_pos_with_merkle( 1152 | &self, 1153 | height: usize, 1154 | tx_pos: usize, 1155 | ) -> Result { 1156 | let params = vec![ 1157 | Param::Usize(height), 1158 | Param::Usize(tx_pos), 1159 | Param::Bool(true), 1160 | ]; 1161 | let req = Request::new_id( 1162 | self.last_id.fetch_add(1, Ordering::SeqCst), 1163 | "blockchain.transaction.id_from_pos", 1164 | params, 1165 | ); 1166 | let result = self.call(req)?; 1167 | 1168 | Ok(serde_json::from_value(result)?) 1169 | } 1170 | 1171 | fn server_features(&self) -> Result { 1172 | let req = Request::new_id( 1173 | self.last_id.fetch_add(1, Ordering::SeqCst), 1174 | "server.features", 1175 | vec![], 1176 | ); 1177 | let result = self.call(req)?; 1178 | 1179 | Ok(serde_json::from_value(result)?) 1180 | } 1181 | 1182 | fn ping(&self) -> Result<(), Error> { 1183 | let req = Request::new_id( 1184 | self.last_id.fetch_add(1, Ordering::SeqCst), 1185 | "server.ping", 1186 | vec![], 1187 | ); 1188 | self.call(req)?; 1189 | 1190 | Ok(()) 1191 | } 1192 | 1193 | #[cfg(feature = "debug-calls")] 1194 | fn calls_made(&self) -> Result { 1195 | Ok(self.calls.load(Ordering::SeqCst)) 1196 | } 1197 | } 1198 | 1199 | #[cfg(test)] 1200 | mod test { 1201 | use std::str::FromStr; 1202 | 1203 | use crate::utils; 1204 | 1205 | use super::RawClient; 1206 | use crate::api::ElectrumApi; 1207 | 1208 | fn get_test_server() -> String { 1209 | std::env::var("TEST_ELECTRUM_SERVER").unwrap_or("electrum.blockstream.info:50001".into()) 1210 | } 1211 | 1212 | #[test] 1213 | fn test_server_features_simple() { 1214 | let client = RawClient::new(get_test_server(), None).unwrap(); 1215 | 1216 | let resp = client.server_features().unwrap(); 1217 | assert_eq!( 1218 | resp.genesis_hash, 1219 | [ 1220 | 0, 0, 0, 0, 0, 25, 214, 104, 156, 8, 90, 225, 101, 131, 30, 147, 79, 247, 99, 174, 1221 | 70, 162, 166, 193, 114, 179, 241, 182, 10, 140, 226, 111 1222 | ], 1223 | ); 1224 | assert_eq!(resp.hash_function, Some("sha256".into())); 1225 | assert_eq!(resp.pruning, None); 1226 | } 1227 | 1228 | #[test] 1229 | #[ignore = "depends on a live server"] 1230 | fn test_batch_response_ordering() { 1231 | // The electrum.blockstream.info:50001 node always sends back ordered responses which will make this always pass. 1232 | // However, many servers do not, so we use one of those servers for this test. 1233 | let client = RawClient::new("exs.dyshek.org:50001", None).unwrap(); 1234 | let heights: Vec = vec![1, 4, 8, 12, 222, 6666, 12]; 1235 | let result_times = [ 1236 | 1231469665, 1231470988, 1231472743, 1231474888, 1231770653, 1236456633, 1231474888, 1237 | ]; 1238 | // Check ordering 10 times. This usually fails within 5 if ordering is incorrect. 1239 | for _ in 0..10 { 1240 | let results = client.batch_block_header(&heights).unwrap(); 1241 | for (index, result) in results.iter().enumerate() { 1242 | assert_eq!(result_times[index], result.time); 1243 | } 1244 | } 1245 | } 1246 | 1247 | #[test] 1248 | fn test_relay_fee() { 1249 | let client = RawClient::new(get_test_server(), None).unwrap(); 1250 | 1251 | let resp = client.relay_fee().unwrap(); 1252 | assert!(resp > 0.0); 1253 | } 1254 | 1255 | #[test] 1256 | fn test_estimate_fee() { 1257 | let client = RawClient::new(get_test_server(), None).unwrap(); 1258 | 1259 | let resp = client.estimate_fee(10).unwrap(); 1260 | assert!(resp > 0.0); 1261 | } 1262 | 1263 | #[test] 1264 | fn test_block_header() { 1265 | let client = RawClient::new(get_test_server(), None).unwrap(); 1266 | 1267 | let resp = client.block_header(0).unwrap(); 1268 | assert_eq!(resp.version, bitcoin::block::Version::ONE); 1269 | assert_eq!(resp.time, 1231006505); 1270 | assert_eq!(resp.nonce, 0x7c2bac1d); 1271 | } 1272 | 1273 | #[test] 1274 | fn test_block_header_raw() { 1275 | let client = RawClient::new(get_test_server(), None).unwrap(); 1276 | 1277 | let resp = client.block_header_raw(0).unwrap(); 1278 | assert_eq!( 1279 | resp, 1280 | vec![ 1281 | 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1282 | 0, 0, 0, 0, 0, 0, 0, 0, 59, 163, 237, 253, 122, 123, 18, 178, 122, 199, 44, 62, 1283 | 103, 118, 143, 97, 127, 200, 27, 195, 136, 138, 81, 50, 58, 159, 184, 170, 75, 30, 1284 | 94, 74, 41, 171, 95, 73, 255, 255, 0, 29, 29, 172, 43, 124 1285 | ] 1286 | ); 1287 | } 1288 | 1289 | #[test] 1290 | fn test_block_headers() { 1291 | let client = RawClient::new(get_test_server(), None).unwrap(); 1292 | 1293 | let resp = client.block_headers(0, 4).unwrap(); 1294 | assert_eq!(resp.count, 4); 1295 | assert_eq!(resp.max, 2016); 1296 | assert_eq!(resp.headers.len(), 4); 1297 | 1298 | assert_eq!(resp.headers[0].time, 1231006505); 1299 | } 1300 | 1301 | #[test] 1302 | fn test_script_get_balance() { 1303 | use std::str::FromStr; 1304 | 1305 | let client = RawClient::new(get_test_server(), None).unwrap(); 1306 | 1307 | // Realistically nobody will ever spend from this address, so we can expect the balance to 1308 | // increase over time 1309 | let addr = bitcoin::Address::from_str("1CounterpartyXXXXXXXXXXXXXXXUWLpVr") 1310 | .unwrap() 1311 | .assume_checked(); 1312 | let resp = client.script_get_balance(&addr.script_pubkey()).unwrap(); 1313 | assert!(resp.confirmed >= 213091301265); 1314 | } 1315 | 1316 | #[test] 1317 | fn test_script_get_history() { 1318 | use std::str::FromStr; 1319 | 1320 | use bitcoin::Txid; 1321 | 1322 | let client = RawClient::new(get_test_server(), None).unwrap(); 1323 | 1324 | // Mt.Gox hack address 1325 | let addr = bitcoin::Address::from_str("1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF") 1326 | .unwrap() 1327 | .assume_checked(); 1328 | let resp = client.script_get_history(&addr.script_pubkey()).unwrap(); 1329 | 1330 | assert!(resp.len() >= 328); 1331 | assert_eq!( 1332 | resp[0].tx_hash, 1333 | Txid::from_str("e67a0550848b7932d7796aeea16ab0e48a5cfe81c4e8cca2c5b03e0416850114") 1334 | .unwrap() 1335 | ); 1336 | } 1337 | 1338 | #[test] 1339 | fn test_script_list_unspent() { 1340 | use bitcoin::Txid; 1341 | use std::str::FromStr; 1342 | 1343 | let client = RawClient::new(get_test_server(), None).unwrap(); 1344 | 1345 | // Peter todd's sha256 bounty address https://bitcointalk.org/index.php?topic=293382.0 1346 | let addr = bitcoin::Address::from_str("35Snmmy3uhaer2gTboc81ayCip4m9DT4ko") 1347 | .unwrap() 1348 | .assume_checked(); 1349 | let resp = client.script_list_unspent(&addr.script_pubkey()).unwrap(); 1350 | 1351 | assert!(resp.len() >= 9); 1352 | let txid = "397f12ee15f8a3d2ab25c0f6bb7d3c64d2038ca056af10dd8251b98ae0f076b0"; 1353 | let txid = Txid::from_str(txid).unwrap(); 1354 | let txs: Vec<_> = resp.iter().filter(|e| e.tx_hash == txid).collect(); 1355 | assert_eq!(txs.len(), 1); 1356 | assert_eq!(txs[0].value, 10000000); 1357 | assert_eq!(txs[0].height, 257674); 1358 | assert_eq!(txs[0].tx_pos, 1); 1359 | } 1360 | 1361 | #[test] 1362 | fn test_batch_script_list_unspent() { 1363 | use std::str::FromStr; 1364 | 1365 | let client = RawClient::new(get_test_server(), None).unwrap(); 1366 | 1367 | // Peter todd's sha256 bounty address https://bitcointalk.org/index.php?topic=293382.0 1368 | let script_1 = bitcoin::Address::from_str("35Snmmy3uhaer2gTboc81ayCip4m9DT4ko") 1369 | .unwrap() 1370 | .assume_checked() 1371 | .script_pubkey(); 1372 | 1373 | let resp = client 1374 | .batch_script_list_unspent(vec![script_1.as_script()]) 1375 | .unwrap(); 1376 | assert_eq!(resp.len(), 1); 1377 | assert!(resp[0].len() >= 9); 1378 | } 1379 | 1380 | #[test] 1381 | fn test_batch_estimate_fee() { 1382 | let client = RawClient::new(get_test_server(), None).unwrap(); 1383 | 1384 | let resp = client.batch_estimate_fee(vec![10, 20]).unwrap(); 1385 | assert_eq!(resp.len(), 2); 1386 | assert!(resp[0] > 0.0); 1387 | assert!(resp[1] > 0.0); 1388 | } 1389 | 1390 | #[test] 1391 | fn test_transaction_get() { 1392 | use bitcoin::{transaction, Txid}; 1393 | 1394 | let client = RawClient::new(get_test_server(), None).unwrap(); 1395 | 1396 | let resp = client 1397 | .transaction_get( 1398 | &Txid::from_str("cc2ca076fd04c2aeed6d02151c447ced3d09be6fb4d4ef36cb5ed4e7a3260566") 1399 | .unwrap(), 1400 | ) 1401 | .unwrap(); 1402 | assert_eq!(resp.version, transaction::Version::ONE); 1403 | assert_eq!(resp.lock_time.to_consensus_u32(), 0); 1404 | } 1405 | 1406 | #[test] 1407 | fn test_transaction_get_raw() { 1408 | use bitcoin::Txid; 1409 | 1410 | let client = RawClient::new(get_test_server(), None).unwrap(); 1411 | 1412 | let resp = client 1413 | .transaction_get_raw( 1414 | &Txid::from_str("cc2ca076fd04c2aeed6d02151c447ced3d09be6fb4d4ef36cb5ed4e7a3260566") 1415 | .unwrap(), 1416 | ) 1417 | .unwrap(); 1418 | assert_eq!( 1419 | resp, 1420 | vec![ 1421 | 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1422 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 255, 255, 255, 84, 3, 240, 156, 9, 27, 77, 1423 | 105, 110, 101, 100, 32, 98, 121, 32, 65, 110, 116, 80, 111, 111, 108, 49, 49, 57, 1424 | 174, 0, 111, 32, 7, 77, 101, 40, 250, 190, 109, 109, 42, 177, 148, 141, 80, 179, 1425 | 217, 145, 226, 160, 130, 29, 247, 67, 88, 237, 156, 37, 83, 175, 0, 199, 166, 31, 1426 | 151, 119, 28, 160, 172, 238, 16, 110, 4, 0, 0, 0, 0, 0, 0, 0, 203, 236, 0, 128, 36, 1427 | 97, 249, 5, 255, 255, 255, 255, 3, 84, 206, 172, 42, 0, 0, 0, 0, 25, 118, 169, 20, 1428 | 17, 219, 228, 140, 198, 182, 23, 249, 198, 173, 175, 77, 158, 213, 246, 37, 177, 1429 | 199, 203, 89, 136, 172, 0, 0, 0, 0, 0, 0, 0, 0, 38, 106, 36, 170, 33, 169, 237, 46, 1430 | 87, 139, 206, 44, 166, 198, 188, 147, 89, 55, 115, 69, 216, 233, 133, 221, 95, 144, 1431 | 199, 132, 33, 255, 166, 239, 165, 235, 96, 66, 142, 105, 140, 0, 0, 0, 0, 0, 0, 0, 1432 | 0, 38, 106, 36, 185, 225, 27, 109, 47, 98, 29, 126, 195, 244, 90, 94, 202, 137, 1433 | 211, 234, 106, 41, 76, 223, 58, 4, 46, 151, 48, 9, 88, 68, 112, 161, 41, 22, 17, 1434 | 30, 44, 170, 1, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1435 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 1436 | ] 1437 | ) 1438 | } 1439 | 1440 | #[test] 1441 | fn test_transaction_get_merkle() { 1442 | use bitcoin::Txid; 1443 | 1444 | let client = RawClient::new(get_test_server(), None).unwrap(); 1445 | 1446 | let txid = 1447 | Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d") 1448 | .unwrap(); 1449 | let resp = client.transaction_get_merkle(&txid, 630000).unwrap(); 1450 | assert_eq!(resp.block_height, 630000); 1451 | assert_eq!(resp.pos, 68); 1452 | assert_eq!(resp.merkle.len(), 12); 1453 | assert_eq!( 1454 | resp.merkle[0], 1455 | [ 1456 | 34, 65, 51, 64, 49, 139, 115, 189, 185, 246, 70, 225, 168, 193, 217, 195, 47, 66, 1457 | 179, 240, 153, 24, 114, 215, 144, 196, 212, 41, 39, 155, 246, 25 1458 | ] 1459 | ); 1460 | 1461 | // Check we can verify the merkle proof validity, but fail if we supply wrong data. 1462 | let block_header = client.block_header(resp.block_height).unwrap(); 1463 | assert!(utils::validate_merkle_proof( 1464 | &txid, 1465 | &block_header.merkle_root, 1466 | &resp 1467 | )); 1468 | 1469 | let mut fail_resp = resp.clone(); 1470 | fail_resp.pos = 13; 1471 | assert!(!utils::validate_merkle_proof( 1472 | &txid, 1473 | &block_header.merkle_root, 1474 | &fail_resp 1475 | )); 1476 | 1477 | let fail_block_header = client.block_header(resp.block_height + 1).unwrap(); 1478 | assert!(!utils::validate_merkle_proof( 1479 | &txid, 1480 | &fail_block_header.merkle_root, 1481 | &resp 1482 | )); 1483 | } 1484 | 1485 | #[test] 1486 | fn test_batch_transaction_get_merkle() { 1487 | use bitcoin::Txid; 1488 | 1489 | struct TestCase { 1490 | txid: Txid, 1491 | block_height: usize, 1492 | exp_pos: usize, 1493 | exp_bytes: [u8; 32], 1494 | } 1495 | 1496 | let client = RawClient::new(get_test_server(), None).unwrap(); 1497 | 1498 | let test_cases: Vec = vec![ 1499 | TestCase { 1500 | txid: Txid::from_str( 1501 | "1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d", 1502 | ) 1503 | .unwrap(), 1504 | block_height: 630000, 1505 | exp_pos: 68, 1506 | exp_bytes: [ 1507 | 34, 65, 51, 64, 49, 139, 115, 189, 185, 246, 70, 225, 168, 193, 217, 195, 47, 1508 | 66, 179, 240, 153, 24, 114, 215, 144, 196, 212, 41, 39, 155, 246, 25, 1509 | ], 1510 | }, 1511 | TestCase { 1512 | txid: Txid::from_str( 1513 | "70a8639bc9b743c0610d1231103a2f8e99f4a25670946b91f16c55a5373b37d1", 1514 | ) 1515 | .unwrap(), 1516 | block_height: 630001, 1517 | exp_pos: 25, 1518 | exp_bytes: [ 1519 | 169, 100, 34, 99, 168, 101, 25, 168, 184, 90, 77, 50, 151, 245, 130, 101, 193, 1520 | 229, 136, 128, 63, 110, 241, 19, 242, 59, 184, 137, 245, 249, 188, 110, 1521 | ], 1522 | }, 1523 | TestCase { 1524 | txid: Txid::from_str( 1525 | "a0db149ace545beabbd87a8d6b20ffd6aa3b5a50e58add49a3d435f898c272cf", 1526 | ) 1527 | .unwrap(), 1528 | block_height: 840000, 1529 | exp_pos: 0, 1530 | exp_bytes: [ 1531 | 43, 184, 95, 75, 0, 75, 230, 218, 84, 247, 102, 193, 124, 30, 133, 81, 135, 50, 1532 | 113, 18, 194, 49, 239, 47, 243, 94, 186, 208, 234, 103, 198, 158, 1533 | ], 1534 | }, 1535 | ]; 1536 | 1537 | let txids_and_heights: Vec<(Txid, usize)> = test_cases 1538 | .iter() 1539 | .map(|case| (case.txid, case.block_height)) 1540 | .collect(); 1541 | 1542 | let resp = client 1543 | .batch_transaction_get_merkle(&txids_and_heights) 1544 | .unwrap(); 1545 | 1546 | for (i, (res, test_case)) in resp.iter().zip(test_cases).enumerate() { 1547 | assert_eq!(res.block_height, test_case.block_height); 1548 | assert_eq!(res.pos, test_case.exp_pos); 1549 | assert_eq!(res.merkle.len(), 12); 1550 | assert_eq!(res.merkle[0], test_case.exp_bytes); 1551 | 1552 | // Check we can verify the merkle proof validity, but fail if we supply wrong data. 1553 | let block_header = client.block_header(res.block_height).unwrap(); 1554 | assert!(utils::validate_merkle_proof( 1555 | &txids_and_heights[i].0, 1556 | &block_header.merkle_root, 1557 | res 1558 | )); 1559 | 1560 | let mut fail_res = res.clone(); 1561 | fail_res.pos = 13; 1562 | assert!(!utils::validate_merkle_proof( 1563 | &txids_and_heights[i].0, 1564 | &block_header.merkle_root, 1565 | &fail_res 1566 | )); 1567 | 1568 | let fail_block_header = client.block_header(res.block_height + 1).unwrap(); 1569 | assert!(!utils::validate_merkle_proof( 1570 | &txids_and_heights[i].0, 1571 | &fail_block_header.merkle_root, 1572 | res 1573 | )); 1574 | } 1575 | } 1576 | 1577 | #[test] 1578 | fn test_txid_from_pos() { 1579 | use bitcoin::Txid; 1580 | 1581 | let client = RawClient::new(get_test_server(), None).unwrap(); 1582 | 1583 | let txid = 1584 | Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d") 1585 | .unwrap(); 1586 | let resp = client.txid_from_pos(630000, 68).unwrap(); 1587 | assert_eq!(resp, txid); 1588 | } 1589 | 1590 | #[test] 1591 | fn test_txid_from_pos_with_merkle() { 1592 | use bitcoin::Txid; 1593 | 1594 | let client = RawClient::new(get_test_server(), None).unwrap(); 1595 | 1596 | let txid = 1597 | Txid::from_str("1f7ff3c407f33eabc8bec7d2cc230948f2249ec8e591bcf6f971ca9366c8788d") 1598 | .unwrap(); 1599 | let resp = client.txid_from_pos_with_merkle(630000, 68).unwrap(); 1600 | assert_eq!(resp.tx_hash, txid); 1601 | assert_eq!( 1602 | resp.merkle[0], 1603 | [ 1604 | 34, 65, 51, 64, 49, 139, 115, 189, 185, 246, 70, 225, 168, 193, 217, 195, 47, 66, 1605 | 179, 240, 153, 24, 114, 215, 144, 196, 212, 41, 39, 155, 246, 25 1606 | ] 1607 | ); 1608 | } 1609 | 1610 | #[test] 1611 | fn test_ping() { 1612 | let client = RawClient::new(get_test_server(), None).unwrap(); 1613 | client.ping().unwrap(); 1614 | } 1615 | 1616 | #[test] 1617 | fn test_block_headers_subscribe() { 1618 | let client = RawClient::new(get_test_server(), None).unwrap(); 1619 | let resp = client.block_headers_subscribe().unwrap(); 1620 | 1621 | assert!(resp.height >= 639000); 1622 | } 1623 | 1624 | #[test] 1625 | fn test_script_subscribe() { 1626 | use std::str::FromStr; 1627 | 1628 | let client = RawClient::new(get_test_server(), None).unwrap(); 1629 | 1630 | // Mt.Gox hack address 1631 | let addr = bitcoin::Address::from_str("1FeexV6bAHb8ybZjqQMjJrcCrHGW9sb6uF") 1632 | .unwrap() 1633 | .assume_checked(); 1634 | 1635 | // Just make sure that the call returns Ok(something) 1636 | client.script_subscribe(&addr.script_pubkey()).unwrap(); 1637 | } 1638 | 1639 | #[test] 1640 | fn test_request_after_error() { 1641 | let client = RawClient::new(get_test_server(), None).unwrap(); 1642 | 1643 | assert!(client.transaction_broadcast_raw(&[0x00]).is_err()); 1644 | assert!(client.server_features().is_ok()); 1645 | } 1646 | 1647 | #[test] 1648 | fn test_raw_call() { 1649 | use crate::types::Param; 1650 | 1651 | let client = RawClient::new(get_test_server(), None).unwrap(); 1652 | 1653 | let params = vec![ 1654 | Param::String( 1655 | "cc2ca076fd04c2aeed6d02151c447ced3d09be6fb4d4ef36cb5ed4e7a3260566".to_string(), 1656 | ), 1657 | Param::Bool(false), 1658 | ]; 1659 | 1660 | let resp = client 1661 | .raw_call("blockchain.transaction.get", params) 1662 | .unwrap(); 1663 | 1664 | assert_eq!( 1665 | resp, 1666 | "01000000000101000000000000000000000000000000000000000000000000000\ 1667 | 0000000000000ffffffff5403f09c091b4d696e656420627920416e74506f6f6c3\ 1668 | 13139ae006f20074d6528fabe6d6d2ab1948d50b3d991e2a0821df74358ed9c255\ 1669 | 3af00c7a61f97771ca0acee106e0400000000000000cbec00802461f905fffffff\ 1670 | f0354ceac2a000000001976a91411dbe48cc6b617f9c6adaf4d9ed5f625b1c7cb5\ 1671 | 988ac0000000000000000266a24aa21a9ed2e578bce2ca6c6bc9359377345d8e98\ 1672 | 5dd5f90c78421ffa6efa5eb60428e698c0000000000000000266a24b9e11b6d2f6\ 1673 | 21d7ec3f45a5eca89d3ea6a294cdf3a042e973009584470a12916111e2caa01200\ 1674 | 000000000000000000000000000000000000000000000000000000000000000000\ 1675 | 00000" 1676 | ) 1677 | } 1678 | } 1679 | --------------------------------------------------------------------------------