Skip to main content

electrum_client/
client.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Electrum Client
4
5use std::{borrow::Borrow, sync::RwLock};
6
7use log::{info, warn};
8
9use bitcoin::{Script, Txid};
10
11use crate::api::ElectrumApi;
12use crate::batch::Batch;
13use crate::config::Config;
14use crate::raw_client::*;
15use crate::types::*;
16use std::convert::TryFrom;
17
18/// Generalized Electrum client that supports multiple backends. This wraps
19/// [`RawClient`](client/struct.RawClient.html) and provides a more user-friendly
20/// constructor that can choose the right backend based on the url prefix.
21///
22/// **Note the `Socks5` client type requires the `proxy` feature to be enabled.**
23pub enum ClientType {
24    #[allow(missing_docs)]
25    TCP(RawClient<ElectrumPlaintextStream>),
26    #[allow(missing_docs)]
27    #[cfg(any(feature = "openssl", feature = "rustls", feature = "rustls-ring"))]
28    SSL(RawClient<ElectrumSslStream>),
29    #[allow(missing_docs)]
30    #[cfg(feature = "proxy")]
31    Socks5(RawClient<ElectrumProxyStream>),
32}
33
34/// Generalized Electrum client that supports multiple backends. Can re-instantiate client_type if connections
35/// drops
36pub struct Client {
37    client_type: RwLock<ClientType>,
38    config: Config,
39    url: String,
40}
41
42macro_rules! impl_inner_call {
43    ( $self:expr, $name:ident $(, $args:expr)* ) => {
44    {
45        let mut errors = vec![];
46        loop {
47            let read_client = $self.client_type.read().unwrap();
48            let res = match &*read_client {
49                ClientType::TCP(inner) => inner.$name( $($args, )* ),
50                #[cfg(any(feature = "openssl", feature = "rustls", feature = "rustls-ring"))]
51                ClientType::SSL(inner) => inner.$name( $($args, )* ),
52                #[cfg(feature = "proxy")]
53                ClientType::Socks5(inner) => inner.$name( $($args, )* ),
54            };
55            drop(read_client);
56            match res {
57                Ok(val) => return Ok(val),
58                Err(Error::Protocol(_) | Error::AlreadySubscribed(_)) => {
59                    return res;
60                },
61                Err(e) => {
62                    let failed_attempts = errors.len() + 1;
63
64                    if retries_exhausted(failed_attempts, $self.config.retry()) {
65                        warn!("call '{}' failed after {} attempts", stringify!($name), failed_attempts);
66                        return Err(Error::AllAttemptsErrored(errors));
67                    }
68
69                    warn!("call '{}' failed with {}, retry: {}/{}", stringify!($name), e, failed_attempts, $self.config.retry());
70
71                    errors.push(e);
72
73                    // Only one thread will try to recreate the client getting the write lock,
74                    // other eventual threads will get Err and will block at the beginning of
75                    // previous loop when trying to read()
76                    if let Ok(mut write_client) = $self.client_type.try_write() {
77                        loop {
78                            std::thread::sleep(std::time::Duration::from_secs((1 << errors.len()).min(30) as u64));
79                            match ClientType::from_config(&$self.url, &$self.config) {
80                                Ok(new_client) => {
81                                    info!("Succesfully created new client");
82                                    *write_client = new_client;
83                                    break;
84                                },
85                                Err(e) => {
86                                    let failed_attempts = errors.len() + 1;
87
88                                    if retries_exhausted(failed_attempts, $self.config.retry()) {
89                                        warn!("re-creating client failed after {} attempts", failed_attempts);
90                                        return Err(Error::AllAttemptsErrored(errors));
91                                    }
92
93                                    warn!("re-creating client failed with {}, retry: {}/{}", e, failed_attempts, $self.config.retry());
94
95                                    errors.push(e);
96                                }
97                            }
98                        }
99                    }
100                },
101            }
102        }}
103    }
104}
105
106fn retries_exhausted(failed_attempts: usize, configured_retries: u8) -> bool {
107    match u8::try_from(failed_attempts) {
108        Ok(failed_attempts) => failed_attempts > configured_retries,
109        Err(_) => true, // if the usize doesn't fit into a u8, we definitely exhausted our retries
110    }
111}
112
113impl ClientType {
114    /// Constructor that supports multiple backends and allows configuration through
115    /// the [Config]
116    pub fn from_config(url: &str, config: &Config) -> Result<Self, Error> {
117        let auth_provider = config.authorization_provider().cloned();
118
119        #[cfg(any(feature = "openssl", feature = "rustls", feature = "rustls-ring"))]
120        if url.starts_with("ssl://") {
121            let url = url.replacen("ssl://", "", 1);
122            #[cfg(feature = "proxy")]
123            let raw_client = match config.socks5() {
124                Some(socks5) => RawClient::new_proxy_ssl(
125                    url.as_str(),
126                    config.validate_domain(),
127                    socks5,
128                    config.timeout(),
129                    auth_provider,
130                )?,
131                None => RawClient::new_ssl(
132                    url.as_str(),
133                    config.validate_domain(),
134                    config.timeout(),
135                    auth_provider,
136                )?,
137            };
138            #[cfg(not(feature = "proxy"))]
139            let raw_client = RawClient::new_ssl(
140                url.as_str(),
141                config.validate_domain(),
142                config.timeout(),
143                auth_provider,
144            )?;
145
146            return Ok(ClientType::SSL(raw_client));
147        }
148
149        #[cfg(not(any(feature = "openssl", feature = "rustls", feature = "rustls-ring")))]
150        if url.starts_with("ssl://") {
151            return Err(Error::Message(
152                "SSL connections require one of the following features to be enabled: openssl, rustls, or rustls-ring".to_string()
153            ));
154        }
155
156        {
157            let url = url.replacen("tcp://", "", 1);
158
159            #[cfg(feature = "proxy")]
160            let client = match config.socks5() {
161                Some(socks5) => ClientType::Socks5(RawClient::new_proxy(
162                    url.as_str(),
163                    socks5,
164                    config.timeout(),
165                    auth_provider,
166                )?),
167                None => ClientType::TCP(RawClient::new(
168                    url.as_str(),
169                    config.timeout(),
170                    auth_provider,
171                )?),
172            };
173
174            #[cfg(not(feature = "proxy"))]
175            let client = ClientType::TCP(RawClient::new(
176                url.as_str(),
177                config.timeout(),
178                auth_provider,
179            )?);
180
181            Ok(client)
182        }
183    }
184}
185
186impl Client {
187    /// Default constructor supporting multiple backends by providing a prefix
188    ///
189    /// Supported prefixes are:
190    /// - tcp:// for a TCP plaintext client.
191    /// - ssl:// for an SSL-encrypted client. The server certificate will be verified.
192    ///
193    /// If no prefix is specified, then `tcp://` is assumed.
194    ///
195    /// See [Client::from_config] for more configuration options
196    pub fn new(url: &str) -> Result<Self, Error> {
197        Self::from_config(url, Config::default())
198    }
199
200    /// Generic constructor that supports multiple backends and allows configuration through
201    /// the [Config]
202    pub fn from_config(url: &str, config: Config) -> Result<Self, Error> {
203        let client_type = RwLock::new(ClientType::from_config(url, &config)?);
204
205        Ok(Client {
206            client_type,
207            config,
208            url: url.to_string(),
209        })
210    }
211}
212
213impl ElectrumApi for Client {
214    #[inline]
215    fn raw_call(
216        &self,
217        method_name: &str,
218        params: impl IntoIterator<Item = Param>,
219    ) -> Result<serde_json::Value, Error> {
220        // We can't passthrough this method to the inner client because it would require the
221        // `params` argument to also be `Copy` (because it's used multiple times for multiple
222        // retries). To avoid adding this extra trait bound we instead re-direct this call to the internal
223        // `RawClient::internal_raw_call_with_vec` method.
224
225        let vec = params.into_iter().collect::<Vec<Param>>();
226        impl_inner_call!(self, internal_raw_call_with_vec, method_name, vec.clone());
227    }
228
229    #[inline]
230    fn batch_call(&self, batch: &Batch) -> Result<Vec<serde_json::Value>, Error> {
231        impl_inner_call!(self, batch_call, batch)
232    }
233
234    #[inline]
235    fn block_headers_subscribe_raw(&self) -> Result<RawHeaderNotification, Error> {
236        impl_inner_call!(self, block_headers_subscribe_raw)
237    }
238
239    #[inline]
240    fn block_headers_pop_raw(&self) -> Result<Option<RawHeaderNotification>, Error> {
241        impl_inner_call!(self, block_headers_pop_raw)
242    }
243
244    #[inline]
245    fn block_header_raw(&self, height: usize) -> Result<Vec<u8>, Error> {
246        impl_inner_call!(self, block_header_raw, height)
247    }
248
249    #[inline]
250    fn block_headers(&self, start_height: usize, count: usize) -> Result<GetHeadersRes, Error> {
251        impl_inner_call!(self, block_headers, start_height, count)
252    }
253
254    #[inline]
255    fn estimate_fee(&self, number: usize, mode: Option<EstimationMode>) -> Result<f64, Error> {
256        impl_inner_call!(self, estimate_fee, number, mode)
257    }
258
259    #[inline]
260    fn relay_fee(&self) -> Result<f64, Error> {
261        impl_inner_call!(self, relay_fee)
262    }
263
264    #[inline]
265    fn script_subscribe(&self, script: &Script) -> Result<Option<ScriptStatus>, Error> {
266        impl_inner_call!(self, script_subscribe, script)
267    }
268
269    #[inline]
270    fn batch_script_subscribe<'s, I>(&self, scripts: I) -> Result<Vec<Option<ScriptStatus>>, Error>
271    where
272        I: IntoIterator + Clone,
273        I::Item: Borrow<&'s Script>,
274    {
275        impl_inner_call!(self, batch_script_subscribe, scripts.clone())
276    }
277
278    #[inline]
279    fn script_unsubscribe(&self, script: &Script) -> Result<bool, Error> {
280        impl_inner_call!(self, script_unsubscribe, script)
281    }
282
283    #[inline]
284    fn script_pop(&self, script: &Script) -> Result<Option<ScriptStatus>, Error> {
285        impl_inner_call!(self, script_pop, script)
286    }
287
288    #[inline]
289    fn script_get_balance(&self, script: &Script) -> Result<GetBalanceRes, Error> {
290        impl_inner_call!(self, script_get_balance, script)
291    }
292
293    #[inline]
294    fn batch_script_get_balance<'s, I>(&self, scripts: I) -> Result<Vec<GetBalanceRes>, Error>
295    where
296        I: IntoIterator + Clone,
297        I::Item: Borrow<&'s Script>,
298    {
299        impl_inner_call!(self, batch_script_get_balance, scripts.clone())
300    }
301
302    #[inline]
303    fn script_get_history(&self, script: &Script) -> Result<Vec<GetHistoryRes>, Error> {
304        impl_inner_call!(self, script_get_history, script)
305    }
306
307    #[inline]
308    fn batch_script_get_history<'s, I>(&self, scripts: I) -> Result<Vec<Vec<GetHistoryRes>>, Error>
309    where
310        I: IntoIterator + Clone,
311        I::Item: Borrow<&'s Script>,
312    {
313        impl_inner_call!(self, batch_script_get_history, scripts.clone())
314    }
315
316    #[inline]
317    fn script_list_unspent(&self, script: &Script) -> Result<Vec<ListUnspentRes>, Error> {
318        impl_inner_call!(self, script_list_unspent, script)
319    }
320
321    #[inline]
322    fn batch_script_list_unspent<'s, I>(
323        &self,
324        scripts: I,
325    ) -> Result<Vec<Vec<ListUnspentRes>>, Error>
326    where
327        I: IntoIterator + Clone,
328        I::Item: Borrow<&'s Script>,
329    {
330        impl_inner_call!(self, batch_script_list_unspent, scripts.clone())
331    }
332
333    #[inline]
334    fn transaction_get_raw(&self, txid: &Txid) -> Result<Vec<u8>, Error> {
335        impl_inner_call!(self, transaction_get_raw, txid)
336    }
337
338    #[inline]
339    fn batch_transaction_get_raw<'t, I>(&self, txids: I) -> Result<Vec<Vec<u8>>, Error>
340    where
341        I: IntoIterator + Clone,
342        I::Item: Borrow<&'t Txid>,
343    {
344        impl_inner_call!(self, batch_transaction_get_raw, txids.clone())
345    }
346
347    #[inline]
348    fn batch_block_header_raw<'s, I>(&self, heights: I) -> Result<Vec<Vec<u8>>, Error>
349    where
350        I: IntoIterator + Clone,
351        I::Item: Borrow<u32>,
352    {
353        impl_inner_call!(self, batch_block_header_raw, heights.clone())
354    }
355
356    #[inline]
357    fn batch_estimate_fee<'s, I>(&self, numbers: I) -> Result<Vec<f64>, Error>
358    where
359        I: IntoIterator + Clone,
360        I::Item: Borrow<usize>,
361    {
362        impl_inner_call!(self, batch_estimate_fee, numbers.clone())
363    }
364
365    #[inline]
366    fn transaction_broadcast_raw(&self, raw_tx: &[u8]) -> Result<Txid, Error> {
367        impl_inner_call!(self, transaction_broadcast_raw, raw_tx)
368    }
369
370    #[inline]
371    fn transaction_broadcast_package_raw<T: AsRef<[u8]>>(
372        &self,
373        raw_txs: &[T],
374    ) -> Result<BroadcastPackageRes, Error> {
375        impl_inner_call!(self, transaction_broadcast_package_raw, raw_txs)
376    }
377
378    #[inline]
379    fn transaction_get_merkle(&self, txid: &Txid, height: usize) -> Result<GetMerkleRes, Error> {
380        impl_inner_call!(self, transaction_get_merkle, txid, height)
381    }
382
383    #[inline]
384    fn batch_transaction_get_merkle<I>(
385        &self,
386        txids_and_heights: I,
387    ) -> Result<Vec<GetMerkleRes>, Error>
388    where
389        I: IntoIterator + Clone,
390        I::Item: Borrow<(Txid, usize)>,
391    {
392        impl_inner_call!(
393            self,
394            batch_transaction_get_merkle,
395            txids_and_heights.clone()
396        )
397    }
398
399    #[inline]
400    fn txid_from_pos(&self, height: usize, tx_pos: usize) -> Result<Txid, Error> {
401        impl_inner_call!(self, txid_from_pos, height, tx_pos)
402    }
403
404    #[inline]
405    fn txid_from_pos_with_merkle(
406        &self,
407        height: usize,
408        tx_pos: usize,
409    ) -> Result<TxidFromPosRes, Error> {
410        impl_inner_call!(self, txid_from_pos_with_merkle, height, tx_pos)
411    }
412
413    #[inline]
414    fn server_features(&self) -> Result<ServerFeaturesRes, Error> {
415        impl_inner_call!(self, server_features)
416    }
417
418    #[inline]
419    fn mempool_get_info(&self) -> Result<MempoolInfoRes, Error> {
420        impl_inner_call!(self, mempool_get_info)
421    }
422
423    #[inline]
424    fn ping(&self) -> Result<(), Error> {
425        impl_inner_call!(self, ping)
426    }
427
428    #[inline]
429    fn calls_made(&self) -> Result<usize, Error> {
430        impl_inner_call!(self, calls_made)
431    }
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    #[test]
439    fn more_failed_attempts_than_retries_means_exhausted() {
440        let exhausted = retries_exhausted(10, 5);
441
442        assert!(exhausted)
443    }
444
445    #[test]
446    fn failed_attempts_bigger_than_u8_means_exhausted() {
447        let failed_attempts = u8::MAX as usize + 1;
448
449        let exhausted = retries_exhausted(failed_attempts, u8::MAX);
450
451        assert!(exhausted)
452    }
453
454    #[test]
455    fn less_failed_attempts_means_not_exhausted() {
456        let exhausted = retries_exhausted(2, 5);
457
458        assert!(!exhausted)
459    }
460
461    #[test]
462    fn attempts_equals_retries_means_not_exhausted_yet() {
463        let exhausted = retries_exhausted(2, 2);
464
465        assert!(!exhausted)
466    }
467}