Skip to main content

breez_sdk_spark/chain/
mod.rs

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