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    /// Get the base URL.
76    pub fn base_url(&self) -> &str {
77        &self.base_url
78    }
79
80    /// Get the HTTP client (for internal use by extension modules).
81    pub fn http_client(&self) -> &Client {
82        &self.client
83    }
84
85    /// Make a GET request to the API.
86    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    /// Make a POST request to the API.
111    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    // ========== Fee Estimation ==========
141
142    /// Get recommended fee estimates.
143    ///
144    /// Returns fee rates in sat/vB for different confirmation targets.
145    pub async fn get_fees(&self) -> Result<FeeEstimates> {
146        self.get("/v1/fees/recommended").await
147    }
148
149    // ========== Address Methods ==========
150
151    /// Get address information including balance and transaction count.
152    pub async fn get_address(&self, address: &str) -> Result<AddressInfo> {
153        self.get(&format!("/address/{}", address)).await
154    }
155
156    /// Get UTXOs for an address.
157    pub async fn get_utxos(&self, address: &str) -> Result<Vec<Utxo>> {
158        self.get(&format!("/address/{}/utxo", address)).await
159    }
160
161    /// Get transaction history for an address.
162    ///
163    /// Returns up to 50 most recent transactions.
164    pub async fn get_address_txs(&self, address: &str) -> Result<Vec<Transaction>> {
165        self.get(&format!("/address/{}/txs", address)).await
166    }
167
168    // ========== Transaction Methods ==========
169
170    /// Get transaction details by txid.
171    pub async fn get_tx(&self, txid: &str) -> Result<Transaction> {
172        self.get(&format!("/tx/{}", txid)).await
173    }
174
175    /// Get raw transaction hex by txid.
176    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    /// Broadcast a signed transaction.
197    ///
198    /// # Arguments
199    /// * `hex` - Raw transaction in hex format
200    ///
201    /// # Returns
202    /// * Transaction ID on success
203    pub async fn broadcast(&self, hex: &str) -> Result<String> {
204        self.post("/tx", hex).await
205    }
206
207    // ========== Block Methods ==========
208
209    /// Get current block height.
210    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    /// Get block hash by height.
231    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    /// Get block information by hash.
252    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}