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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
89#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
90pub enum Outspend {
91 Unspent,
92 Spent {
95 txid: String,
96 vin: u32,
97 status: TxStatus,
98 },
99}
100
101#[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 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#[derive(Debug, Clone, Default)]
156#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
157pub struct NewRestChainServiceRequest {
158 #[cfg_attr(feature = "uniffi", uniffi(default = None))]
162 pub proxy: Option<crate::ProxyConfig>,
163}
164
165#[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}