spark_rust/wallet/
mempool.rs

1//! Mempool API client for Bitcoin blockchain data
2//!
3//! Provides a client for interacting with the Mempool.space API to retrieve
4//! Bitcoin blockchain and transaction data. Supports both mainnet and regtest
5//! environments.
6
7use reqwest::Response;
8use url::Url;
9
10use crate::{
11    error::{NetworkError, SparkSdkError},
12    SparkNetwork,
13};
14
15/// Public Mempool.space API endpoint
16pub const MEMPOOL_ENDPOINT: &str = "https://mempool.space";
17
18/// Regtest Mempool endpoint for testing
19pub const LIGHTSPARK_MEMPOOL_ENDPOINT: &str = "https://regtest-mempool.dev.dev.sparkinfra.net";
20pub const LIGHTSPARK_REGTEST_MEMPOOL_USERNAME: &str = "spark-sdk";
21pub const LIGHTSPARK_REGTEST_MEMPOOL_PASSWORD: &str = "mCMk1JqlBNtetUNy";
22
23/// Client for making requests to the Mempool API
24pub(crate) struct MempoolClient {
25    regtest_base_url: Url,
26    regtest_username: String,
27    regtest_password: String,
28}
29
30impl MempoolClient {
31    /// Creates a new Mempool API client
32    ///
33    /// Uses environment variables for configuration if available:
34    /// - LIGHTSPARK_MEMPOOL_ENDPOINT
35    /// - LIGHTSPARK_REGTEST_MEMPOOL_USERNAME
36    /// - LIGHTSPARK_REGTEST_MEMPOOL_PASSWORD
37    pub(crate) fn new() -> Result<Self, SparkSdkError> {
38        let regtest_base_url = {
39            let url = std::env::var("LIGHTSPARK_MEMPOOL_ENDPOINT")
40                .unwrap_or(LIGHTSPARK_MEMPOOL_ENDPOINT.to_string());
41            Url::parse(&url).map_err(|_| {
42                SparkSdkError::from(NetworkError::InvalidUrl {
43                    url: url.to_string(),
44                    details: None,
45                })
46            })?
47        };
48
49        let regtest_username = std::env::var("LIGHTSPARK_REGTEST_MEMPOOL_USERNAME")
50            .unwrap_or(LIGHTSPARK_REGTEST_MEMPOOL_USERNAME.to_string());
51        let regtest_password = std::env::var("LIGHTSPARK_REGTEST_MEMPOOL_PASSWORD")
52            .unwrap_or(LIGHTSPARK_REGTEST_MEMPOOL_PASSWORD.to_string());
53
54        Ok(Self {
55            regtest_base_url,
56            regtest_username,
57            regtest_password,
58        })
59    }
60
61    /// Makes a GET request to the Mempool API
62    ///
63    /// # Arguments
64    /// * `network` - Bitcoin network to use (mainnet or regtest)
65    /// * `path` - API path to request, without the leading "api/"
66    pub(crate) async fn request(
67        &self,
68        network: SparkNetwork,
69        path: &str,
70    ) -> Result<Response, SparkSdkError> {
71        let client = reqwest::Client::new();
72        let url = match network {
73            SparkNetwork::Regtest => self.regtest_base_url.clone(),
74            SparkNetwork::Mainnet => Url::parse(MEMPOOL_ENDPOINT).unwrap(),
75        };
76
77        let url = url.join(&format!("api/{}", path)).map_err(|err| {
78            SparkSdkError::from(NetworkError::InvalidUrl {
79                url: url.to_string(),
80                details: Some(err.to_string()),
81            })
82        })?;
83
84        let mut request = client.get(url).header("Content-Type", "application/json");
85
86        if network == SparkNetwork::Regtest {
87            request = request.basic_auth(&self.regtest_username, Some(&self.regtest_password));
88        }
89
90        request
91            .send()
92            .await
93            .and_then(|res| res.error_for_status())
94            .map_err(|err| SparkSdkError::from(NetworkError::Http(err)))
95    }
96
97    /// Makes a request and returns the response as a string
98    ///
99    /// # Arguments
100    /// * `network` - Bitcoin network to use
101    /// * `path` - API path to request
102    pub(crate) async fn request_text(
103        &self,
104        network: SparkNetwork,
105        path: &str,
106    ) -> Result<String, SparkSdkError> {
107        self.request(network, path)
108            .await?
109            .text()
110            .await
111            .map_err(|err| SparkSdkError::from(NetworkError::Http(err)))
112    }
113
114    /// Makes a request and parses the JSON response into the specified type
115    ///
116    /// # Arguments
117    /// * `network` - Bitcoin network to use
118    /// * `path` - API path to request
119    ///
120    /// # Type Parameters
121    /// * `T` - The type to deserialize the JSON response into
122    pub(crate) async fn request_json<T>(
123        &self,
124        network: SparkNetwork,
125        path: &str,
126    ) -> Result<T, SparkSdkError>
127    where
128        T: serde::de::DeserializeOwned,
129    {
130        self.request(network, path)
131            .await?
132            .json()
133            .await
134            .map_err(|err| SparkSdkError::from(NetworkError::Http(err)))
135    }
136}