rustywallet_mempool/
client.rs1use std::time::Duration;
4
5use reqwest::Client;
6
7use crate::error::{MempoolError, Result};
8use crate::types::{AddressInfo, BlockInfo, FeeEstimates, Transaction, Utxo};
9
10pub const MAINNET_URL: &str = "https://mempool.space/api";
12pub const TESTNET_URL: &str = "https://mempool.space/testnet/api";
14pub const SIGNET_URL: &str = "https://mempool.space/signet/api";
16
17pub struct MempoolClient {
42 client: Client,
43 base_url: String,
44}
45
46impl MempoolClient {
47 pub fn new() -> Self {
49 Self::with_base_url(MAINNET_URL)
50 }
51
52 pub fn testnet() -> Self {
54 Self::with_base_url(TESTNET_URL)
55 }
56
57 pub fn signet() -> Self {
59 Self::with_base_url(SIGNET_URL)
60 }
61
62 pub fn with_base_url(base_url: &str) -> Self {
64 let client = Client::builder()
65 .timeout(Duration::from_secs(30))
66 .build()
67 .expect("Failed to create HTTP client");
68
69 Self {
70 client,
71 base_url: base_url.trim_end_matches('/').to_string(),
72 }
73 }
74
75 pub fn base_url(&self) -> &str {
77 &self.base_url
78 }
79
80 pub fn http_client(&self) -> &Client {
82 &self.client
83 }
84
85 async fn get<T: serde::de::DeserializeOwned>(&self, endpoint: &str) -> Result<T> {
87 let url = format!("{}{}", self.base_url, endpoint);
88
89 let response = self.client.get(&url).send().await?;
90
91 let status = response.status();
92 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
93 return Err(MempoolError::RateLimited);
94 }
95
96 if !status.is_success() {
97 let message = response.text().await.unwrap_or_default();
98 return Err(MempoolError::ApiError {
99 status: status.as_u16(),
100 message,
101 });
102 }
103
104 response
105 .json()
106 .await
107 .map_err(|e| MempoolError::ParseError(e.to_string()))
108 }
109
110 async fn post(&self, endpoint: &str, body: &str) -> Result<String> {
112 let url = format!("{}{}", self.base_url, endpoint);
113
114 let response = self.client
115 .post(&url)
116 .header("Content-Type", "text/plain")
117 .body(body.to_string())
118 .send()
119 .await?;
120
121 let status = response.status();
122 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
123 return Err(MempoolError::RateLimited);
124 }
125
126 if !status.is_success() {
127 let message = response.text().await.unwrap_or_default();
128 return Err(MempoolError::ApiError {
129 status: status.as_u16(),
130 message,
131 });
132 }
133
134 response
135 .text()
136 .await
137 .map_err(|e| MempoolError::ParseError(e.to_string()))
138 }
139
140 pub async fn get_fees(&self) -> Result<FeeEstimates> {
146 self.get("/v1/fees/recommended").await
147 }
148
149 pub async fn get_address(&self, address: &str) -> Result<AddressInfo> {
153 self.get(&format!("/address/{}", address)).await
154 }
155
156 pub async fn get_utxos(&self, address: &str) -> Result<Vec<Utxo>> {
158 self.get(&format!("/address/{}/utxo", address)).await
159 }
160
161 pub async fn get_address_txs(&self, address: &str) -> Result<Vec<Transaction>> {
165 self.get(&format!("/address/{}/txs", address)).await
166 }
167
168 pub async fn get_tx(&self, txid: &str) -> Result<Transaction> {
172 self.get(&format!("/tx/{}", txid)).await
173 }
174
175 pub async fn get_tx_hex(&self, txid: &str) -> Result<String> {
177 let url = format!("{}/tx/{}/hex", self.base_url, txid);
178
179 let response = self.client.get(&url).send().await?;
180
181 let status = response.status();
182 if !status.is_success() {
183 let message = response.text().await.unwrap_or_default();
184 return Err(MempoolError::ApiError {
185 status: status.as_u16(),
186 message,
187 });
188 }
189
190 response
191 .text()
192 .await
193 .map_err(|e| MempoolError::ParseError(e.to_string()))
194 }
195
196 pub async fn broadcast(&self, hex: &str) -> Result<String> {
204 self.post("/tx", hex).await
205 }
206
207 pub async fn get_block_height(&self) -> Result<u64> {
211 let url = format!("{}/blocks/tip/height", self.base_url);
212
213 let response = self.client.get(&url).send().await?;
214
215 let status = response.status();
216 if !status.is_success() {
217 let message = response.text().await.unwrap_or_default();
218 return Err(MempoolError::ApiError {
219 status: status.as_u16(),
220 message,
221 });
222 }
223
224 let text = response.text().await?;
225 text.trim()
226 .parse()
227 .map_err(|_| MempoolError::ParseError("Invalid block height".into()))
228 }
229
230 pub async fn get_block_hash(&self, height: u64) -> Result<String> {
232 let url = format!("{}/block-height/{}", self.base_url, height);
233
234 let response = self.client.get(&url).send().await?;
235
236 let status = response.status();
237 if !status.is_success() {
238 let message = response.text().await.unwrap_or_default();
239 return Err(MempoolError::ApiError {
240 status: status.as_u16(),
241 message,
242 });
243 }
244
245 response
246 .text()
247 .await
248 .map_err(|e| MempoolError::ParseError(e.to_string()))
249 }
250
251 pub async fn get_block(&self, hash: &str) -> Result<BlockInfo> {
253 self.get(&format!("/block/{}", hash)).await
254 }
255}
256
257impl Default for MempoolClient {
258 fn default() -> Self {
259 Self::new()
260 }
261}