bitcoin_rpc_midas/transport/
core.rs

1use serde_json::Value;
2use thiserror::Error;
3use {reqwest, serde};
4
5#[derive(Debug, Error, serde::Serialize, serde::Deserialize)]
6pub enum TransportError {
7    #[error("HTTP error: {0}")]
8    Http(String),
9    #[error("JSON error: {0}")]
10    Json(String),
11    #[error("RPC error: {0}")]
12    Rpc(String),
13}
14
15impl From<reqwest::Error> for TransportError {
16    fn from(err: reqwest::Error) -> Self { TransportError::Http(err.to_string()) }
17}
18
19impl From<serde_json::Error> for TransportError {
20    fn from(err: serde_json::Error) -> Self { TransportError::Json(err.to_string()) }
21}
22
23impl From<anyhow::Error> for TransportError {
24    fn from(err: anyhow::Error) -> Self { TransportError::Rpc(err.to_string()) }
25}
26
27pub trait TransportTrait: Send + Sync {
28    fn send_request<'a>(
29        &'a self,
30        method: &'a str,
31        params: &'a [Value],
32    ) -> std::pin::Pin<
33        Box<dyn std::future::Future<Output = Result<Value, TransportError>> + Send + 'a>,
34    >;
35
36    /// Send a **batch** of raw JSON-RPC objects in one HTTP call.
37    ///
38    /// The `bodies` slice is already serializable JSON-RPC-2.0 frames:
39    ///   [ { "jsonrpc":"2.0", "id":0, "method":"foo", "params": [...] }, … ]
40    fn send_batch<'a>(
41        &'a self,
42        bodies: &'a [Value],
43    ) -> std::pin::Pin<
44        Box<dyn std::future::Future<Output = Result<Vec<Value>, TransportError>> + Send + 'a>,
45    >;
46
47    fn url(&self) -> &str;
48}
49pub trait TransportExt {
50    fn call<'a, T: serde::de::DeserializeOwned>(
51        &'a self,
52        method: &'a str,
53        params: &'a [Value],
54    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T, TransportError>> + Send + 'a>>;
55}
56
57impl<T: TransportTrait> TransportExt for T {
58    fn call<'a, T2: serde::de::DeserializeOwned>(
59        &'a self,
60        method: &'a str,
61        params: &'a [Value],
62    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<T2, TransportError>> + Send + 'a>>
63    {
64        Box::pin(async move {
65            let result = self.send_request(method, params).await?;
66            Ok(serde_json::from_value(result)?)
67        })
68    }
69}
70
71#[derive(Clone, Debug)]
72pub struct DefaultTransport {
73    client: reqwest::Client,
74    url: String,
75    auth: Option<(String, String)>,
76    wallet_name: Option<String>,
77}
78
79impl DefaultTransport {
80    pub fn new(url: impl Into<String>, auth: Option<(String, String)>) -> Self {
81        Self { client: reqwest::Client::new(), url: url.into(), auth, wallet_name: None }
82    }
83
84    pub fn with_wallet(mut self, wallet_name: impl Into<String>) -> Self {
85        self.wallet_name = Some(wallet_name.into());
86        self
87    }
88}
89
90impl TransportTrait for DefaultTransport {
91    fn send_request<'a>(
92        &'a self,
93        method: &'a str,
94        params: &'a [Value],
95    ) -> std::pin::Pin<
96        Box<dyn std::future::Future<Output = Result<Value, TransportError>> + Send + 'a>,
97    > {
98        let client = self.client.clone();
99        let url = self.url.clone();
100        let auth = self.auth.clone();
101        let wallet_name = self.wallet_name.clone();
102        Box::pin(async move {
103            let request = serde_json::json!({
104                "jsonrpc": "2.0", "id": "1", "method": method, "params": params
105            });
106            eprintln!("[debug] Sending request to {}", url);
107
108            // If a wallet is configured, prefer wallet endpoint; fallback to base URL on -32601 (method not found)
109            if let Some(wallet) = &wallet_name {
110                let wallet_url = format!("{}/wallet/{}", url.trim_end_matches('/'), wallet);
111
112                // Try wallet endpoint first
113                let mut req = client.post(&wallet_url).json(&request);
114                if let Some((username, password)) = &auth {
115                    req = req.basic_auth(username, Some(password));
116                }
117                let response = match req.send().await {
118                    Ok(resp) => {
119                        eprintln!("[debug] Response status: {}", resp.status());
120                        resp
121                    }
122                    Err(e) => return Err(TransportError::Http(e.to_string())),
123                };
124
125                let text =
126                    response.text().await.map_err(|e| TransportError::Http(e.to_string()))?;
127                eprintln!("[debug] Response body: {}", text);
128                let json: Value =
129                    serde_json::from_str(&text).map_err(|e| TransportError::Json(e.to_string()))?;
130
131                if let Some(error) = json.get("error") {
132                    // Fallback only for -32601 (Method not found)
133                    if error.get("code").and_then(|c| c.as_i64()) == Some(-32601) {
134                        let mut req = client.post(&url).json(&request);
135                        if let Some((username, password)) = &auth {
136                            req = req.basic_auth(username, Some(password));
137                        }
138                        let response = match req.send().await {
139                            Ok(resp) => {
140                                eprintln!("[debug] Base response status: {}", resp.status());
141                                resp
142                            }
143                            Err(e) => return Err(TransportError::Http(e.to_string())),
144                        };
145                        let text = response
146                            .text()
147                            .await
148                            .map_err(|e| TransportError::Http(e.to_string()))?;
149                        eprintln!("[debug] Base response body: {}", text);
150                        let json: Value = serde_json::from_str(&text)
151                            .map_err(|e| TransportError::Json(e.to_string()))?;
152                        if let Some(error) = json.get("error") {
153                            return Err(TransportError::Rpc(error.to_string()));
154                        }
155                        return json
156                            .get("result")
157                            .cloned()
158                            .ok_or_else(|| TransportError::Rpc("No result field".to_string()));
159                    } else {
160                        return Err(TransportError::Rpc(error.to_string()));
161                    }
162                }
163
164                return json
165                    .get("result")
166                    .cloned()
167                    .ok_or_else(|| TransportError::Rpc("No result field".to_string()));
168            }
169
170            // No wallet configured → base URL
171            let mut req = client.post(&url).json(&request);
172            if let Some((username, password)) = &auth {
173                req = req.basic_auth(username, Some(password));
174            }
175            let response = match req.send().await {
176                Ok(resp) => {
177                    eprintln!("[debug] Response status: {}", resp.status());
178                    resp
179                }
180                Err(e) => return Err(TransportError::Http(e.to_string())),
181            };
182            let text = response.text().await.map_err(|e| TransportError::Http(e.to_string()))?;
183            eprintln!("[debug] Response body: {}", text);
184            let json: Value =
185                serde_json::from_str(&text).map_err(|e| TransportError::Json(e.to_string()))?;
186            if let Some(error) = json.get("error") {
187                return Err(TransportError::Rpc(error.to_string()));
188            }
189            json.get("result")
190                .cloned()
191                .ok_or_else(|| TransportError::Rpc("No result field".to_string()))
192        })
193    }
194
195    fn send_batch<'a>(
196        &'a self,
197        bodies: &'a [Value],
198    ) -> std::pin::Pin<
199        Box<dyn std::future::Future<Output = Result<Vec<Value>, TransportError>> + Send + 'a>,
200    > {
201        let client = self.client.clone();
202        let url = self.url.clone();
203        let auth = self.auth.clone();
204        Box::pin(async move {
205            eprintln!("[debug] Sending batch request to {}: {:?}", url, bodies);
206            let mut req = client.post(&url).json(bodies);
207            if let Some((username, password)) = &auth {
208                req = req.basic_auth(username, Some(password));
209            }
210            let response = match req.send().await {
211                Ok(resp) => {
212                    eprintln!("[debug] Batch response status: {}", resp.status());
213                    resp
214                }
215                Err(e) => return Err(TransportError::Http(e.to_string())),
216            };
217            let text = response.text().await.map_err(|e| TransportError::Http(e.to_string()))?;
218            eprintln!("[debug] Batch response body: {}", text);
219            let v: Vec<Value> =
220                serde_json::from_str(&text).map_err(|e| TransportError::Json(e.to_string()))?;
221            Ok(v)
222        })
223    }
224
225    fn url(&self) -> &str { &self.url }
226}