blockbook/
lib.rs

1//! # Rust Blockbook Library
2//!
3//! This crate provides REST and WebSocket clients to query various
4//! information from a Blockbook server, which is a block explorer
5//! backend [created and maintained](https://github.com/trezor/blockbook)
6//! by SatoshiLabs.
7//!
8//! Note that this crate currently only exposes a Bitcoin-specific API,
9//! even though Blockbook provides a unified API that supports
10//! [multiple cryptocurrencies](https://github.com/trezor/blockbook#implemented-coins).
11//!
12//! The methods exposed in this crate make extensive use of types from the
13//! [`bitcoin`](https://crates.io/crates/bitcoin) crate to provide strongly typed APIs.
14//!
15//! An example of how to use the [`REST client`]:
16//!
17//! ```ignore
18//! # tokio_test::block_on(async {
19//! # let url = format!("https://{}", std::env::var("BLOCKBOOK_SERVER").unwrap()).parse().unwrap();
20//! let client = blockbook::Client::new(url).await?;
21//!
22//! // query the Genesis block hash
23//! let genesis_hash = client
24//!     .block_hash(&blockbook::Height::from_consensus(0).unwrap())
25//!     .await?;
26//! assert_eq!(
27//!     genesis_hash.to_string(),
28//!     "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
29//! );
30//!
31//! // query the full block
32//! let genesis = client.block_by_hash(&genesis_hash).await?;
33//! assert_eq!(genesis.previous_block_hash, None);
34//!
35//! // inspect the first coinbase transaction
36//! let tx = genesis.txs.get(0).unwrap();
37//! assert!((tx.vout.get(0).unwrap().value.to_btc() - 50.0).abs() < f64::EPSILON);
38//! # Ok::<_,blockbook::Error>(())
39//! # });
40//! ```
41//!
42//! For an example of how to use the WebSocket client, see [`its documentation`].
43//!
44//! ## Available feature flags
45//!
46//! * `bdk` - adds functions for integration with the [`bdk`](https://crates.io/crates/bdk) wallet library
47//!
48//! ## Supported Blockbook Version
49//!
50//! The currently supported version of Blockbook is commit [`95ee9b5b`](https://github.com/trezor/blockbook/commit/95ee9b5b).
51//!
52//! [`REST client`]: Client
53//! [`its documentation`]: websocket::Client
54
55#![forbid(unsafe_code)]
56
57mod external {
58    pub use bitcoin::address::Address;
59    pub use bitcoin::address::NetworkUnchecked;
60    pub use bitcoin::amount::Amount;
61    pub use bitcoin::bip32::DerivationPath;
62    pub use bitcoin::blockdata::locktime::absolute::{Height, LockTime, Time};
63    pub use bitcoin::blockdata::script::ScriptBuf;
64    pub use bitcoin::blockdata::witness::Witness;
65    pub use bitcoin::hash_types::{BlockHash, TxMerkleNode, Txid, Wtxid};
66    pub use bitcoin::hashes;
67    pub use bitcoin::Sequence;
68    pub use bitcoin::Transaction as BitcoinTransaction;
69    pub use reqwest::Error as ReqwestError;
70    pub use url::ParseError;
71}
72
73#[doc(hidden)]
74pub use external::*;
75
76/// The WebSocket client.
77pub mod websocket;
78
79#[cfg(feature = "bdk")]
80pub mod bdk;
81
82/// The errors emitted by the REST client.
83#[derive(Debug, thiserror::Error)]
84pub enum Error {
85    /// An error during a network request.
86    #[error("error occurred during the network request: {0}")]
87    RequestError(#[from] reqwest::Error),
88    /// An error while parsing a URL.
89    #[error("invalid url: {0}")]
90    UrlError(#[from] url::ParseError),
91    /// The Blockbook version run on the provided server does not match the version required.
92    #[error("Blockbook version {client} required but server runs {server}.")]
93    VersionMismatch {
94        client: semver::Version,
95        server: semver::Version,
96    },
97    /// An error while preparing bdk wallet information.
98    #[cfg(feature = "bdk")]
99    #[error("bdk error: {0}")]
100    BdkError(String),
101}
102
103type Result<T> = std::result::Result<T, Error>;
104
105/// A REST client that can query a Blockbook server.
106///
107/// Provides a set methods that allow strongly typed
108/// access to the APIs available from a Blockbook server.
109///
110/// See the [`module documentation`] for some concrete examples
111/// of how to use call these APIs.
112///
113/// [`module documentation`]: crate
114pub struct Client {
115    base_url: url::Url,
116    client: reqwest::Client,
117}
118
119impl Client {
120    /// Constructs a new client for a given server `base_url`.
121    ///
122    /// `base_url` should not contain the `/api/v2/` path fragment.
123    ///
124    /// # Errors
125    ///
126    /// If the server status could not be retreived or the Blockbook
127    /// version run on the server does not match the version required.
128    pub async fn new(base_url: url::Url) -> Result<Self> {
129        let mut headers = reqwest::header::HeaderMap::new();
130        headers.insert(
131            reqwest::header::CONTENT_TYPE,
132            reqwest::header::HeaderValue::from_static("application/json"),
133        );
134        let client = Self {
135            base_url,
136            client: reqwest::Client::builder()
137                .default_headers(headers)
138                .timeout(std::time::Duration::from_secs(10))
139                .build()
140                .unwrap(),
141        };
142        let client_version = semver::Version::new(0, 4, 0);
143        let server_version = client.status().await?.blockbook.version;
144        if server_version != client_version {
145            return Err(Error::VersionMismatch {
146                client: client_version,
147                server: server_version,
148            });
149        }
150        Ok(client)
151    }
152
153    fn url(&self, endpoint: impl AsRef<str>) -> Result<url::Url> {
154        Ok(self.base_url.join(endpoint.as_ref())?)
155    }
156
157    async fn query<T: serde::de::DeserializeOwned>(&self, path: impl AsRef<str>) -> Result<T> {
158        Ok(self
159            .client
160            .get(self.url(path.as_ref())?)
161            .send()
162            .await?
163            .error_for_status()?
164            .json()
165            .await?)
166    }
167
168    /// Queries [information](https://github.com/trezor/blockbook/blob/95eb699ccbaeef0ec6d8fd0486de3445b8405e8a/docs/api.md#status-page) about the Blockbook server status.
169    ///
170    /// # Errors
171    ///
172    /// If the underlying network request fails, if the server returns a
173    /// non-success response, or if the response body is of unexpected format.
174    pub async fn status(&self) -> Result<Status> {
175        self.query("/api/v2").await
176    }
177
178    /// Retrieves the [`BlockHash`] of a block of the given `height`.
179    ///
180    /// # Errors
181    ///
182    /// If the underlying network request fails, if the server returns a
183    /// non-success response, or if the response body is of unexpected format.
184    pub async fn block_hash(&self, height: &Height) -> Result<BlockHash> {
185        #[derive(serde::Deserialize)]
186        #[serde(rename_all = "camelCase")]
187        struct BlockHashObject {
188            block_hash: BlockHash,
189        }
190        Ok(self
191            .query::<BlockHashObject>(format!("/api/v2/block-index/{height}"))
192            .await?
193            .block_hash)
194    }
195
196    /// Retrieves [information](https://github.com/trezor/blockbook/blob/95eb699ccbaeef0ec6d8fd0486de3445b8405e8a/docs/api.md#get-transaction)
197    /// about a transaction with a given `txid`.
198    ///
199    /// # Errors
200    ///
201    /// If the underlying network request fails, if the server returns a
202    /// non-success response, or if the response body is of unexpected format.
203    pub async fn transaction(&self, txid: &Txid) -> Result<Transaction> {
204        self.query(format!("/api/v2/tx/{txid}")).await
205    }
206
207    /// Retrieves [information](https://github.com/trezor/blockbook/blob/95eb699ccbaeef0ec6d8fd0486de3445b8405e8a/docs/api.md#get-transaction-specific)
208    /// about a transaction with a given `txid` as reported by the Bitcoin Core backend.
209    ///
210    /// # Errors
211    ///
212    /// If the underlying network request fails, if the server returns a
213    /// non-success response, or if the response body is of unexpected format.
214    pub async fn transaction_specific(&self, txid: &Txid) -> Result<TransactionSpecific> {
215        self.query(format!("/api/v2/tx-specific/{txid}")).await
216    }
217
218    /// Retrieves [information](https://github.com/trezor/blockbook/blob/86ff5a9538dba6b869f53850676f9edfc3cb5fa8/docs/api.md#get-block)
219    /// about a block of the specified `height`.
220    ///
221    /// # Errors
222    ///
223    /// If the underlying network request fails, if the server returns a
224    /// non-success response, or if the response body is of unexpected format.
225    pub async fn block_by_height(&self, height: &Height) -> Result<Block> {
226        self.query(format!("/api/v2/block/{height}")).await
227    }
228
229    /// Retrieves [information](https://github.com/trezor/blockbook/blob/86ff5a9538dba6b869f53850676f9edfc3cb5fa8/docs/api.md#get-block)
230    /// about a block with the specified `hash`.
231    ///
232    /// # Errors
233    ///
234    /// If the underlying network request fails, if the server returns a
235    /// non-success response, or if the response body is of unexpected format.
236    pub async fn block_by_hash(&self, hash: &BlockHash) -> Result<Block> {
237        self.query(format!("/api/v2/block/{hash}")).await
238    }
239
240    /// Retrieves a list of available price tickers close to a given `timestamp`.
241    ///
242    /// The API will return a tickers list that is as close as possible
243    /// to the specified `timestamp`.
244    ///
245    /// # Errors
246    ///
247    /// If the underlying network request fails, if the server returns a
248    /// non-success response, or if the response body is of unexpected format.
249    pub async fn tickers_list(&self, timestamp: &Time) -> Result<TickersList> {
250        self.query(format!("/api/v2/tickers-list/?timestamp={timestamp}"))
251            .await
252    }
253
254    /// Retrieves the exchange rate for a given `currency`.
255    ///
256    /// The API will return a ticker that is as close as possible to the provided
257    /// `timestamp`. If `timestamp` is `None`, the latest available ticker will be
258    /// returned.
259    ///
260    /// # Errors
261    ///
262    /// If the underlying network request fails, if the server returns a
263    /// non-success response, or if the response body is of unexpected format.
264    pub async fn ticker(&self, currency: &Currency, timestamp: Option<&Time>) -> Result<Ticker> {
265        let mut query_pairs = url::form_urlencoded::Serializer::new(String::new());
266        query_pairs.append_pair("currency", &format!("{currency:?}"));
267        if let Some(ts) = timestamp {
268            query_pairs.append_pair("timestamp", &ts.to_string());
269        }
270        self.query(format!("/api/v2/tickers?{}", query_pairs.finish()))
271            .await
272    }
273
274    /// Retrieves the exchange rates for all available currencies.
275    ///
276    /// The API will return tickers that are as close as possible to the provided
277    /// `timestamp`. If `timestamp` is `None`, the latest available tickers will be
278    /// returned.
279    ///
280    /// # Errors
281    ///
282    /// If the underlying network request fails, if the server returns a
283    /// non-success response, or if the response body is of unexpected format.
284    pub async fn tickers(&self, timestamp: Option<&Time>) -> Result<Ticker> {
285        self.query(format!(
286            "/api/v2/tickers/{}",
287            timestamp.map_or(String::new(), |ts| format!("?timestamp={ts}"))
288        ))
289        .await
290    }
291
292    /// Retrieves [basic aggregated information](https://github.com/trezor/blockbook/blob/211aeff22d6f9ce59b26895883aa85905bba566b/docs/api.md#get-address)
293    /// about a provided `address`.
294    ///
295    /// If an `also_in` [`Currency`] is specified, the total balance will also be returned in terms of that currency.
296    ///
297    /// # Errors
298    ///
299    /// If the underlying network request fails, if the server returns a
300    /// non-success response, or if the response body is of unexpected format.
301    pub async fn address_info_specific_basic(
302        &self,
303        address: &Address,
304        also_in: Option<&Currency>,
305    ) -> Result<AddressInfoBasic> {
306        let mut query_pairs = url::form_urlencoded::Serializer::new(String::new());
307        query_pairs.append_pair("details", "basic");
308        if let Some(currency) = also_in {
309            query_pairs.append_pair("secondary", &format!("{currency:?}"));
310        }
311        self.query(format!(
312            "/api/v2/address/{address}?{}",
313            query_pairs.finish()
314        ))
315        .await
316    }
317
318    /// Retrieves [basic aggregated information](https://github.com/trezor/blockbook/blob/211aeff22d6f9ce59b26895883aa85905bba566b/docs/api.md#get-address)
319    /// as well as a list of [`Txid`]s for a given `address`.
320    ///
321    /// The [`txids`] field of the response will be paged if the `address` was
322    /// involved in many transactions. In this case, use [`address_info_specific`]
323    /// to control the pagination.
324    ///
325    /// [`txids`]: AddressInfo::txids
326    /// [`address_info_specific`]: Client::address_info_specific
327    ///
328    /// # Errors
329    ///
330    /// If the underlying network request fails, if the server returns a
331    /// non-success response, or if the response body is of unexpected format.
332    pub async fn address_info(&self, address: &Address) -> Result<AddressInfo> {
333        self.query(format!("/api/v2/address/{address}")).await
334    }
335
336    /// Retrieves [basic aggregated information](https://github.com/trezor/blockbook/blob/211aeff22d6f9ce59b26895883aa85905bba566b/docs/api.md#get-address)
337    /// as well as a paginated list of [`Txid`]s for a given `address`.
338    ///
339    /// If an `also_in` [`Currency`] is specified, the total balance will also be returned in terms of that currency.
340    ///
341    /// # Errors
342    ///
343    /// If the underlying network request fails, if the server returns a
344    /// non-success response, or if the response body is of unexpected format.
345    pub async fn address_info_specific(
346        &self,
347        address: &Address,
348        page: Option<&std::num::NonZeroU32>,
349        pagesize: Option<&std::num::NonZeroU16>,
350        from: Option<&Height>,
351        to: Option<&Height>,
352        also_in: Option<&Currency>,
353    ) -> Result<AddressInfo> {
354        let mut query_pairs = url::form_urlencoded::Serializer::new(String::new());
355        if let Some(p) = page {
356            query_pairs.append_pair("page", &p.to_string());
357        }
358        if let Some(ps) = pagesize {
359            query_pairs.append_pair("pageSize", &ps.to_string());
360        }
361        if let Some(start_block) = from {
362            query_pairs.append_pair("from", &start_block.to_string());
363        }
364        if let Some(end_block) = to {
365            query_pairs.append_pair("to", &end_block.to_string());
366        }
367        if let Some(currency) = also_in {
368            query_pairs.append_pair("secondary", &format!("{currency:?}"));
369        }
370        self.query(format!(
371            "/api/v2/address/{address}?{}",
372            query_pairs.finish()
373        ))
374        .await
375    }
376
377    /// Retrieves [basic aggregated information](https://github.com/trezor/blockbook/blob/211aeff22d6f9ce59b26895883aa85905bba566b/docs/api.md#get-address)
378    /// as well as a paginated list of [`Tx`] objects for a given `address`.
379    ///
380    /// The `details` parameter specifies how much information should
381    /// be returned for the transactions in question:
382    /// - [`TxDetail::Light`]: A list of [`Tx::Light`] abbreviated transaction information
383    /// - [`TxDetail::Full`]: A list of [`Tx::Ordinary`] detailed transaction information
384    ///
385    /// # Errors
386    ///
387    /// If the underlying network request fails, if the server returns a
388    /// non-success response, or if the response body is of unexpected format.
389    #[allow(clippy::too_many_arguments)]
390    pub async fn address_info_specific_detailed(
391        &self,
392        address: &Address,
393        page: Option<&std::num::NonZeroU32>,
394        pagesize: Option<&std::num::NonZeroU16>,
395        from: Option<&Height>,
396        to: Option<&Height>,
397        details: &TxDetail,
398        also_in: Option<&Currency>,
399    ) -> Result<AddressInfo> {
400        let mut query_pairs = url::form_urlencoded::Serializer::new(String::new());
401        query_pairs.append_pair("details", details.as_str());
402        if let Some(p) = page {
403            query_pairs.append_pair("page", &p.to_string());
404        }
405        if let Some(ps) = pagesize {
406            query_pairs.append_pair("pageSize", &ps.to_string());
407        }
408        if let Some(start_block) = from {
409            query_pairs.append_pair("from", &start_block.to_string());
410        }
411        if let Some(end_block) = to {
412            query_pairs.append_pair("to", &end_block.to_string());
413        }
414        if let Some(currency) = also_in {
415            query_pairs.append_pair("secondary", &format!("{currency:?}"));
416        }
417        self.query(format!(
418            "/api/v2/address/{address}?{}",
419            query_pairs.finish()
420        ))
421        .await
422    }
423
424    /// Retrieves [information](https://github.com/trezor/blockbook/blob/78cf3c264782e60a147031c6ae80b3ab1f704783/docs/api.md#get-utxo)
425    /// about unspent transaction outputs (UTXOs) that a given address controls.
426    ///
427    /// # Errors
428    ///
429    /// If the underlying network request fails, if the server returns a
430    /// non-success response, or if the response body is of unexpected format.
431    pub async fn utxos_from_address(
432        &self,
433        address: &Address,
434        confirmed_only: bool,
435    ) -> Result<Vec<Utxo>> {
436        self.query(format!("/api/v2/utxo/{address}?confirmed={confirmed_only}"))
437            .await
438    }
439
440    /// Retrieves [information](https://github.com/trezor/blockbook/blob/78cf3c264782e60a147031c6ae80b3ab1f704783/docs/api.md#get-utxo)
441    /// about unspent transaction outputs (UTXOs) that are controlled by addresses that
442    /// can be derived from the given [`extended public key`].
443    ///
444    /// For details of how Blockbook attempts to derive addresses, see the
445    /// [`xpub_info_basic`] documentation.
446    ///
447    /// # Errors
448    ///
449    /// If the underlying network request fails, if the server returns a
450    /// non-success response, or if the response body is of unexpected format.
451    ///
452    /// [`extended public key`]: bitcoin::bip32::Xpub
453    /// [`xpub_info_basic`]: Client::xpub_info_basic
454    pub async fn utxos_from_xpub(&self, xpub: &str, confirmed_only: bool) -> Result<Vec<Utxo>> {
455        self.query(format!("/api/v2/utxo/{xpub}?confirmed={confirmed_only}"))
456            .await
457    }
458
459    /// Retrieves a paginated list of [information](https://github.com/trezor/blockbook/blob/78cf3c264782e60a147031c6ae80b3ab1f704783/docs/api.md#balance-history)
460    /// about the balance history of a given `address`.
461    ///
462    /// If a `currency` is specified, contemporary exchange rates
463    /// will be included for each balance history event.
464    ///
465    /// The history can be aggregated into chunks of time of a desired
466    /// length by specifying a `group_by` interval in seconds.
467    ///
468    /// # Errors
469    ///
470    /// If the underlying network request fails, if the server returns a
471    /// non-success response, or if the response body is of unexpected format.
472    ///
473    /// [`extended public key`]: bitcoin::bip32::Xpub
474    /// [`xpub_info_basic`]: Client::xpub_info_basic
475    pub async fn balance_history(
476        &self,
477        address: &Address,
478        from: Option<&Time>,
479        to: Option<&Time>,
480        currency: Option<&Currency>,
481        group_by: Option<u32>,
482    ) -> Result<Vec<BalanceHistory>> {
483        let mut query_pairs = url::form_urlencoded::Serializer::new(String::new());
484        if let Some(f) = from {
485            query_pairs.append_pair("from", &f.to_string());
486        }
487        if let Some(t) = to {
488            query_pairs.append_pair("to", &t.to_string());
489        }
490        if let Some(t) = currency {
491            query_pairs.append_pair(
492                "fiatcurrency",
493                serde_json::to_value(t).unwrap().as_str().unwrap(),
494            );
495        }
496        if let Some(gb) = group_by {
497            query_pairs.append_pair("groupBy", &gb.to_string());
498        }
499        self.query(format!(
500            "/api/v2/balancehistory/{address}?{}",
501            query_pairs.finish()
502        ))
503        .await
504    }
505
506    /// Broadcasts a transaction to the network, returning its [`Txid`].
507    ///
508    /// If you already have a serialized transaction, you can use this
509    /// API as follows:
510    /// ```no_run
511    /// # tokio_test::block_on(async {
512    /// # let raw_tx = vec![0_u8];
513    /// # let client = blockbook::Client::new("dummy:".parse().unwrap()).await?;
514    /// // Assuming you have a hex serialization of a transaction:
515    /// // let raw_tx = hex::decode(raw_tx_hex).unwrap();
516    /// let tx: bitcoin::Transaction = bitcoin::consensus::deserialize(&raw_tx).unwrap();
517    /// client.broadcast_transaction(&tx).await?;
518    /// # Ok::<_,blockbook::Error>(())
519    /// # });
520    /// ```
521    ///
522    /// # Errors
523    ///
524    /// If the underlying network request fails, if the server returns a
525    /// non-success response, or if the response body is of unexpected format.
526    pub async fn broadcast_transaction(&self, tx: &BitcoinTransaction) -> Result<Txid> {
527        #[derive(serde::Deserialize)]
528        struct Response {
529            result: Txid,
530        }
531        Ok(self
532            .query::<Response>(format!(
533                "/api/v2/sendtx/{}",
534                bitcoin::consensus::encode::serialize_hex(tx)
535            ))
536            .await?
537            .result)
538    }
539
540    /// Retrieves [information](https://github.com/trezor/blockbook/blob/211aeff22d6f9ce59b26895883aa85905bba566b/docs/api.md#get-xpub)
541    /// about the funds held by addresses from public keys derivable from an [`extended public key`].
542    ///
543    /// See the above link for more information about how the Blockbook server will
544    /// try to derive public keys and addresses from the extended public key.
545    /// Briefly, the extended key is expected to be derived at `m/purpose'/coin_type'/account'`,
546    /// and Blockbook will derive `change` and `index` levels below that, subject to a
547    /// gap limit of unused indices.
548    ///
549    /// In addition to the aggregated amounts, per-address indicators can
550    /// also be retrieved (Blockbook calls them `tokens`) by setting
551    /// `include_token_list`. The [`AddressFilter`] enum then allows selecting
552    /// the addresses holding a balance, addresses having been used, or all
553    /// addresses.
554    ///
555    /// If an `also_in` [`Currency`] is specified, the total balance will also be returned
556    /// in terms of that currency.
557    ///
558    /// # Errors
559    ///
560    /// If the underlying network request fails, if the server returns a
561    /// non-success response, or if the response body is of unexpected format.
562    ///
563    /// [`extended public key`]: bitcoin::bip32::Xpub
564    pub async fn xpub_info_basic(
565        &self,
566        xpub: &str,
567        include_token_list: bool,
568        address_filter: Option<&AddressFilter>,
569        also_in: Option<&Currency>,
570    ) -> Result<XPubInfoBasic> {
571        let mut query_pairs = url::form_urlencoded::Serializer::new(String::new());
572        query_pairs.append_pair(
573            "details",
574            if include_token_list {
575                "tokenBalances"
576            } else {
577                "basic"
578            },
579        );
580        if let Some(address_property) = address_filter {
581            query_pairs.append_pair("tokens", address_property.as_str());
582        }
583        if let Some(currency) = also_in {
584            query_pairs.append_pair("secondary", &format!("{currency:?}"));
585        }
586        self.query(format!("/api/v2/xpub/{xpub}?{}", query_pairs.finish()))
587            .await
588    }
589
590    /// Retrieves [information](https://github.com/trezor/blockbook/blob/211aeff22d6f9ce59b26895883aa85905bba566b/docs/api.md#get-xpub)
591    /// about the funds held by addresses from public keys derivable from an [`extended public key`],
592    /// as well as a paginated list of [`Txid`]s or [`Transaction`]s that affect addresses derivable from
593    /// the extended public key.
594    ///
595    /// [`Txid`]s or [`Transaction`]s are included in the returned [`XPubInfo`] based
596    /// on whether `entire_txs` is set to `false` or `true` respectively.
597    ///
598    /// For the other arguments, see the documentation of [`xpub_info_basic`].
599    ///
600    /// # Errors
601    ///
602    /// If the underlying network request fails, if the server returns a
603    /// non-success response, or if the response body is of unexpected format.
604    ///
605    /// [`extended public key`]: bitcoin::bip32::Xpub
606    /// [`xpub_info_basic`]: Client::xpub_info_basic
607    #[allow(clippy::too_many_arguments)]
608    pub async fn xpub_info(
609        &self,
610        xpub: &str,
611        page: Option<&std::num::NonZeroU32>,
612        pagesize: Option<&std::num::NonZeroU16>,
613        from: Option<&Height>,
614        to: Option<&Height>,
615        entire_txs: bool,
616        address_filter: Option<&AddressFilter>,
617        also_in: Option<&Currency>,
618    ) -> Result<XPubInfo> {
619        let mut query_pairs = url::form_urlencoded::Serializer::new(String::new());
620        if let Some(p) = page {
621            query_pairs.append_pair("page", &p.to_string());
622        }
623        if let Some(ps) = pagesize {
624            query_pairs.append_pair("pageSize", &ps.to_string());
625        }
626        if let Some(start_block) = from {
627            query_pairs.append_pair("from", &start_block.to_string());
628        }
629        if let Some(end_block) = to {
630            query_pairs.append_pair("to", &end_block.to_string());
631        }
632        query_pairs.append_pair("details", if entire_txs { "txs" } else { "txids" });
633        if let Some(address_property) = address_filter {
634            query_pairs.append_pair("tokens", address_property.as_str());
635        }
636        if let Some(currency) = also_in {
637            query_pairs.append_pair("secondary", &format!("{currency:?}"));
638        }
639        self.query(format!("/api/v2/xpub/{xpub}?{}", query_pairs.finish()))
640            .await
641    }
642}
643
644/// Aggregated information about funds held in addresses derivable from an [`extended public key`].
645///
646/// [`extended public key`]: bitcoin::bip32::Xpub
647#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
648#[serde(rename_all = "camelCase")]
649#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
650pub struct XPubInfoBasic {
651    pub address: String,
652    #[serde(with = "amount")]
653    pub balance: Amount,
654    #[serde(with = "amount")]
655    pub total_received: Amount,
656    #[serde(with = "amount")]
657    pub total_sent: Amount,
658    #[serde(with = "amount")]
659    pub unconfirmed_balance: Amount,
660    pub unconfirmed_txs: u32,
661    pub txs: u32,
662    #[serde(rename = "addrTxCount")]
663    pub used_addresses_count: usize,
664    pub used_tokens: u32,
665    pub secondary_value: Option<f64>,
666    pub tokens: Option<Vec<Token>>,
667}
668
669/// Information about funds at a Bitcoin address derived from an [`extended public key`].
670///
671/// [`extended public key`]: bitcoin::bip32::Xpub
672#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
673#[serde(rename_all = "camelCase")]
674pub struct Token {
675    pub r#type: String,
676    #[serde(rename = "name")]
677    #[serde(deserialize_with = "deserialize_address")]
678    pub address: Address,
679    pub path: DerivationPath,
680    pub transfers: u32,
681    pub decimals: u8,
682    #[serde(with = "amount")]
683    pub balance: Amount,
684    #[serde(with = "amount")]
685    pub total_received: Amount,
686    #[serde(with = "amount")]
687    pub total_sent: Amount,
688}
689
690/// Detailed information about funds held in addresses derivable from an [`extended public key`],
691///
692/// [`extended public key`]: bitcoin::bip32::Xpub
693#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
694#[serde(rename_all = "camelCase")]
695#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
696pub struct XPubInfo {
697    #[serde(flatten)]
698    pub paging: AddressInfoPaging,
699    #[serde(flatten)]
700    pub basic: XPubInfoBasic,
701    pub txids: Option<Vec<Txid>>,
702    pub transactions: Option<Vec<Transaction>>,
703}
704
705/// Used to select which addresses to consider when deriving from
706/// [`extended public keys`].
707///
708/// [`extended public keys`]: bitcoin::bip32::Xpub
709pub enum AddressFilter {
710    NonZero,
711    Used,
712    Derived,
713}
714
715impl AddressFilter {
716    fn as_str(&self) -> &'static str {
717        match self {
718            AddressFilter::NonZero => "nonzero",
719            AddressFilter::Used => "used",
720            AddressFilter::Derived => "derived",
721        }
722    }
723}
724
725/// Used to select the level of detail for [`address info`] transactions.
726///
727/// [`address info`]: Client::address_info_specific_detailed
728pub enum TxDetail {
729    Light,
730    Full,
731}
732
733impl TxDetail {
734    fn as_str(&self) -> &'static str {
735        match self {
736            TxDetail::Light => "txslight",
737            TxDetail::Full => "txs",
738        }
739    }
740}
741
742fn to_u32_option<'de, D>(deserializer: D) -> std::result::Result<Option<u32>, D::Error>
743where
744    D: serde::Deserializer<'de>,
745{
746    let value: i32 = serde::Deserialize::deserialize(deserializer)?;
747    Ok(u32::try_from(value).ok())
748}
749
750/// Paging information.
751#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
752#[serde(rename_all = "camelCase")]
753pub struct AddressInfoPaging {
754    pub page: u32,
755    /// The `total_pages` is unknown and hence set to `None` when
756    /// a block height filter is set and the number of transactions
757    /// is higher than the `pagesize` (default: 1000).
758    #[serde(deserialize_with = "to_u32_option")]
759    pub total_pages: Option<u32>,
760    pub items_on_page: u32,
761}
762
763/// Information about the transactional activity of an address.
764#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
765#[serde(rename_all = "camelCase")]
766pub struct AddressInfo {
767    #[serde(flatten)]
768    pub paging: AddressInfoPaging,
769    #[serde(flatten)]
770    pub basic: AddressInfoBasic,
771    pub txids: Option<Vec<Txid>>,
772    pub transactions: Option<Vec<Tx>>,
773}
774
775fn deserialize_address<'de, D>(deserializer: D) -> std::result::Result<Address, D::Error>
776where
777    D: serde::Deserializer<'de>,
778{
779    let unchecked_address: Address<NetworkUnchecked> =
780        serde::Deserialize::deserialize(deserializer)?;
781    unchecked_address
782        .require_network(bitcoin::Network::Bitcoin)
783        .map_err(|error| {
784            serde::de::Error::custom(
785                if let bitcoin::address::Error::NetworkValidation { found, .. } = error {
786                    format!("invalid address: network {found} is not supported")
787                } else {
788                    format!("unexpected error: {error}")
789                },
790            )
791        })
792}
793
794fn deserialize_optional_address<'de, D>(
795    deserializer: D,
796) -> std::result::Result<Option<Address>, D::Error>
797where
798    D: serde::Deserializer<'de>,
799{
800    #[derive(serde::Deserialize)]
801    struct Helper(#[serde(deserialize_with = "deserialize_address")] Address);
802
803    let helper_option: Option<Helper> = serde::Deserialize::deserialize(deserializer)?;
804    Ok(helper_option.map(|helper| helper.0))
805}
806
807/// Information about the funds moved from or to an address.
808#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
809#[serde(rename_all = "camelCase")]
810pub struct AddressInfoBasic {
811    #[serde(deserialize_with = "deserialize_address")]
812    pub address: Address,
813    #[serde(with = "amount")]
814    pub balance: Amount,
815    #[serde(with = "amount")]
816    pub total_received: Amount,
817    #[serde(with = "amount")]
818    pub total_sent: Amount,
819    #[serde(with = "amount")]
820    pub unconfirmed_balance: Amount,
821    pub unconfirmed_txs: u32,
822    pub txs: u32,
823    pub secondary_value: Option<f64>,
824}
825
826/// The variants for the transactions contained in [`AddressInfo::transactions`].
827#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
828#[serde(untagged)]
829pub enum Tx {
830    Ordinary(Transaction),
831    Light(BlockTransaction),
832}
833
834/// Information about an unspent transaction outputs.
835#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
836#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
837pub struct Utxo {
838    pub txid: Txid,
839    pub vout: u32,
840    #[serde(with = "amount")]
841    pub value: Amount,
842    pub height: Option<Height>,
843    pub confirmations: u32,
844    #[serde(rename = "lockTime")]
845    pub locktime: Option<Time>,
846    pub coinbase: Option<bool>,
847    #[serde(deserialize_with = "deserialize_optional_address")]
848    #[serde(default)]
849    pub address: Option<Address>,
850    pub path: Option<DerivationPath>,
851}
852
853/// A timestamp and a set of exchange rates for multiple currencies.
854#[derive(Clone, Debug, PartialEq, serde::Deserialize, serde::Serialize)]
855#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
856pub struct Ticker {
857    #[serde(rename = "ts")]
858    pub timestamp: Time,
859    pub rates: std::collections::HashMap<Currency, f64>,
860}
861
862/// Information about a block.
863#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
864#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
865#[serde(rename_all = "camelCase")]
866pub struct Block {
867    pub page: u32,
868    pub total_pages: u32,
869    pub items_on_page: u32,
870    pub hash: BlockHash,
871    pub previous_block_hash: Option<BlockHash>,
872    pub next_block_hash: Option<BlockHash>,
873    pub height: Height,
874    pub confirmations: u32,
875    pub size: u32,
876    pub time: Time,
877    pub version: bitcoin::blockdata::block::Version,
878    pub merkle_root: TxMerkleNode,
879    pub nonce: String,
880    pub bits: String,
881    pub difficulty: String,
882    pub tx_count: u32,
883    pub txs: Vec<BlockTransaction>,
884}
885
886/// Information about a transaction.
887#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
888#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
889#[serde(rename_all = "camelCase")]
890pub struct BlockTransaction {
891    pub txid: Txid,
892    pub vsize: u32,
893    pub vin: Vec<BlockVin>,
894    pub vout: Vec<BlockVout>,
895    pub block_hash: BlockHash,
896    pub block_height: Height,
897    pub confirmations: u32,
898    pub block_time: Time,
899    #[serde(with = "amount")]
900    pub value: Amount,
901    #[serde(with = "amount")]
902    pub value_in: Amount,
903    #[serde(with = "amount")]
904    pub fees: Amount,
905}
906
907fn deserialize_optional_address_vector<'de, D>(
908    deserializer: D,
909) -> std::result::Result<Option<Vec<Address>>, D::Error>
910where
911    D: serde::Deserializer<'de>,
912{
913    #[derive(serde::Deserialize)]
914    struct Helper(#[serde(deserialize_with = "deserialize_address_vector")] Vec<Address>);
915
916    let helper_option: Option<Helper> = serde::Deserialize::deserialize(deserializer)?;
917    Ok(helper_option.map(|helper| helper.0))
918}
919
920/// Information about a transaction input.
921#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
922#[serde(rename_all = "camelCase")]
923pub struct BlockVin {
924    pub n: u16,
925    /// Can be `None` or multiple addresses for a non-standard script,
926    /// where the latter indicates a multisig input
927    #[serde(deserialize_with = "deserialize_optional_address_vector")]
928    #[serde(default)]
929    pub addresses: Option<Vec<Address>>,
930    /// Indicates a [standard script](https://github.com/trezor/blockbook/blob/0ebbf16f18551f1c73b59bec6cfcbbdc96ec47e8/bchain/coins/btc/bitcoinlikeparser.go#L193-L194)
931    pub is_address: bool,
932    #[serde(with = "amount")]
933    pub value: Amount,
934}
935
936/// Information about a transaction output.
937#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
938#[serde(rename_all = "camelCase")]
939pub struct BlockVout {
940    #[serde(with = "amount")]
941    pub value: Amount,
942    pub n: u16,
943    pub spent: Option<bool>,
944    pub addresses: Vec<AddressBlockVout>,
945    /// Indicates the `addresses` vector to contain the `Address` `AddressBlockVout` variant
946    pub is_address: bool,
947}
948
949/// Either an address or an [`OP_RETURN output`].
950///
951/// [`OP_RETURN output`]: OpReturn
952#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
953#[serde(untagged)]
954pub enum AddressBlockVout {
955    #[serde(deserialize_with = "deserialize_address")]
956    Address(Address),
957    OpReturn(OpReturn),
958}
959
960/// An `OP_RETURN` output.
961#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
962pub struct OpReturn(pub String);
963
964/// A cryptocurrency asset.
965#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
966#[non_exhaustive]
967pub enum Asset {
968    Bitcoin,
969}
970
971/// Status and backend information of the Blockbook server.
972#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
973#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
974pub struct Status {
975    pub blockbook: StatusBlockbook,
976    pub backend: Backend,
977}
978
979/// Status information of the Blockbook server.
980#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
981#[serde(rename_all = "camelCase")]
982#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
983#[allow(clippy::struct_excessive_bools)]
984pub struct StatusBlockbook {
985    pub coin: Asset,
986    pub host: String,
987    pub version: semver::Version,
988    pub git_commit: String,
989    pub build_time: chrono::DateTime<chrono::Utc>,
990    pub sync_mode: bool,
991    #[serde(rename = "initialSync")]
992    pub is_initial_sync: bool,
993    #[serde(rename = "inSync")]
994    pub is_in_sync: bool,
995    pub best_height: crate::Height,
996    pub last_block_time: chrono::DateTime<chrono::Utc>,
997    #[serde(rename = "inSyncMempool")]
998    pub is_in_sync_mempool: bool,
999    pub last_mempool_time: chrono::DateTime<chrono::Utc>,
1000    pub mempool_size: u32,
1001    pub decimals: u8,
1002    pub db_size: u64,
1003    pub about: String,
1004    pub has_fiat_rates: bool,
1005    pub current_fiat_rates_time: chrono::DateTime<chrono::Utc>,
1006    pub historical_fiat_rates_time: chrono::DateTime<chrono::Utc>,
1007}
1008
1009/// The specific chain (mainnet, testnet, ...).
1010#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
1011#[non_exhaustive]
1012pub enum Chain {
1013    #[serde(rename = "main")]
1014    Main,
1015}
1016
1017/// Information about the full node backing the Blockbook server.
1018#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
1019#[serde(rename_all = "camelCase")]
1020#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1021pub struct Backend {
1022    pub chain: Chain,
1023    pub blocks: crate::Height,
1024    pub headers: u32,
1025    pub best_block_hash: crate::BlockHash,
1026    pub difficulty: String,
1027    pub size_on_disk: u64,
1028    #[serde(flatten)]
1029    pub version: Version,
1030    pub protocol_version: String,
1031}
1032
1033/// Version information about the full node.
1034#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
1035#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1036pub struct Version {
1037    pub version: String,
1038    pub subversion: String,
1039}
1040
1041mod amount {
1042    struct AmountVisitor;
1043
1044    impl<'de> serde::de::Visitor<'de> for AmountVisitor {
1045        type Value = super::Amount;
1046
1047        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1048            formatter.write_str("a valid Bitcoin amount")
1049        }
1050
1051        fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
1052        where
1053            E: serde::de::Error,
1054        {
1055            if let Ok(amount) = super::Amount::from_btc(value) {
1056                Ok(amount)
1057            } else {
1058                Err(E::custom("invalid Bitcoin amount"))
1059            }
1060        }
1061
1062        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
1063        where
1064            E: serde::de::Error,
1065        {
1066            if let Ok(amount) =
1067                super::Amount::from_str_in(value, bitcoin::amount::Denomination::Satoshi)
1068            {
1069                Ok(amount)
1070            } else {
1071                Err(E::custom("invalid Bitcoin amount"))
1072            }
1073        }
1074    }
1075
1076    pub(super) fn deserialize<'de, D>(deserializer: D) -> Result<super::Amount, D::Error>
1077    where
1078        D: serde::Deserializer<'de>,
1079    {
1080        deserializer.deserialize_any(AmountVisitor)
1081    }
1082
1083    #[allow(clippy::trivially_copy_pass_by_ref)]
1084    pub(super) fn serialize<S>(amount: &super::Amount, serializer: S) -> Result<S::Ok, S::Error>
1085    where
1086        S: serde::Serializer,
1087    {
1088        serializer.collect_str(&amount.to_sat().to_string())
1089    }
1090}
1091
1092/// Information about the available exchange rates at a given timestamp.
1093#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
1094#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1095pub struct TickersList {
1096    #[serde(rename = "ts")]
1097    pub timestamp: Time,
1098    pub available_currencies: Vec<Currency>,
1099}
1100
1101/// The supported currencies.
1102#[derive(Clone, Debug, Eq, PartialEq, Hash, serde::Deserialize, serde::Serialize)]
1103#[serde(rename_all = "lowercase")]
1104#[non_exhaustive]
1105pub enum Currency {
1106    Aed,
1107    Ars,
1108    Aud,
1109    Bch,
1110    Bdt,
1111    Bhd,
1112    Bits,
1113    Bmd,
1114    Bnb,
1115    Brl,
1116    Btc,
1117    Cad,
1118    Chf,
1119    Clp,
1120    Cny,
1121    Czk,
1122    Dkk,
1123    Dot,
1124    Eos,
1125    Eth,
1126    Eur,
1127    Gbp,
1128    Hkd,
1129    Huf,
1130    Idr,
1131    Ils,
1132    Inr,
1133    Jpy,
1134    Krw,
1135    Kwd,
1136    Link,
1137    Lkr,
1138    Ltc,
1139    Mmk,
1140    Mxn,
1141    Myr,
1142    Ngn,
1143    Nok,
1144    Nzd,
1145    Php,
1146    Pkr,
1147    Pln,
1148    Rub,
1149    Sar,
1150    Sats,
1151    Sek,
1152    Sgd,
1153    Thb,
1154    Try,
1155    Twd,
1156    Uah,
1157    Usd,
1158    Vef,
1159    Vnd,
1160    Xag,
1161    Xau,
1162    Xdr,
1163    Xlm,
1164    Xrp,
1165    Yfi,
1166    Zar,
1167}
1168
1169fn maybe_block_height<'de, D>(deserializer: D) -> std::result::Result<Option<Height>, D::Error>
1170where
1171    D: serde::Deserializer<'de>,
1172{
1173    Ok(to_u32_option(deserializer)?.and_then(|h| Height::from_consensus(h).ok()))
1174}
1175
1176/// Information about a transaction.
1177#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1178#[serde(rename_all = "camelCase")]
1179#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1180pub struct Transaction {
1181    pub txid: Txid,
1182    pub version: bitcoin::blockdata::transaction::Version,
1183    pub lock_time: Option<Height>,
1184    pub vin: Vec<Vin>,
1185    pub vout: Vec<Vout>,
1186    pub size: u32,
1187    pub vsize: u32,
1188    /// `None` for unconfirmed transactions
1189    pub block_hash: Option<BlockHash>,
1190    /// `None` for unconfirmed transactions
1191    #[serde(deserialize_with = "maybe_block_height")]
1192    pub block_height: Option<Height>,
1193    pub confirmations: u32,
1194    pub block_time: Time,
1195    #[serde(with = "amount")]
1196    pub value: Amount,
1197    #[serde(with = "amount")]
1198    pub value_in: Amount,
1199    #[serde(with = "amount")]
1200    pub fees: Amount,
1201    #[serde(rename = "hex")]
1202    pub script: ScriptBuf,
1203}
1204
1205fn deserialize_address_vector<'de, D>(
1206    deserializer: D,
1207) -> std::result::Result<Vec<Address>, D::Error>
1208where
1209    D: serde::Deserializer<'de>,
1210{
1211    #[derive(serde::Deserialize)]
1212    struct Helper(#[serde(deserialize_with = "deserialize_address")] Address);
1213
1214    let helper_vector: Vec<Helper> = serde::Deserialize::deserialize(deserializer)?;
1215    Ok(helper_vector.into_iter().map(|helper| helper.0).collect())
1216}
1217
1218/// Information about a transaction input.
1219#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1220#[serde(rename_all = "camelCase")]
1221pub struct Vin {
1222    pub txid: Txid,
1223    pub vout: Option<u16>,
1224    pub sequence: Option<Sequence>,
1225    pub n: u16,
1226    #[serde(deserialize_with = "deserialize_address_vector")]
1227    pub addresses: Vec<Address>,
1228    pub is_address: bool,
1229    #[serde(with = "amount")]
1230    pub value: Amount,
1231}
1232
1233/// Information about a transaction output.
1234#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1235#[serde(rename_all = "camelCase")]
1236#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1237pub struct Vout {
1238    #[serde(with = "amount")]
1239    pub value: Amount,
1240    pub n: u16,
1241    pub spent: Option<bool>,
1242    pub spent_tx_id: Option<Txid>,
1243    pub spent_height: Option<Height>,
1244    pub spent_index: Option<u16>,
1245    #[serde(rename = "hex")]
1246    pub script: ScriptBuf,
1247    #[serde(deserialize_with = "deserialize_address_vector")]
1248    pub addresses: Vec<Address>,
1249    pub is_address: bool,
1250    /// only present in an xpub context
1251    pub is_own: Option<bool>,
1252}
1253
1254/// Detailed information about a transaction input.
1255#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1256#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1257pub struct TransactionSpecific {
1258    pub txid: Txid,
1259    pub version: bitcoin::blockdata::transaction::Version,
1260    pub vin: Vec<VinSpecific>,
1261    pub vout: Vec<VoutSpecific>,
1262    pub blockhash: Option<BlockHash>,
1263    pub blocktime: Option<Time>,
1264    #[serde(rename = "hash")]
1265    pub wtxid: Wtxid,
1266    pub confirmations: Option<u32>,
1267    pub locktime: LockTime,
1268    #[serde(rename = "hex")]
1269    pub script: ScriptBuf,
1270    pub size: u32,
1271    pub time: Option<Time>,
1272    pub vsize: u32,
1273    pub weight: u32,
1274}
1275
1276impl From<TransactionSpecific> for BitcoinTransaction {
1277    fn from(tx: TransactionSpecific) -> Self {
1278        BitcoinTransaction {
1279            version: tx.version,
1280            lock_time: tx.locktime,
1281            input: tx.vin.into_iter().map(Into::into).collect(),
1282            output: tx.vout.into_iter().map(Into::into).collect(),
1283        }
1284    }
1285}
1286
1287/// Bitcoin-specific information about a transaction input.
1288#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1289#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1290pub struct VinSpecific {
1291    pub sequence: Sequence,
1292    pub txid: Txid,
1293    #[serde(rename = "txinwitness")]
1294    pub tx_in_witness: Option<Witness>,
1295    #[serde(rename = "scriptSig")]
1296    pub script_sig: ScriptSig,
1297    pub vout: u32,
1298}
1299
1300impl From<VinSpecific> for bitcoin::TxIn {
1301    fn from(vin: VinSpecific) -> Self {
1302        Self {
1303            previous_output: bitcoin::transaction::OutPoint {
1304                txid: vin.txid,
1305                vout: vin.vout,
1306            },
1307            script_sig: vin.script_sig.script,
1308            sequence: vin.sequence,
1309            witness: vin.tx_in_witness.unwrap_or_default(),
1310        }
1311    }
1312}
1313
1314/// A script fulfilling spending conditions.
1315#[derive(Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1316#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1317pub struct ScriptSig {
1318    pub asm: String,
1319    #[serde(rename = "hex")]
1320    pub script: ScriptBuf,
1321}
1322
1323/// Bitcoin-specific information about a transaction output.
1324#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1325#[serde(rename_all = "camelCase")]
1326#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1327pub struct VoutSpecific {
1328    pub n: u32,
1329    pub script_pub_key: ScriptPubKey,
1330    #[serde(with = "amount")]
1331    pub value: Amount,
1332}
1333
1334impl From<VoutSpecific> for bitcoin::TxOut {
1335    fn from(vout: VoutSpecific) -> Self {
1336        Self {
1337            value: vout.value,
1338            script_pubkey: vout.script_pub_key.script,
1339        }
1340    }
1341}
1342
1343/// A script specifying spending conditions.
1344#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1345#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1346pub struct ScriptPubKey {
1347    #[serde(deserialize_with = "deserialize_address")]
1348    pub address: Address,
1349    pub asm: String,
1350    pub desc: Option<String>,
1351    #[serde(rename = "hex")]
1352    pub script: ScriptBuf,
1353    pub r#type: ScriptPubKeyType,
1354}
1355
1356/// The type of spending condition.
1357#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
1358#[serde(rename_all = "lowercase")]
1359#[cfg_attr(feature = "test", serde(deny_unknown_fields))]
1360#[non_exhaustive]
1361pub enum ScriptPubKeyType {
1362    NonStandard,
1363    PubKey,
1364    PubKeyHash,
1365    #[serde(rename = "witness_v0_keyhash")]
1366    WitnessV0PubKeyHash,
1367    ScriptHash,
1368    #[serde(rename = "witness_v0_scripthash")]
1369    WitnessV0ScriptHash,
1370    MultiSig,
1371    NullData,
1372    #[serde(rename = "witness_v1_taproot")]
1373    WitnessV1Taproot,
1374    #[serde(rename = "witness_unknown")]
1375    WitnessUnknown,
1376}
1377
1378/// A balance history entry.
1379#[derive(Debug, PartialEq, serde::Deserialize)]
1380pub struct BalanceHistory {
1381    pub time: Time,
1382    pub txs: u32,
1383    #[serde(with = "amount")]
1384    pub received: Amount,
1385    #[serde(with = "amount")]
1386    pub sent: Amount,
1387    #[serde(rename = "sentToSelf")]
1388    #[serde(with = "amount")]
1389    pub sent_to_self: Amount,
1390    pub rates: std::collections::HashMap<Currency, f64>,
1391}
1392
1393#[cfg(test)]
1394mod test {
1395    #[test]
1396    fn serde_amounts() {
1397        #[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1398        struct TestStruct {
1399            #[serde(with = "super::amount")]
1400            pub amount: super::Amount,
1401        }
1402
1403        serde_test::assert_tokens(
1404            &TestStruct {
1405                amount: super::Amount::from_sat(123_456_789),
1406            },
1407            &[
1408                serde_test::Token::Struct {
1409                    name: "TestStruct",
1410                    len: 1,
1411                },
1412                serde_test::Token::Str("amount"),
1413                serde_test::Token::Str("123456789"),
1414                serde_test::Token::StructEnd,
1415            ],
1416        );
1417    }
1418}