Skip to main content

electrum_client/
types.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Return types
4//!
5//! This module contains definitions of all the complex data structures that are returned by calls
6
7use std::convert::TryFrom;
8use std::fmt::{self, Display, Formatter};
9use std::ops::Deref;
10use std::sync::Arc;
11
12use bitcoin::blockdata::block;
13use bitcoin::consensus::encode::deserialize;
14use bitcoin::hashes::{sha256, Hash};
15use bitcoin::hex::{DisplayHex, FromHex};
16use bitcoin::{Script, Txid};
17
18use serde::{de, Deserialize, Serialize};
19
20static JSONRPC_2_0: &str = "2.0";
21
22pub(crate) type Call = (String, Vec<Param>);
23
24#[derive(Serialize, Clone)]
25#[serde(untagged)]
26/// A single parameter of a [`Request`](struct.Request.html)
27pub enum Param {
28    /// Integer parameter
29    U32(u32),
30    /// Integer parameter
31    Usize(usize),
32    /// String parameter
33    String(String),
34    /// Boolean parameter
35    Bool(bool),
36    /// Bytes array parameter
37    Bytes(Vec<u8>),
38    /// String array parameter
39    StringVec(Vec<String>),
40}
41
42/// Fee estimation mode for [`estimate_fee`](../api/trait.ElectrumApi.html#method.estimate_fee).
43///
44/// This parameter was added in protocol v1.6 and is passed to bitcoind's
45/// `estimatesmartfee` RPC as the `estimate_mode` parameter.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum EstimationMode {
48    /// A conservative estimate potentially returns a higher feerate and is more likely to be
49    /// sufficient for the desired target, but is not as responsive to short term drops in the
50    /// prevailing fee market.
51    Conservative,
52    /// Economical fee estimate - potentially lower fees but may take longer to confirm.
53    Economical,
54}
55
56impl Display for EstimationMode {
57    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
58        match self {
59            EstimationMode::Conservative => write!(f, "CONSERVATIVE"),
60            EstimationMode::Economical => write!(f, "ECONOMICAL"),
61        }
62    }
63}
64
65#[derive(Serialize, Clone)]
66/// A request that can be sent to the server
67pub struct Request<'a> {
68    jsonrpc: &'static str,
69
70    /// The JSON-RPC request id
71    pub id: usize,
72    /// The request method
73    pub method: &'a str,
74    /// The request parameters
75    pub params: Vec<Param>,
76
77    /// Authorization token (e.g. `"Bearer <token>"`) included in the JSON-RPC request, if any.
78    #[serde(skip_serializing_if = "Option::is_none")]
79    authorization: Option<String>,
80}
81
82impl<'a> Request<'a> {
83    /// Creates a new [`Request`] with a default `id`.
84    fn new(method: &'a str, params: Vec<Param>) -> Self {
85        Self {
86            id: 0,
87            jsonrpc: JSONRPC_2_0,
88            method,
89            params,
90            authorization: None,
91        }
92    }
93
94    /// Creates a new [`Request`] with a user-specified `id`.
95    pub fn new_id(id: usize, method: &'a str, params: Vec<Param>) -> Self {
96        let mut instance = Self::new(method, params);
97        instance.id = id;
98
99        instance
100    }
101
102    /// Sets the `authorization` token for this [`Request`].
103    pub fn with_auth(mut self, authorization: Option<String>) -> Self {
104        self.authorization = authorization;
105        self
106    }
107}
108
109#[doc(hidden)]
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
111pub struct Hex32Bytes(#[serde(deserialize_with = "from_hex", serialize_with = "to_hex")] [u8; 32]);
112
113impl Deref for Hex32Bytes {
114    type Target = [u8; 32];
115
116    fn deref(&self) -> &Self::Target {
117        &self.0
118    }
119}
120
121impl From<[u8; 32]> for Hex32Bytes {
122    fn from(other: [u8; 32]) -> Hex32Bytes {
123        Hex32Bytes(other)
124    }
125}
126
127impl Hex32Bytes {
128    pub(crate) fn to_hex(self) -> String {
129        self.0.to_lower_hex_string()
130    }
131}
132
133/// Format used by the Electrum server to identify an address. The reverse sha256 hash of the
134/// scriptPubKey. Documented [here](https://electrumx.readthedocs.io/en/latest/protocol-basics.html#script-hashes).
135pub type ScriptHash = Hex32Bytes;
136
137/// Binary blob that condenses all the activity of an address. Used to detect changes without
138/// having to compare potentially long lists of transactions.
139pub type ScriptStatus = Hex32Bytes;
140
141/// Trait used to convert a struct into the Electrum representation of an address
142pub trait ToElectrumScriptHash {
143    /// Transforms the current struct into a `ScriptHash`
144    fn to_electrum_scripthash(&self) -> ScriptHash;
145}
146
147impl ToElectrumScriptHash for Script {
148    fn to_electrum_scripthash(&self) -> ScriptHash {
149        let mut result = sha256::Hash::hash(self.as_bytes()).to_byte_array();
150        result.reverse();
151
152        result.into()
153    }
154}
155
156fn from_hex<'de, T, D>(deserializer: D) -> Result<T, D::Error>
157where
158    T: FromHex,
159    D: de::Deserializer<'de>,
160{
161    let s = String::deserialize(deserializer)?;
162    T::from_hex(&s).map_err(de::Error::custom)
163}
164
165fn to_hex<S>(bytes: &[u8], serializer: S) -> std::result::Result<S::Ok, S::Error>
166where
167    S: serde::ser::Serializer,
168{
169    serializer.serialize_str(&bytes.to_lower_hex_string())
170}
171
172fn from_hex_array<'de, T, D>(deserializer: D) -> Result<Vec<T>, D::Error>
173where
174    T: FromHex + std::fmt::Debug,
175    D: de::Deserializer<'de>,
176{
177    let arr = Vec::<String>::deserialize(deserializer)?;
178
179    let results: Vec<Result<T, _>> = arr
180        .into_iter()
181        .map(|s| T::from_hex(&s).map_err(de::Error::custom))
182        .collect();
183
184    let mut answer = Vec::new();
185    for x in results.into_iter() {
186        answer.push(x?);
187    }
188
189    Ok(answer)
190}
191
192fn from_hex_header<'de, D>(deserializer: D) -> Result<block::Header, D::Error>
193where
194    D: de::Deserializer<'de>,
195{
196    let vec: Vec<u8> = from_hex(deserializer)?;
197    deserialize(&vec).map_err(de::Error::custom)
198}
199
200/// Response to a [`script_get_history`](../client/struct.Client.html#method.script_get_history) request.
201#[derive(Clone, Debug, Deserialize)]
202pub struct GetHistoryRes {
203    /// Confirmation height of the transaction. 0 if unconfirmed, -1 if unconfirmed while some of
204    /// its inputs are unconfirmed too.
205    pub height: i32,
206    /// Txid of the transaction.
207    pub tx_hash: Txid,
208    /// Fee of the transaction.
209    pub fee: Option<u64>,
210}
211
212/// Response to a [`script_list_unspent`](../client/struct.Client.html#method.script_list_unspent) request.
213#[derive(Clone, Debug, Deserialize)]
214pub struct ListUnspentRes {
215    /// Confirmation height of the transaction that created this output.
216    pub height: usize,
217    /// Txid of the transaction
218    pub tx_hash: Txid,
219    /// Index of the output in the transaction.
220    pub tx_pos: usize,
221    /// Value of the output.
222    pub value: u64,
223}
224
225/// Response to a [`server_features`](../client/struct.Client.html#method.server_features) request.
226#[derive(Clone, Debug, Deserialize)]
227pub struct ServerFeaturesRes {
228    /// Server version reported.
229    pub server_version: String,
230    /// Hash of the genesis block.
231    #[serde(deserialize_with = "from_hex")]
232    pub genesis_hash: [u8; 32],
233    /// Minimum supported version of the protocol.
234    pub protocol_min: String,
235    /// Maximum supported version of the protocol.
236    pub protocol_max: String,
237    /// Hash function used to create the [`ScriptHash`](type.ScriptHash.html).
238    pub hash_function: Option<String>,
239    /// Pruned height of the server.
240    pub pruning: Option<i64>,
241}
242
243/// Response to a [`server_version`](../client/struct.Client.html#method.server_version) request.
244///
245/// This is returned as an array of two strings: `[server_software_version, protocol_version]`.
246
247#[derive(Clone, Debug, Deserialize)]
248#[serde(from = "(String, String)")]
249pub struct ServerVersionRes {
250    /// Server software version string (e.g., "ElectrumX 1.18.0").
251    pub server_software_version: String,
252    /// Negotiated protocol version (e.g., "1.6").
253    pub protocol_version: String,
254}
255
256impl From<(String, String)> for ServerVersionRes {
257    fn from((server_software_version, protocol_version): (String, String)) -> Self {
258        Self {
259            server_software_version,
260            protocol_version,
261        }
262    }
263}
264
265/// Response to a [`mempool_get_info`](../client/struct.Client.html#method.mempool_get_info) request.
266///
267/// Contains information about the current state of the mempool.
268#[derive(Clone, Debug, Deserialize)]
269pub struct MempoolInfoRes {
270    /// Dynamic minimum fee rate in BTC/kvB for tx to be accepted given current conditions.
271    /// The maximum of `minrelaytxfee` and minimum mempool fee.
272    pub mempoolminfee: f64,
273    /// Static operator-configurable minimum relay fee for transactions, in BTC/kvB.
274    pub minrelaytxfee: f64,
275    /// Static operator-configurable minimum fee rate increment for mempool limiting or
276    /// replacement, in BTC/kvB.
277    pub incrementalrelayfee: f64,
278}
279
280/// Response to a [`block_headers`](../client/struct.Client.html#method.block_headers) request (protocol v1.4, legacy format).
281///
282/// In protocol v1.4, the headers are returned as a single concatenated hex string.
283#[derive(Clone, Debug, Deserialize)]
284pub(crate) struct GetHeadersResLegacy {
285    /// Maximum number of headers returned in a single response.
286    pub max: usize,
287    /// Number of headers in this response.
288    pub count: usize,
289    /// Raw headers concatenated.
290    #[serde(rename(deserialize = "hex"), deserialize_with = "from_hex")]
291    pub raw_headers: Vec<u8>,
292}
293
294/// Response to a [`block_headers`](../client/struct.Client.html#method.block_headers) request.
295#[derive(Clone, Debug, Deserialize)]
296pub struct GetHeadersRes {
297    /// Maximum number of headers returned in a single response.
298    pub max: usize,
299    /// Number of headers in this response.
300    pub count: usize,
301    /// Array of header hex strings (v1.6 format).
302    #[serde(default, rename(deserialize = "headers"))]
303    pub(crate) header_hexes: Vec<String>,
304    /// Array of block headers (populated after parsing).
305    #[serde(skip)]
306    pub headers: Vec<block::Header>,
307}
308
309/// Response to a [`script_get_balance`](../client/struct.Client.html#method.script_get_balance) request.
310#[derive(Clone, Debug, Deserialize)]
311pub struct GetBalanceRes {
312    /// Confirmed balance in Satoshis for the address.
313    pub confirmed: u64,
314    /// Unconfirmed balance in Satoshis for the address.
315    ///
316    /// Some servers (e.g. `electrs`) return this as a negative value.
317    pub unconfirmed: i64,
318}
319
320/// Response to a [`transaction_get_merkle`](../client/struct.Client.html#method.transaction_get_merkle) request.
321#[derive(Clone, Debug, Deserialize)]
322pub struct GetMerkleRes {
323    /// Height of the block that confirmed the transaction
324    pub block_height: usize,
325    /// Position in the block of the transaction.
326    pub pos: usize,
327    /// The merkle path of the transaction.
328    #[serde(deserialize_with = "from_hex_array")]
329    pub merkle: Vec<[u8; 32]>,
330}
331
332/// Response to a [`txid_from_pos_with_merkle`](../client/struct.Client.html#method.txid_from_pos_with_merkle)
333/// request.
334#[derive(Clone, Debug, Deserialize)]
335pub struct TxidFromPosRes {
336    /// Txid of the transaction.
337    pub tx_hash: Txid,
338    /// The merkle path of the transaction.
339    #[serde(deserialize_with = "from_hex_array")]
340    pub merkle: Vec<[u8; 32]>,
341}
342
343/// Error details for a transaction that failed to broadcast in a package.
344#[derive(Clone, Debug, Deserialize)]
345pub struct BroadcastPackageError {
346    /// The txid of the transaction that failed.
347    pub txid: Txid,
348    /// The error message describing why the transaction was rejected.
349    pub error: String,
350}
351
352/// Response to a [`transaction_broadcast_package`](../client/struct.Client.html#method.transaction_broadcast_package)
353/// request.
354///
355/// This method was added in protocol v1.6 for package relay support.
356#[derive(Clone, Debug, Deserialize)]
357pub struct BroadcastPackageRes {
358    /// Whether the package was successfully accepted by the mempool.
359    pub success: bool,
360    /// List of errors for transactions that were rejected.
361    /// Only present if some transactions failed.
362    #[serde(default)]
363    pub errors: Vec<BroadcastPackageError>,
364}
365
366/// Notification of a new block header
367#[derive(Clone, Debug, Deserialize)]
368pub struct HeaderNotification {
369    /// New block height.
370    pub height: usize,
371    /// Newly added header.
372    #[serde(rename = "hex", deserialize_with = "from_hex_header")]
373    pub header: block::Header,
374}
375
376/// Notification of a new block header with the header encoded as raw bytes
377#[derive(Clone, Debug, Deserialize)]
378pub struct RawHeaderNotification {
379    /// New block height.
380    pub height: usize,
381    /// Newly added header.
382    #[serde(rename = "hex", deserialize_with = "from_hex")]
383    pub header: Vec<u8>,
384}
385
386impl TryFrom<RawHeaderNotification> for HeaderNotification {
387    type Error = Error;
388
389    fn try_from(raw: RawHeaderNotification) -> Result<Self, Self::Error> {
390        Ok(HeaderNotification {
391            height: raw.height,
392            header: deserialize(&raw.header)?,
393        })
394    }
395}
396
397/// Notification of the new status of a script
398#[derive(Clone, Debug, Deserialize)]
399pub struct ScriptNotification {
400    /// Address that generated this notification.
401    pub scripthash: ScriptHash,
402    /// The new status of the address.
403    pub status: ScriptStatus,
404}
405
406/// Errors
407#[derive(Debug)]
408pub enum Error {
409    /// Wraps `std::io::Error`
410    IOError(std::io::Error),
411    /// Wraps `serde_json::error::Error`
412    JSON(serde_json::error::Error),
413    /// Wraps `bitcoin::hex::HexToBytesError`
414    Hex(bitcoin::hex::HexToBytesError),
415    /// Error returned by the Electrum server
416    Protocol(serde_json::Value),
417    /// Error during the deserialization of a Bitcoin data structure
418    Bitcoin(bitcoin::consensus::encode::Error),
419    /// Already subscribed to the notifications of an address
420    AlreadySubscribed(ScriptHash),
421    /// Not subscribed to the notifications of an address
422    NotSubscribed(ScriptHash),
423    /// Error during the deserialization of a response from the server
424    InvalidResponse(serde_json::Value),
425    /// Generic error with a message
426    Message(String),
427    /// Invalid domain name for an SSL certificate
428    InvalidDNSNameError(String),
429    /// Missing domain while it was explicitly asked to validate it
430    MissingDomain,
431    /// Made one or multiple attempts, always in Error
432    AllAttemptsErrored(Vec<Error>),
433    /// There was an io error reading the socket, to be shared between threads
434    SharedIOError(Arc<std::io::Error>),
435
436    /// Couldn't take a lock on the reader mutex. This means that there's already another reader
437    /// thread running
438    CouldntLockReader,
439    /// Broken IPC communication channel: the other thread probably has exited
440    Mpsc,
441    #[cfg(any(feature = "rustls", feature = "rustls-ring"))]
442    /// Could not create a rustls client connection
443    CouldNotCreateConnection(rustls::Error),
444
445    #[cfg(feature = "openssl")]
446    /// Invalid OpenSSL method used
447    InvalidSslMethod(openssl::error::ErrorStack),
448    #[cfg(feature = "openssl")]
449    /// SSL Handshake failed with the server
450    SslHandshakeError(openssl::ssl::HandshakeError<std::net::TcpStream>),
451}
452
453impl Display for Error {
454    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
455        match self {
456            Error::IOError(e) => Display::fmt(e, f),
457            Error::JSON(e) => Display::fmt(e, f),
458            Error::Hex(e) => Display::fmt(e, f),
459            Error::Bitcoin(e) => Display::fmt(e, f),
460            Error::SharedIOError(e) => Display::fmt(e, f),
461            #[cfg(feature = "openssl")]
462            Error::SslHandshakeError(e) => Display::fmt(e, f),
463            #[cfg(feature = "openssl")]
464            Error::InvalidSslMethod(e) => Display::fmt(e, f),
465            #[cfg(any(
466                feature = "rustls",
467                feature = "rustls-ring",
468            ))]
469            Error::CouldNotCreateConnection(e) => Display::fmt(e, f),
470
471            Error::Message(e) => f.write_str(e),
472            Error::InvalidDNSNameError(domain) => write!(f, "Invalid domain name {} not matching SSL certificate", domain),
473            Error::AllAttemptsErrored(errors) => {
474                f.write_str("Made one or multiple attempts, all errored:\n")?;
475                for err in errors {
476                    writeln!(f, "\t- {}", err)?;
477                }
478                Ok(())
479            }
480
481            Error::Protocol(e) => write!(f, "Electrum server error: {}", e.clone().take()),
482            Error::InvalidResponse(e) => write!(f, "Error during the deserialization of a response from the server: {}", e.clone().take()),
483
484            // TODO: Print out addresses once `ScriptHash` will implement `Display`
485            Error::AlreadySubscribed(_) => write!(f, "Already subscribed to the notifications of an address"),
486            Error::NotSubscribed(_) => write!(f, "Not subscribed to the notifications of an address"),
487
488            Error::MissingDomain => f.write_str("Missing domain while it was explicitly asked to validate it"),
489            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"),
490            Error::Mpsc => f.write_str("Broken IPC communication channel: the other thread probably has exited"),
491        }
492    }
493}
494
495impl std::error::Error for Error {}
496
497macro_rules! impl_error {
498    ( $from:ty, $to:ident ) => {
499        impl std::convert::From<$from> for Error {
500            fn from(err: $from) -> Self {
501                Error::$to(err.into())
502            }
503        }
504    };
505}
506
507impl_error!(std::io::Error, IOError);
508impl_error!(serde_json::Error, JSON);
509impl_error!(bitcoin::hex::HexToBytesError, Hex);
510impl_error!(bitcoin::consensus::encode::Error, Bitcoin);
511
512impl<T> From<std::sync::PoisonError<T>> for Error {
513    fn from(_: std::sync::PoisonError<T>) -> Self {
514        Error::IOError(std::io::Error::from(std::io::ErrorKind::BrokenPipe))
515    }
516}
517
518impl<T> From<std::sync::mpsc::SendError<T>> for Error {
519    fn from(_: std::sync::mpsc::SendError<T>) -> Self {
520        Error::Mpsc
521    }
522}
523
524impl From<std::sync::mpsc::RecvError> for Error {
525    fn from(_: std::sync::mpsc::RecvError) -> Self {
526        Error::Mpsc
527    }
528}
529
530#[cfg(test)]
531mod tests {
532    use crate::ScriptStatus;
533
534    use super::{Param, Request};
535
536    #[test]
537    fn script_status_roundtrip() {
538        let script_status: ScriptStatus = [1u8; 32].into();
539        let script_status_json = serde_json::to_string(&script_status).unwrap();
540        let script_status_back = serde_json::from_str(&script_status_json).unwrap();
541        assert_eq!(script_status, script_status_back);
542    }
543
544    #[test]
545    fn test_request_serialization_without_authorization() {
546        let req = Request::new_id(1, "server.version", vec![]);
547
548        let json = serde_json::to_string(&req).unwrap();
549        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
550
551        // Authorization field should not be present when None
552        assert!(parsed.get("authorization").is_none());
553        assert!(!json.contains("authorization"));
554        assert_eq!(parsed["jsonrpc"], "2.0");
555        assert_eq!(parsed["method"], "server.version");
556        assert_eq!(parsed["id"], 1);
557    }
558
559    #[test]
560    fn test_request_serialization_with_authorization() {
561        let mut req = Request::new_id(1, "server.version", vec![]);
562        req.authorization = Some("Bearer test-jwt-token".to_string());
563
564        let json = serde_json::to_string(&req).unwrap();
565        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
566
567        // Authorization field should be present
568        assert_eq!(
569            parsed["authorization"],
570            serde_json::Value::String("Bearer test-jwt-token".to_string())
571        );
572        assert_eq!(parsed["jsonrpc"], "2.0");
573        assert_eq!(parsed["method"], "server.version");
574        assert_eq!(parsed["id"], 1);
575    }
576
577    #[test]
578    fn test_request_with_params_and_authorization() {
579        let mut req = Request::new_id(
580            42,
581            "blockchain.scripthash.get_balance",
582            vec![Param::String("test-scripthash".to_string())],
583        );
584        req.authorization = Some("Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9".to_string());
585
586        let json = serde_json::to_string(&req).unwrap();
587        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
588
589        assert_eq!(parsed["id"], 42);
590        assert_eq!(parsed["method"], "blockchain.scripthash.get_balance");
591        assert_eq!(
592            parsed["authorization"],
593            "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9"
594        );
595        assert!(parsed["params"].is_array());
596        assert_eq!(parsed["params"][0], "test-scripthash");
597    }
598}