rustywallet_mempool/
client.rs

1//! Mempool.space API client.
2
3use std::time::Duration;
4
5use reqwest::Client;
6
7use crate::error::{MempoolError, Result};
8use crate::types::{AddressInfo, BlockInfo, FeeEstimates, Transaction, Utxo};
9
10/// Base URL for mainnet mempool.space API.
11pub const MAINNET_URL: &str = "https://mempool.space/api";
12/// Base URL for testnet mempool.space API.
13pub const TESTNET_URL: &str = "https://mempool.space/testnet/api";
14/// Base URL for signet mempool.space API.
15pub const SIGNET_URL: &str = "https://mempool.space/signet/api";
16
17/// Mempool.space API client.
18///
19/// Provides methods for querying fee estimates, address information,
20/// transactions, and broadcasting.
21///
22/// # Example
23/// ```no_run
24/// use rustywallet_mempool::MempoolClient;
25///
26/// #[tokio::main]
27/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
28///     let client = MempoolClient::new();
29///     
30///     // Get fee estimates
31///     let fees = client.get_fees().await?;
32///     println!("Next block fee: {} sat/vB", fees.fastest_fee);
33///     
34///     // Get address info
35///     let info = client.get_address("1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa").await?;
36///     println!("Balance: {} sats", info.confirmed_balance());
37///     
38///     Ok(())
39/// }
40/// ```
41pub struct MempoolClient {
42    client: Client,
43    base_url: String,
44}
45
46impl MempoolClient {
47    /// Create a new client for mainnet.
48    pub fn new() -> Self {
49        Self::with_base_url(MAINNET_URL)
50    }
51
52    /// Create a new client for testnet.
53    pub fn testnet() -> Self {
54        Self::with_base_url(TESTNET_URL)
55    }
56
57    /// Create a new client for signet.
58    pub fn signet() -> Self {
59        Self::with_base_url(SIGNET_URL)
60    }
61
62    /// Create a new client with custom base URL.
63    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    /// Make a GET request to the API.
76    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    /// Make a POST request to the API.
101    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    // ========== Fee Estimation ==========
131
132    /// Get recommended fee estimates.
133    ///
134    /// Returns fee rates in sat/vB for different confirmation targets.
135    pub async fn get_fees(&self) -> Result<FeeEstimates> {
136        self.get("/v1/fees/recommended").await
137    }
138
139    // ========== Address Methods ==========
140
141    /// Get address information including balance and transaction count.
142    pub async fn get_address(&self, address: &str) -> Result<AddressInfo> {
143        self.get(&format!("/address/{}", address)).await
144    }
145
146    /// Get UTXOs for an address.
147    pub async fn get_utxos(&self, address: &str) -> Result<Vec<Utxo>> {
148        self.get(&format!("/address/{}/utxo", address)).await
149    }
150
151    /// Get transaction history for an address.
152    ///
153    /// Returns up to 50 most recent transactions.
154    pub async fn get_address_txs(&self, address: &str) -> Result<Vec<Transaction>> {
155        self.get(&format!("/address/{}/txs", address)).await
156    }
157
158    // ========== Transaction Methods ==========
159
160    /// Get transaction details by txid.
161    pub async fn get_tx(&self, txid: &str) -> Result<Transaction> {
162        self.get(&format!("/tx/{}", txid)).await
163    }
164
165    /// Get raw transaction hex by txid.
166    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    /// Broadcast a signed transaction.
187    ///
188    /// # Arguments
189    /// * `hex` - Raw transaction in hex format
190    ///
191    /// # Returns
192    /// * Transaction ID on success
193    pub async fn broadcast(&self, hex: &str) -> Result<String> {
194        self.post("/tx", hex).await
195    }
196
197    // ========== Block Methods ==========
198
199    /// Get current block height.
200    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    /// Get block hash by height.
221    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    /// Get block information by hash.
242    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}