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 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#[derive(Clone, Debug, PartialEq, Eq)]
82#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
83pub enum Outspend {
84 Unspent,
85 Spent {
88 txid: String,
89 vin: u32,
90 status: TxStatus,
91 },
92}
93
94#[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 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#[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}