1#![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
76pub mod websocket;
78
79#[cfg(feature = "bdk")]
80pub mod bdk;
81
82#[derive(Debug, thiserror::Error)]
84pub enum Error {
85 #[error("error occurred during the network request: {0}")]
87 RequestError(#[from] reqwest::Error),
88 #[error("invalid url: {0}")]
90 UrlError(#[from] url::ParseError),
91 #[error("Blockbook version {client} required but server runs {server}.")]
93 VersionMismatch {
94 client: semver::Version,
95 server: semver::Version,
96 },
97 #[cfg(feature = "bdk")]
99 #[error("bdk error: {0}")]
100 BdkError(String),
101}
102
103type Result<T> = std::result::Result<T, Error>;
104
105pub struct Client {
115 base_url: url::Url,
116 client: reqwest::Client,
117}
118
119impl Client {
120 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 pub async fn status(&self) -> Result<Status> {
175 self.query("/api/v2").await
176 }
177
178 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 pub async fn transaction(&self, txid: &Txid) -> Result<Transaction> {
204 self.query(format!("/api/v2/tx/{txid}")).await
205 }
206
207 pub async fn transaction_specific(&self, txid: &Txid) -> Result<TransactionSpecific> {
215 self.query(format!("/api/v2/tx-specific/{txid}")).await
216 }
217
218 pub async fn block_by_height(&self, height: &Height) -> Result<Block> {
226 self.query(format!("/api/v2/block/{height}")).await
227 }
228
229 pub async fn block_by_hash(&self, hash: &BlockHash) -> Result<Block> {
237 self.query(format!("/api/v2/block/{hash}")).await
238 }
239
240 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 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 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 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 pub async fn address_info(&self, address: &Address) -> Result<AddressInfo> {
333 self.query(format!("/api/v2/address/{address}")).await
334 }
335
336 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 #[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 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 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 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 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 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 #[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#[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#[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#[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
705pub 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
725pub 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#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
752#[serde(rename_all = "camelCase")]
753pub struct AddressInfoPaging {
754 pub page: u32,
755 #[serde(deserialize_with = "to_u32_option")]
759 pub total_pages: Option<u32>,
760 pub items_on_page: u32,
761}
762
763#[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#[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#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
828#[serde(untagged)]
829pub enum Tx {
830 Ordinary(Transaction),
831 Light(BlockTransaction),
832}
833
834#[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#[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#[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#[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#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
922#[serde(rename_all = "camelCase")]
923pub struct BlockVin {
924 pub n: u16,
925 #[serde(deserialize_with = "deserialize_optional_address_vector")]
928 #[serde(default)]
929 pub addresses: Option<Vec<Address>>,
930 pub is_address: bool,
932 #[serde(with = "amount")]
933 pub value: Amount,
934}
935
936#[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 pub is_address: bool,
947}
948
949#[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#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
962pub struct OpReturn(pub String);
963
964#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
966#[non_exhaustive]
967pub enum Asset {
968 Bitcoin,
969}
970
971#[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#[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#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
1011#[non_exhaustive]
1012pub enum Chain {
1013 #[serde(rename = "main")]
1014 Main,
1015}
1016
1017#[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#[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#[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#[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#[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 pub block_hash: Option<BlockHash>,
1190 #[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#[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#[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 pub is_own: Option<bool>,
1252}
1253
1254#[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#[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#[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#[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#[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#[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#[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}