Skip to main content

breez_sdk_spark/chain/
mod.rs

1use std::sync::Arc;
2
3use platform_utils::{DefaultHttpClient, HttpClient};
4use serde::{Deserialize, Serialize};
5use thiserror::Error;
6
7use crate::{
8    Credentials, Network,
9    chain::rest_client::{BasicAuth, ChainApiType, RestClientChainService},
10};
11
12pub mod rest_client;
13
14#[derive(Debug, Error, Clone)]
15#[cfg_attr(feature = "uniffi", derive(uniffi::Error))]
16pub enum ChainServiceError {
17    #[error("Invalid address: {0}")]
18    InvalidAddress(String),
19    #[error("Service connectivity: {0}")]
20    ServiceConnectivity(String),
21    #[error("Generic: {0}")]
22    Generic(String),
23}
24
25impl From<platform_utils::HttpError> for ChainServiceError {
26    fn from(value: platform_utils::HttpError) -> Self {
27        ChainServiceError::ServiceConnectivity(value.to_string())
28    }
29}
30
31impl From<bitcoin::address::ParseError> for ChainServiceError {
32    fn from(value: bitcoin::address::ParseError) -> Self {
33        ChainServiceError::InvalidAddress(value.to_string())
34    }
35}
36
37#[cfg_attr(feature = "uniffi", uniffi::export(with_foreign))]
38#[macros::async_trait]
39pub trait BitcoinChainService: Send + Sync {
40    async fn get_address_utxos(&self, address: String) -> Result<Vec<Utxo>, ChainServiceError>;
41    /// Every output ever paid to `address`, spent or not, unlike
42    /// [`get_address_utxos`](Self::get_address_utxos) which omits spent ones.
43    /// Recovers an output's outpoint and value after it has been spent, so a
44    /// swept refund can still be distinguished from one never broadcast.
45    async fn get_address_txos(&self, address: String) -> Result<Vec<Utxo>, ChainServiceError>;
46    async fn get_transaction_status(&self, txid: String) -> Result<TxStatus, ChainServiceError>;
47    async fn get_transaction_hex(&self, txid: String) -> Result<String, ChainServiceError>;
48    async fn get_outspend(&self, txid: String, vout: u32) -> Result<Outspend, ChainServiceError>;
49    async fn broadcast_transaction(&self, tx: String) -> Result<(), ChainServiceError>;
50    async fn recommended_fees(&self) -> Result<RecommendedFees, ChainServiceError>;
51}
52
53#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
54#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
55pub struct TxStatus {
56    pub confirmed: bool,
57    pub block_height: Option<u32>,
58    pub block_time: Option<u64>,
59}
60
61#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
62#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
63pub struct Utxo {
64    pub txid: String,
65    pub vout: u32,
66    pub value: u64,
67    pub status: TxStatus,
68}
69
70#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Eq)]
71#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
72pub struct RecommendedFees {
73    pub fastest_fee: u64,
74    pub half_hour_fee: u64,
75    pub hour_fee: u64,
76    pub economy_fee: u64,
77    pub minimum_fee: u64,
78}
79
80/// The spend status of a transaction output.
81#[derive(Clone, Debug, PartialEq, Eq)]
82#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
83pub enum Outspend {
84    Unspent,
85    /// The output is spent by input `vin` of transaction `txid`; `status` is
86    /// that spending transaction's confirmation status.
87    Spent {
88        txid: String,
89        vin: u32,
90        status: TxStatus,
91    },
92}
93
94/// Flat Esplora wire form, converted to/from [`Outspend`].
95#[derive(Deserialize, Serialize)]
96struct RawOutspend {
97    spent: bool,
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    txid: Option<String>,
100    #[serde(default, skip_serializing_if = "Option::is_none")]
101    vin: Option<u32>,
102    #[serde(default, skip_serializing_if = "Option::is_none")]
103    status: Option<TxStatus>,
104}
105
106impl<'de> Deserialize<'de> for Outspend {
107    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
108        use serde::de::Error;
109        let raw = RawOutspend::deserialize(deserializer)?;
110        if !raw.spent {
111            return Ok(Outspend::Unspent);
112        }
113        Ok(Outspend::Spent {
114            txid: raw.txid.ok_or_else(|| Error::missing_field("txid"))?,
115            vin: raw.vin.ok_or_else(|| Error::missing_field("vin"))?,
116            // A spent output whose spender status is omitted is treated as
117            // unconfirmed rather than rejected.
118            status: raw.status.unwrap_or(TxStatus {
119                confirmed: false,
120                block_height: None,
121                block_time: None,
122            }),
123        })
124    }
125}
126
127impl Serialize for Outspend {
128    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
129        let raw = match self {
130            Outspend::Unspent => RawOutspend {
131                spent: false,
132                txid: None,
133                vin: None,
134                status: None,
135            },
136            Outspend::Spent { txid, vin, status } => RawOutspend {
137                spent: true,
138                txid: Some(txid.clone()),
139                vin: Some(*vin),
140                status: Some(status.clone()),
141            },
142        };
143        raw.serialize(serializer)
144    }
145}
146
147/// Constructs a shareable REST-based [`BitcoinChainService`].
148///
149/// Pass the returned `Arc` to multiple [`SdkBuilder`](crate::SdkBuilder)s via
150/// [`SdkBuilder::with_chain_service`](crate::SdkBuilder::with_chain_service)
151/// to reuse a single underlying HTTP client (and its connection pool) across
152/// SDK instances. All SDKs sharing the service must use the same `network`.
153///
154/// For one-off, non-shared use, prefer
155/// [`SdkBuilder::with_rest_chain_service`](crate::SdkBuilder::with_rest_chain_service).
156#[cfg_attr(feature = "uniffi", uniffi::export(async_runtime = "tokio"))]
157#[must_use]
158pub async fn new_rest_chain_service(
159    url: String,
160    network: Network,
161    api_type: ChainApiType,
162    credentials: Option<Credentials>,
163) -> Arc<dyn BitcoinChainService> {
164    let http_client: Arc<dyn HttpClient> = Arc::new(DefaultHttpClient::default());
165    Arc::new(RestClientChainService::new(
166        url,
167        network,
168        5,
169        http_client,
170        credentials.map(|c| BasicAuth::new(c.username, c.password)),
171        api_type,
172    ))
173}