electrum_client_netagnostic/
client.rs1use std::sync::RwLock;
4
5use log::{info, warn};
6
7use crate::api::ElectrumApi;
8use crate::batch::Batch;
9use crate::config::Config;
10use crate::raw_client::*;
11use crate::types::*;
12use std::convert::TryFrom;
13
14pub enum ClientType {
20 #[allow(missing_docs)]
21 TCP(RawClient<ElectrumPlaintextStream>),
22 #[allow(missing_docs)]
23 SSL(RawClient<ElectrumSslStream>),
24 #[allow(missing_docs)]
25 Socks5(RawClient<ElectrumProxyStream>),
26 #[cfg(feature = "use-websocket")]
27 #[allow(missing_docs)]
28 WS(RawClient<ElectrumWsStream>),
29 #[cfg(all(
30 feature = "use-websocket",
31 any(feature = "use-rustls", feature = "use-rustls-ring"),
32 not(feature = "use-openssl")
33 ))]
34 #[allow(missing_docs)]
35 WSS(RawClient<ElectrumWssStream>),
36}
37
38pub struct Client {
41 client_type: RwLock<ClientType>,
42 config: Config,
43 url: String,
44}
45
46macro_rules! impl_inner_call {
47 ( $self:expr, $name:ident $(, $args:expr)* ) => {
48 {
49 let mut errors = vec![];
50 loop {
51 let read_client = $self.client_type.read().unwrap();
52 let res = match &*read_client {
53 ClientType::TCP(inner) => inner.$name( $($args, )* ),
54 ClientType::SSL(inner) => inner.$name( $($args, )* ),
55 ClientType::Socks5(inner) => inner.$name( $($args, )* ),
56 #[cfg(feature = "use-websocket")]
57 ClientType::WS(inner) => inner.$name( $($args, )* ),
58 #[cfg(all(
59 feature = "use-websocket",
60 any(feature = "use-rustls", feature = "use-rustls-ring"),
61 not(feature = "use-openssl")
62 ))]
63 ClientType::WSS(inner) => inner.$name( $($args, )* ),
64 };
65 drop(read_client);
66 match res {
67 Ok(val) => return Ok(val),
68 Err(Error::Protocol(_)) => {
69 return res;
70 },
71 Err(e) => {
72 let failed_attempts = errors.len() + 1;
73
74 if retries_exhausted(failed_attempts, $self.config.retry()) {
75 warn!("call '{}' failed after {} attempts", stringify!($name), failed_attempts);
76 return Err(Error::AllAttemptsErrored(errors));
77 }
78
79 warn!("call '{}' failed with {}, retry: {}/{}", stringify!($name), e, failed_attempts, $self.config.retry());
80
81 errors.push(e);
82
83 if let Ok(mut write_client) = $self.client_type.try_write() {
87 loop {
88 std::thread::sleep(std::time::Duration::from_secs((1 << errors.len()).min(30) as u64));
89 match ClientType::from_config(&$self.url, &$self.config) {
90 Ok(new_client) => {
91 info!("Succesfully created new client");
92 *write_client = new_client;
93 break;
94 },
95 Err(e) => {
96 let failed_attempts = errors.len() + 1;
97
98 if retries_exhausted(failed_attempts, $self.config.retry()) {
99 warn!("re-creating client failed after {} attempts", failed_attempts);
100 return Err(Error::AllAttemptsErrored(errors));
101 }
102
103 warn!("re-creating client failed with {}, retry: {}/{}", e, failed_attempts, $self.config.retry());
104
105 errors.push(e);
106 }
107 }
108 }
109 }
110 },
111 }
112 }}
113 }
114}
115
116fn retries_exhausted(failed_attempts: usize, configured_retries: u8) -> bool {
117 match u8::try_from(failed_attempts) {
118 Ok(failed_attempts) => failed_attempts > configured_retries,
119 Err(_) => true, }
121}
122
123impl ClientType {
124 pub fn from_config(url: &str, config: &Config) -> Result<Self, Error> {
127 if url.starts_with("ssl://") {
128 let url = url.replacen("ssl://", "", 1);
129 let client = match config.socks5() {
130 Some(socks5) => RawClient::new_proxy_ssl(
131 url.as_str(),
132 config.validate_domain(),
133 socks5,
134 config.timeout(),
135 )?,
136 None => {
137 RawClient::new_ssl(url.as_str(), config.validate_domain(), config.timeout())?
138 }
139 };
140
141 Ok(ClientType::SSL(client))
142 } else if url.starts_with("wss://") {
143 #[cfg(all(
144 feature = "use-websocket",
145 any(feature = "use-rustls", feature = "use-rustls-ring"),
146 not(feature = "use-openssl")
147 ))]
148 {
149 let url = url.replacen("wss://", "", 1);
150 let client = RawClient::new_wss(
151 url.as_str(),
152 config.validate_domain(),
153 config.timeout(),
154 config.max_message_size(),
155 )?;
156 Ok(ClientType::WSS(client))
157 }
158 #[cfg(not(all(
159 feature = "use-websocket",
160 any(feature = "use-rustls", feature = "use-rustls-ring"),
161 not(feature = "use-openssl")
162 )))]
163 {
164 Err(Error::Message(
165 "WSS support requires the 'use-websocket' feature and a rustls feature"
166 .to_string(),
167 ))
168 }
169 } else if url.starts_with("ws://") {
170 #[cfg(feature = "use-websocket")]
171 {
172 let url = url.replacen("ws://", "", 1);
173 let client =
174 RawClient::new_ws(url.as_str(), config.timeout(), config.max_message_size())?;
175 Ok(ClientType::WS(client))
176 }
177 #[cfg(not(feature = "use-websocket"))]
178 {
179 Err(Error::Message(
180 "WebSocket support requires the 'use-websocket' feature".to_string(),
181 ))
182 }
183 } else {
184 let url = url.replacen("tcp://", "", 1);
185
186 Ok(match config.socks5().as_ref() {
187 None => ClientType::TCP(RawClient::new(url.as_str(), config.timeout())?),
188 Some(socks5) => ClientType::Socks5(RawClient::new_proxy(
189 url.as_str(),
190 socks5,
191 config.timeout(),
192 )?),
193 })
194 }
195 }
196}
197
198impl Client {
199 pub fn new(url: &str) -> Result<Self, Error> {
212 Self::from_config(url, Config::default())
213 }
214
215 pub fn from_config(url: &str, config: Config) -> Result<Self, Error> {
218 let client_type = RwLock::new(ClientType::from_config(url, &config)?);
219
220 Ok(Client {
221 client_type,
222 config,
223 url: url.to_string(),
224 })
225 }
226}
227
228impl ElectrumApi for Client {
229 #[inline]
230 fn raw_call(
231 &self,
232 method_name: &str,
233 params: impl IntoIterator<Item = Param>,
234 ) -> Result<serde_json::Value, Error> {
235 let vec = params.into_iter().collect::<Vec<Param>>();
241 impl_inner_call!(self, internal_raw_call_with_vec, method_name, vec.clone());
242 }
243
244 #[inline]
245 fn batch_call(&self, batch: &Batch) -> Result<Vec<serde_json::Value>, Error> {
246 impl_inner_call!(self, batch_call, batch)
247 }
248
249 #[inline]
250 fn ping(&self) -> Result<(), Error> {
251 impl_inner_call!(self, ping)
252 }
253
254 #[inline]
255 #[cfg(feature = "debug-calls")]
256 fn calls_made(&self) -> Result<usize, Error> {
257 impl_inner_call!(self, calls_made)
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn more_failed_attempts_than_retries_means_exhausted() {
267 let exhausted = retries_exhausted(10, 5);
268
269 assert_eq!(exhausted, true)
270 }
271
272 #[test]
273 fn failed_attempts_bigger_than_u8_means_exhausted() {
274 let failed_attempts = u8::MAX as usize + 1;
275
276 let exhausted = retries_exhausted(failed_attempts, u8::MAX);
277
278 assert_eq!(exhausted, true)
279 }
280
281 #[test]
282 fn less_failed_attempts_means_not_exhausted() {
283 let exhausted = retries_exhausted(2, 5);
284
285 assert_eq!(exhausted, false)
286 }
287
288 #[test]
289 fn attempts_equals_retries_means_not_exhausted_yet() {
290 let exhausted = retries_exhausted(2, 2);
291
292 assert_eq!(exhausted, false)
293 }
294
295 #[test]
296 #[ignore]
297 fn test_local_timeout() {
298 use std::net::TcpListener;
312 use std::sync::mpsc::channel;
313 use std::time::{Duration, Instant};
314
315 let endpoint =
316 std::env::var("TEST_ELECTRUM_TIMEOUT_PORT").unwrap_or("localhost:60000".into());
317 let (sender, receiver) = channel();
318
319 std::thread::spawn(move || {
320 let listener = TcpListener::bind("127.0.0.1:60000").unwrap();
321 sender.send(()).unwrap();
322
323 for _stream in listener.incoming() {
324 loop {}
325 }
326 });
327
328 receiver
329 .recv_timeout(Duration::from_secs(5))
330 .expect("Can't start local listener");
331
332 let now = Instant::now();
333 let client = Client::from_config(
334 &endpoint,
335 crate::config::ConfigBuilder::new()
336 .timeout(Some(Duration::from_secs(5)))
337 .build(),
338 );
339 let elapsed = now.elapsed();
340
341 assert!(client.is_ok());
342 assert!(elapsed > Duration::from_secs(2));
343 }
344}