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 async fn get<T: serde::de::DeserializeOwned>(&self, endpoint: &str) -> Result<T> {
77 let url = format!("{}{}", self.base_url, endpoint);
78
79 let response = self.client.get(&url).send().await?;
80
81 let status = response.status();
82 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
83 return Err(MempoolError::RateLimited);
84 }
85
86 if !status.is_success() {
87 let message = response.text().await.unwrap_or_default();
88 return Err(MempoolError::ApiError {
89 status: status.as_u16(),
90 message,
91 });
92 }
93
94 response
95 .json()
96 .await
97 .map_err(|e| MempoolError::ParseError(e.to_string()))
98 }
99
100 async fn post(&self, endpoint: &str, body: &str) -> Result<String> {
102 let url = format!("{}{}", self.base_url, endpoint);
103
104 let response = self.client
105 .post(&url)
106 .header("Content-Type", "text/plain")
107 .body(body.to_string())
108 .send()
109 .await?;
110
111 let status = response.status();
112 if status == reqwest::StatusCode::TOO_MANY_REQUESTS {
113 return Err(MempoolError::RateLimited);
114 }
115
116 if !status.is_success() {
117 let message = response.text().await.unwrap_or_default();
118 return Err(MempoolError::ApiError {
119 status: status.as_u16(),
120 message,
121 });
122 }
123
124 response
125 .text()
126 .await
127 .map_err(|e| MempoolError::ParseError(e.to_string()))
128 }
129
130 pub async fn get_fees(&self) -> Result<FeeEstimates> {
136 self.get("/v1/fees/recommended").await
137 }
138
139 pub async fn get_address(&self, address: &str) -> Result<AddressInfo> {
143 self.get(&format!("/address/{}", address)).await
144 }
145
146 pub async fn get_utxos(&self, address: &str) -> Result<Vec<Utxo>> {
148 self.get(&format!("/address/{}/utxo", address)).await
149 }
150
151 pub async fn get_address_txs(&self, address: &str) -> Result<Vec<Transaction>> {
155 self.get(&format!("/address/{}/txs", address)).await
156 }
157
158 pub async fn get_tx(&self, txid: &str) -> Result<Transaction> {
162 self.get(&format!("/tx/{}", txid)).await
163 }
164
165 pub async fn get_tx_hex(&self, txid: &str) -> Result<String> {
167 let url = format!("{}/tx/{}/hex", self.base_url, txid);
168
169 let response = self.client.get(&url).send().await?;
170
171 let status = response.status();
172 if !status.is_success() {
173 let message = response.text().await.unwrap_or_default();
174 return Err(MempoolError::ApiError {
175 status: status.as_u16(),
176 message,
177 });
178 }
179
180 response
181 .text()
182 .await
183 .map_err(|e| MempoolError::ParseError(e.to_string()))
184 }
185
186 pub async fn broadcast(&self, hex: &str) -> Result<String> {
194 self.post("/tx", hex).await
195 }
196
197 pub async fn get_block_height(&self) -> Result<u64> {
201 let url = format!("{}/blocks/tip/height", self.base_url);
202
203 let response = self.client.get(&url).send().await?;
204
205 let status = response.status();
206 if !status.is_success() {
207 let message = response.text().await.unwrap_or_default();
208 return Err(MempoolError::ApiError {
209 status: status.as_u16(),
210 message,
211 });
212 }
213
214 let text = response.text().await?;
215 text.trim()
216 .parse()
217 .map_err(|_| MempoolError::ParseError("Invalid block height".into()))
218 }
219
220 pub async fn get_block_hash(&self, height: u64) -> Result<String> {
222 let url = format!("{}/block-height/{}", self.base_url, height);
223
224 let response = self.client.get(&url).send().await?;
225
226 let status = response.status();
227 if !status.is_success() {
228 let message = response.text().await.unwrap_or_default();
229 return Err(MempoolError::ApiError {
230 status: status.as_u16(),
231 message,
232 });
233 }
234
235 response
236 .text()
237 .await
238 .map_err(|e| MempoolError::ParseError(e.to_string()))
239 }
240
241 pub async fn get_block(&self, hash: &str) -> Result<BlockInfo> {
243 self.get(&format!("/block/{}", hash)).await
244 }
245}
246
247impl Default for MempoolClient {
248 fn default() -> Self {
249 Self::new()
250 }
251}