1use std::collections::{HashMap, HashSet};
7use std::sync::{Arc, OnceLock};
8
9use async_trait::async_trait;
10use crate::chain_client::{ChainBlock, ChainClient};
11use crate::error::TransportError;
12use crate::method_safety::MethodSafety;
13use crate::request::{JsonRpcRequest, JsonRpcResponse};
14use crate::transport::{HealthStatus, RpcTransport};
15
16pub fn classify_bitcoin_method(method: &str) -> MethodSafety {
22 if bitcoin_unsafe_methods().contains(method) {
23 MethodSafety::Unsafe
24 } else if bitcoin_idempotent_methods().contains(method) {
25 MethodSafety::Idempotent
26 } else {
27 MethodSafety::Safe
28 }
29}
30
31pub fn is_bitcoin_safe_to_retry(method: &str) -> bool {
32 classify_bitcoin_method(method) == MethodSafety::Safe
33}
34
35pub fn is_bitcoin_safe_to_dedup(method: &str) -> bool {
36 classify_bitcoin_method(method) == MethodSafety::Safe
37}
38
39pub fn is_bitcoin_cacheable(method: &str) -> bool {
40 classify_bitcoin_method(method) == MethodSafety::Safe
41}
42
43fn bitcoin_unsafe_methods() -> &'static HashSet<&'static str> {
44 static UNSAFE: OnceLock<HashSet<&'static str>> = OnceLock::new();
45 UNSAFE.get_or_init(|| {
46 [
47 "walletpassphrase",
48 "encryptwallet",
49 "backupwallet",
50 "importprivkey",
51 ]
52 .into_iter()
53 .collect()
54 })
55}
56
57fn bitcoin_idempotent_methods() -> &'static HashSet<&'static str> {
58 static IDEMPOTENT: OnceLock<HashSet<&'static str>> = OnceLock::new();
59 IDEMPOTENT.get_or_init(|| {
60 ["sendrawtransaction"].into_iter().collect()
61 })
62}
63
64#[derive(Debug, Clone)]
69pub struct BitcoinCuCostTable {
70 costs: HashMap<String, u32>,
71 default_cost: u32,
72}
73
74impl BitcoinCuCostTable {
75 pub fn defaults() -> Self {
76 let mut table = Self::new(15);
77 let entries: &[(&str, u32)] = &[
78 ("getblockcount", 5),
79 ("getbestblockhash", 5),
80 ("getblockhash", 5),
81 ("getblock", 20),
82 ("getblockheader", 10),
83 ("getrawtransaction", 15),
84 ("gettxout", 10),
85 ("getmempoolinfo", 5),
86 ("getrawmempool", 20),
87 ("getnetworkinfo", 5),
88 ("getblockchaininfo", 10),
89 ("estimatesmartfee", 10),
90 ("sendrawtransaction", 10),
91 ("decoderawtransaction", 10),
92 ];
93 for &(method, cost) in entries {
94 table.costs.insert(method.to_string(), cost);
95 }
96 table
97 }
98
99 pub fn new(default_cost: u32) -> Self {
100 Self { costs: HashMap::new(), default_cost }
101 }
102
103 pub fn set_cost(&mut self, method: &str, cost: u32) {
104 self.costs.insert(method.to_string(), cost);
105 }
106
107 pub fn cost_for(&self, method: &str) -> u32 {
108 self.costs.get(method).copied().unwrap_or(self.default_cost)
109 }
110}
111
112impl Default for BitcoinCuCostTable {
113 fn default() -> Self {
114 Self::defaults()
115 }
116}
117
118pub fn bitcoin_mainnet_endpoints() -> &'static [&'static str] {
123 &[
124 "https://btc.getblock.io/mainnet/",
125 "https://bitcoin-mainnet.public.blastapi.io",
126 ]
127}
128
129pub fn bitcoin_testnet_endpoints() -> &'static [&'static str] {
130 &[
131 "https://btc.getblock.io/testnet/",
132 ]
133}
134
135pub struct BitcoinTransport {
141 inner: Arc<dyn RpcTransport>,
142}
143
144impl BitcoinTransport {
145 pub fn new(inner: Arc<dyn RpcTransport>) -> Self {
146 Self { inner }
147 }
148
149 pub fn inner(&self) -> &Arc<dyn RpcTransport> {
150 &self.inner
151 }
152}
153
154#[async_trait]
155impl RpcTransport for BitcoinTransport {
156 async fn send(&self, req: JsonRpcRequest) -> Result<JsonRpcResponse, TransportError> {
157 self.inner.send(req).await
158 }
159
160 async fn send_batch(&self, reqs: Vec<JsonRpcRequest>) -> Result<Vec<JsonRpcResponse>, TransportError> {
161 self.inner.send_batch(reqs).await
162 }
163
164 fn health(&self) -> HealthStatus { self.inner.health() }
165 fn url(&self) -> &str { self.inner.url() }
166}
167
168pub struct BitcoinChainClient {
174 transport: Arc<dyn RpcTransport>,
175 chain_id: String,
176}
177
178impl BitcoinChainClient {
179 pub fn new(transport: Arc<dyn RpcTransport>, chain_id: impl Into<String>) -> Self {
180 Self {
181 transport,
182 chain_id: chain_id.into(),
183 }
184 }
185}
186
187#[async_trait]
188impl ChainClient for BitcoinChainClient {
189 async fn get_head_height(&self) -> Result<u64, TransportError> {
190 let req = JsonRpcRequest::new(1, "getblockcount", vec![]);
191 let resp = self.transport.send(req).await?;
192 let result = resp.into_result().map_err(TransportError::Rpc)?;
193 result.as_u64().ok_or_else(|| {
194 TransportError::Other("expected u64 for block count".into())
195 })
196 }
197
198 async fn get_block_by_height(
199 &self,
200 height: u64,
201 ) -> Result<Option<ChainBlock>, TransportError> {
202 let hash_req = JsonRpcRequest::new(
204 1,
205 "getblockhash",
206 vec![serde_json::json!(height)],
207 );
208 let hash_resp = self.transport.send(hash_req).await?;
209 let hash_result = hash_resp.into_result().map_err(TransportError::Rpc)?;
210 let block_hash = match hash_result.as_str() {
211 Some(h) => h.to_string(),
212 None => return Ok(None),
213 };
214
215 let block_req = JsonRpcRequest::new(
217 1,
218 "getblock",
219 vec![
220 serde_json::Value::String(block_hash.clone()),
221 serde_json::json!(1), ],
223 );
224 let block_resp = self.transport.send(block_req).await?;
225 let result = block_resp.into_result().map_err(TransportError::Rpc)?;
226
227 if result.is_null() {
228 return Ok(None);
229 }
230
231 let parent_hash = result["previousblockhash"]
232 .as_str()
233 .unwrap_or_default()
234 .to_string();
235 let timestamp = result["time"].as_i64().unwrap_or(0);
236 let tx_count = result["tx"]
237 .as_array()
238 .map(|a| a.len() as u32)
239 .unwrap_or(0);
240
241 Ok(Some(ChainBlock {
242 height,
243 hash: block_hash,
244 parent_hash,
245 timestamp,
246 tx_count,
247 }))
248 }
249
250 fn chain_id(&self) -> &str {
251 &self.chain_id
252 }
253
254 fn chain_family(&self) -> &str {
255 "bitcoin"
256 }
257
258 async fn health_check(&self) -> Result<bool, TransportError> {
259 self.get_head_height().await.map(|_| true)
260 }
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266 use crate::request::RpcId;
267 use serde_json::Value;
268 use std::sync::Mutex;
269
270 struct MockTransport {
271 url: String,
272 responses: Mutex<Vec<JsonRpcResponse>>,
273 }
274
275 impl MockTransport {
276 fn new(responses: Vec<JsonRpcResponse>) -> Self {
277 Self { url: "mock://btc".to_string(), responses: Mutex::new(responses) }
278 }
279 }
280
281 #[async_trait]
282 impl RpcTransport for MockTransport {
283 async fn send(&self, _req: JsonRpcRequest) -> Result<JsonRpcResponse, TransportError> {
284 let mut r = self.responses.lock().unwrap();
285 if r.is_empty() { Err(TransportError::Other("no mock".into())) }
286 else { Ok(r.remove(0)) }
287 }
288 fn url(&self) -> &str { &self.url }
289 }
290
291 fn ok(result: Value) -> JsonRpcResponse {
292 JsonRpcResponse { jsonrpc: "2.0".into(), id: RpcId::Number(1), result: Some(result), error: None }
293 }
294
295 #[test]
296 fn classify() {
297 assert_eq!(classify_bitcoin_method("getblock"), MethodSafety::Safe);
298 assert_eq!(classify_bitcoin_method("sendrawtransaction"), MethodSafety::Idempotent);
299 assert_eq!(classify_bitcoin_method("walletpassphrase"), MethodSafety::Unsafe);
300 }
301
302 #[test]
303 fn cu_costs() {
304 let t = BitcoinCuCostTable::defaults();
305 assert_eq!(t.cost_for("getblockcount"), 5);
306 assert_eq!(t.cost_for("getblock"), 20);
307 }
308
309 #[tokio::test]
310 async fn btc_get_head_height() {
311 let t = Arc::new(MockTransport::new(vec![ok(serde_json::json!(830000u64))]));
312 let c = BitcoinChainClient::new(t, "mainnet");
313 assert_eq!(c.get_head_height().await.unwrap(), 830000);
314 }
315
316 #[tokio::test]
317 async fn btc_get_block() {
318 let t = Arc::new(MockTransport::new(vec![
319 ok(serde_json::Value::String("00000000abc".to_string())),
320 ok(serde_json::json!({
321 "previousblockhash": "00000000def",
322 "time": 1700000000,
323 "tx": ["tx1", "tx2"]
324 })),
325 ]));
326 let c = BitcoinChainClient::new(t, "mainnet");
327 let b = c.get_block_by_height(830000).await.unwrap().unwrap();
328 assert_eq!(b.hash, "00000000abc");
329 assert_eq!(b.parent_hash, "00000000def");
330 assert_eq!(b.tx_count, 2);
331 }
332
333 #[tokio::test]
334 async fn btc_metadata() {
335 let t = Arc::new(MockTransport::new(vec![]));
336 let c = BitcoinChainClient::new(t, "mainnet");
337 assert_eq!(c.chain_family(), "bitcoin");
338 }
339}