1use std::{thread::sleep, time::Duration};
2
3use bitcoin::{consensus::encode, hex::FromHex};
4use brk_error::{Error, Result};
5use brk_types::{
6 Bitcoin, BlockHash, FeeRate, Height, MempoolEntryInfo, Sats, Timestamp, Txid, VSize, Vout,
7 Weight,
8};
9use corepc_jsonrpc::error::Error as JsonRpcError;
10use corepc_types::{
11 v17::{
12 BlockTemplateTransaction, GetBlockCount, GetBlockHash, GetBlockHeader,
13 GetBlockHeaderVerbose, GetBlockTemplate, GetBlockVerboseOne, GetBlockVerboseZero,
14 GetRawMempool, GetTxOut,
15 },
16 v28::GetBlockchainInfo,
17 v24::{GetMempoolInfo, MempoolEntry},
18};
19use rustc_hash::FxHashMap;
20use serde_json::Value;
21use tracing::{debug, info};
22
23const RPC_NOT_FOUND: i32 = -5;
27
28use crate::{BlockTemplateTx, Client};
29
30const BATCH_CHUNK: usize = 2000;
37
38pub struct MempoolState {
43 pub live_txids: Vec<Txid>,
44 pub min_fee: FeeRate,
45 pub tip_hash: BlockHash,
48 pub tip_height: Height,
50}
51
52fn build_entry(txid: Txid, e: MempoolEntry) -> Result<MempoolEntryInfo> {
53 let depends = e
54 .depends
55 .iter()
56 .map(|s| Client::parse_txid(s, "depends txid"))
57 .collect::<Result<Vec<_>>>()?;
58 Ok(MempoolEntryInfo {
59 txid,
60 vsize: VSize::from(e.vsize as u64),
61 weight: Weight::from(e.weight as u64),
62 fee: Sats::from(Bitcoin::from(e.fees.base)),
63 first_seen: Timestamp::from(e.time),
64 depends,
65 })
66}
67
68fn build_gbt(raw: GetBlockTemplate) -> Result<Vec<BlockTemplateTx>> {
69 let n = raw.transactions.len();
73 let mut depends_idx: Vec<Vec<i64>> = Vec::with_capacity(n);
74 let mut result: Vec<BlockTemplateTx> = Vec::with_capacity(n);
75 for t in raw.transactions {
76 let BlockTemplateTransaction {
77 data,
78 txid,
79 depends,
80 fee,
81 weight,
82 ..
83 } = t;
84 depends_idx.push(depends);
85 result.push(BlockTemplateTx {
86 txid: Client::parse_txid(&txid, "gbt txid")?,
87 fee: Sats::from(fee as u64),
88 weight: Weight::from(weight),
89 depends: Vec::new(),
90 tx: encode::deserialize_hex(&data)?,
91 });
92 }
93 for (i, indices) in depends_idx.iter().enumerate() {
95 let resolved: Vec<Txid> = indices
96 .iter()
97 .filter_map(|d| {
98 let idx = usize::try_from(*d).ok()?.checked_sub(1)?;
99 result.get(idx).map(|t| t.txid)
100 })
101 .collect();
102 result[i].depends = resolved;
103 }
104 Ok(result)
105}
106
107fn build_min_fee(raw: GetMempoolInfo) -> FeeRate {
112 let sat_per_kvb = (raw.mempool_min_fee * 100_000_000.0).round() as u64;
113 FeeRate::from(sat_per_kvb as f64 / 1000.0)
114}
115
116impl Client {
117 pub fn get_block_count(&self) -> Result<u64> {
119 let r: GetBlockCount = self.0.call_with_retry("getblockcount", &[])?;
120 Ok(r.0)
121 }
122
123 pub fn get_last_height(&self) -> Result<Height> {
125 self.get_block_count().map(Height::from)
126 }
127
128 pub fn get_block<'a, H>(&self, hash: &'a H) -> Result<bitcoin::Block>
129 where
130 &'a H: Into<&'a bitcoin::BlockHash>,
131 {
132 let hash: &bitcoin::BlockHash = hash.into();
133 let r: GetBlockVerboseZero = self
134 .0
135 .call_with_retry("getblock", &[serde_json::to_value(hash)?, Value::from(0u8)])?;
136 r.block()
137 .map_err(|e| Error::Parse(format!("decode getblock: {e}")))
138 }
139
140 pub fn get_block_info<'a, H>(&self, hash: &'a H) -> Result<GetBlockVerboseOne>
141 where
142 &'a H: Into<&'a bitcoin::BlockHash>,
143 {
144 let hash: &bitcoin::BlockHash = hash.into();
145 self.0
146 .call_with_retry("getblock", &[serde_json::to_value(hash)?, Value::from(1u8)])
147 }
148
149 pub fn get_block_header<'a, H>(&self, hash: &'a H) -> Result<bitcoin::block::Header>
150 where
151 &'a H: Into<&'a bitcoin::BlockHash>,
152 {
153 let hash: &bitcoin::BlockHash = hash.into();
154 let r: GetBlockHeader = self.0.call_with_retry(
155 "getblockheader",
156 &[serde_json::to_value(hash)?, Value::Bool(false)],
157 )?;
158 let bytes = Vec::from_hex(&r.0).map_err(|e| Error::Parse(format!("header hex: {e}")))?;
159 bitcoin::consensus::deserialize::<bitcoin::block::Header>(&bytes).map_err(Error::from)
160 }
161
162 pub fn get_block_header_info<'a, H>(&self, hash: &'a H) -> Result<GetBlockHeaderVerbose>
163 where
164 &'a H: Into<&'a bitcoin::BlockHash>,
165 {
166 let hash: &bitcoin::BlockHash = hash.into();
167 self.0
168 .call_with_retry("getblockheader", &[serde_json::to_value(hash)?])
169 }
170
171 pub fn get_block_hash<H>(&self, height: H) -> Result<BlockHash>
172 where
173 H: Into<u64> + Copy,
174 {
175 let height: u64 = height.into();
176 let r: GetBlockHash = self
177 .0
178 .call_with_retry("getblockhash", &[serde_json::to_value(height)?])?;
179 Ok(BlockHash::from(r.block_hash()?))
180 }
181
182 pub fn get_block_hashes_range<H1, H2>(&self, start: H1, end: H2) -> Result<Vec<BlockHash>>
189 where
190 H1: Into<u64>,
191 H2: Into<u64>,
192 {
193 let start: u64 = start.into();
194 let end: u64 = end.into();
195 if end < start {
196 return Ok(Vec::new());
197 }
198 let total = (end - start + 1) as usize;
199 let mut hashes = Vec::with_capacity(total);
200
201 let mut chunk_start = start;
202 while chunk_start <= end {
203 let chunk_end = (chunk_start + BATCH_CHUNK as u64 - 1).min(end);
204 let args = (chunk_start..=chunk_end).map(|h| vec![Value::from(h)]);
205 let chunk: Vec<String> = self.0.call_batch("getblockhash", args)?;
206 for hex in chunk {
207 hashes.push(Self::parse_block_hash(&hex, "getblockhash batch")?);
208 }
209 chunk_start = chunk_end + 1;
210 }
211 Ok(hashes)
212 }
213
214 pub fn get_tx_out(
215 &self,
216 txid: &Txid,
217 vout: Vout,
218 include_mempool: Option<bool>,
219 ) -> Result<Option<GetTxOut>> {
220 let txid: &bitcoin::Txid = txid.into();
221 let mut args: Vec<Value> = vec![
222 serde_json::to_value(txid)?,
223 serde_json::to_value(u32::from(vout))?,
224 ];
225 if let Some(mempool) = include_mempool {
226 args.push(Value::Bool(mempool));
227 }
228 self.0.call_with_retry("gettxout", &args)
229 }
230
231 pub fn get_raw_mempool(&self) -> Result<Vec<Txid>> {
232 let r: GetRawMempool = self.0.call_with_retry("getrawmempool", &[])?;
233 r.0.iter()
234 .map(|s| Self::parse_txid(s, "mempool txid"))
235 .collect()
236 }
237
238 pub fn get_raw_transaction<'a, T>(&self, txid: &'a T) -> Result<bitcoin::Transaction>
239 where
240 &'a T: Into<&'a bitcoin::Txid>,
241 {
242 let hex = self.get_raw_transaction_hex(txid)?;
243 Ok(encode::deserialize_hex::<bitcoin::Transaction>(&hex)?)
244 }
245
246 pub fn get_raw_transaction_from<'a, T, H>(
247 &self,
248 txid: &'a T,
249 block_hash: &'a H,
250 ) -> Result<bitcoin::Transaction>
251 where
252 &'a T: Into<&'a bitcoin::Txid>,
253 &'a H: Into<&'a bitcoin::BlockHash>,
254 {
255 let hex = self.get_raw_transaction_hex_from(txid, block_hash)?;
256 Ok(encode::deserialize_hex::<bitcoin::Transaction>(&hex)?)
257 }
258
259 pub fn get_raw_transaction_hex<'a, T>(&self, txid: &'a T) -> Result<String>
260 where
261 &'a T: Into<&'a bitcoin::Txid>,
262 {
263 let txid: &bitcoin::Txid = txid.into();
264 let args = [serde_json::to_value(txid)?, Value::Bool(false)];
265 self.0.call_with_retry("getrawtransaction", &args)
266 }
267
268 pub fn get_raw_transaction_hex_from<'a, T, H>(
269 &self,
270 txid: &'a T,
271 block_hash: &'a H,
272 ) -> Result<String>
273 where
274 &'a T: Into<&'a bitcoin::Txid>,
275 &'a H: Into<&'a bitcoin::BlockHash>,
276 {
277 let txid: &bitcoin::Txid = txid.into();
278 let bh: &bitcoin::BlockHash = block_hash.into();
279 let args = [
280 serde_json::to_value(txid)?,
281 Value::Bool(false),
282 serde_json::to_value(bh)?,
283 ];
284 self.0.call_with_retry("getrawtransaction", &args)
285 }
286
287 pub fn get_mempool_raw_tx(&self, txid: &Txid) -> Result<bitcoin::Transaction> {
288 self.get_raw_transaction(txid)
289 }
290
291 pub fn get_raw_transactions(
298 &self,
299 txids: &[Txid],
300 ) -> Result<FxHashMap<Txid, bitcoin::Transaction>> {
301 let mut out: FxHashMap<Txid, bitcoin::Transaction> =
302 FxHashMap::with_capacity_and_hasher(txids.len(), Default::default());
303
304 for chunk in txids.chunks(BATCH_CHUNK) {
305 let args = chunk.iter().map(|t| {
306 let bt: &bitcoin::Txid = t.into();
307 vec![
308 serde_json::to_value(bt).unwrap_or(Value::Null),
309 Value::Bool(false),
310 ]
311 });
312 let results: Vec<Result<String>> =
313 self.0.call_batch_per_item("getrawtransaction", args)?;
314
315 for (txid, res) in chunk.iter().zip(results) {
316 match res.and_then(|hex| {
317 Ok(encode::deserialize_hex::<bitcoin::Transaction>(&hex)?)
318 }) {
319 Ok(tx) => {
320 out.insert(*txid, tx);
321 }
322 Err(Error::CorepcRPC(JsonRpcError::Rpc(rpc))) if rpc.code == RPC_NOT_FOUND => {}
323 Err(e) => {
324 debug!(txid = %txid, error = %e, "getrawtransaction batch: item failed")
325 }
326 }
327 }
328 }
329
330 Ok(out)
331 }
332
333 pub fn send_raw_transaction(&self, hex: &str) -> Result<Txid> {
334 let txid: bitcoin::Txid = self
335 .0
336 .call_once("sendrawtransaction", &[Value::String(hex.to_string())])
337 .map_err(|e| {
338 if let Error::CorepcRPC(JsonRpcError::Rpc(rpc)) = &e
343 && matches!(rpc.code, -22 | -25 | -26 | -27)
344 {
345 return Error::Parse(rpc.message.clone());
346 }
347 e
348 })?;
349 Ok(Txid::from(txid))
350 }
351
352 pub fn fetch_mempool_state(&self) -> Result<(MempoolState, Vec<BlockTemplateTx>)> {
362 let requests: [(&str, Vec<Value>); 3] = [
363 (
364 "getblocktemplate",
365 vec![serde_json::json!({ "rules": ["segwit"] })],
366 ),
367 ("getrawmempool", vec![Value::Bool(false)]),
368 ("getmempoolinfo", vec![]),
369 ];
370 let mut out = self.0.call_mixed_batch(&requests)?.into_iter();
371 let template_raw = out.next().ok_or(Error::Internal("missing gbt"))??;
372 let txids_raw = out.next().ok_or(Error::Internal("missing rawmempool"))??;
373 let info_raw = out.next().ok_or(Error::Internal("missing mempoolinfo"))??;
374
375 let txid_strs: Vec<String> = serde_json::from_str(txids_raw.get())?;
376 let live_txids: Vec<Txid> = txid_strs
377 .iter()
378 .map(|s| Self::parse_txid(s, "mempool txid"))
379 .collect::<Result<Vec<_>>>()?;
380 let template: GetBlockTemplate = serde_json::from_str(template_raw.get())?;
381 let tip_hash = Self::parse_block_hash(&template.previous_block_hash, "previousblockhash")?;
382 let tip_height = Height::from(u64::try_from(template.height - 1).map_err(|_| {
383 Error::Parse(format!("gbt height out of range: {}", template.height))
384 })?);
385 let block_template = build_gbt(template)?;
386 let min_fee = build_min_fee(serde_json::from_str(info_raw.get())?);
387
388 Ok((
389 MempoolState {
390 live_txids,
391 min_fee,
392 tip_hash,
393 tip_height,
394 },
395 block_template,
396 ))
397 }
398
399 pub fn fetch_new_pool_data(
406 &self,
407 txids: &[Txid],
408 ) -> Result<(Vec<MempoolEntryInfo>, FxHashMap<Txid, bitcoin::Transaction>)> {
409 let mut entries: Vec<MempoolEntryInfo> = Vec::with_capacity(txids.len());
410 let mut txs: FxHashMap<Txid, bitcoin::Transaction> =
411 FxHashMap::with_capacity_and_hasher(txids.len(), Default::default());
412
413 for chunk in txids.chunks(BATCH_CHUNK) {
414 let mut requests: Vec<(&str, Vec<Value>)> = Vec::with_capacity(chunk.len() * 2);
415 for txid in chunk {
416 let bt: &bitcoin::Txid = txid.into();
417 let tv = serde_json::to_value(bt).unwrap_or(Value::Null);
418 requests.push(("getmempoolentry", vec![tv.clone()]));
419 requests.push(("getrawtransaction", vec![tv, Value::Bool(false)]));
420 }
421
422 let results = self.0.call_mixed_batch(&requests)?;
423 let mut iter = results.into_iter();
424 for txid in chunk {
425 let entry_res = iter.next().ok_or(Error::Internal("missing entry"))?;
426 let raw_res = iter.next().ok_or(Error::Internal("missing raw"))?;
427
428 match entry_res.and_then(|raw| {
429 let me: MempoolEntry = serde_json::from_str(raw.get())?;
430 build_entry(*txid, me)
431 }) {
432 Ok(info) => entries.push(info),
433 Err(Error::CorepcRPC(JsonRpcError::Rpc(rpc))) if rpc.code == RPC_NOT_FOUND => {}
434 Err(e) => {
435 debug!(txid = %txid, error = %e, "getmempoolentry mixed batch: item failed")
436 }
437 }
438
439 match raw_res.and_then(|raw| {
440 let hex: String = serde_json::from_str(raw.get())?;
441 Ok(encode::deserialize_hex::<bitcoin::Transaction>(&hex)?)
442 }) {
443 Ok(tx) => {
444 txs.insert(*txid, tx);
445 }
446 Err(Error::CorepcRPC(JsonRpcError::Rpc(rpc))) if rpc.code == RPC_NOT_FOUND => {}
447 Err(e) => {
448 debug!(txid = %txid, error = %e, "getrawtransaction mixed batch: item failed")
449 }
450 }
451 }
452 }
453
454 Ok((entries, txs))
455 }
456
457 pub fn get_closest_valid_height(&self, hash: BlockHash) -> Result<(Height, BlockHash)> {
458 debug!("Get closest valid height...");
459
460 let mut current = hash;
461 loop {
462 let info = self.get_block_header_info(¤t)?;
463 if info.confirmations > 0 {
464 return Ok((Height::from(info.height as u64), current));
465 }
466 let prev = info.previous_block_hash.ok_or(Error::NotFound(
467 "Reached genesis without finding main chain".into(),
468 ))?;
469 current = Self::parse_block_hash(&prev, "previousblockhash")?;
470 }
471 }
472
473 pub fn get_blockchain_info(&self) -> Result<GetBlockchainInfo> {
474 self.0.call_with_retry("getblockchaininfo", &[])
475 }
476
477 pub fn get_network(&self) -> Result<bitcoin::Network> {
480 let chain = self.get_blockchain_info()?.chain;
481 bitcoin::Network::from_core_arg(&chain)
482 .map_err(|e| Error::Parse(format!("getblockchaininfo.chain '{chain}': {e}")))
483 }
484
485 pub fn wait_for_synced_node(&self) -> Result<()> {
486 let is_synced = || -> Result<bool> {
487 let info = self.get_blockchain_info()?;
488 Ok(info.headers == info.blocks)
489 };
490
491 if !is_synced()? {
492 info!("Waiting for node to sync...");
493 while !is_synced()? {
494 sleep(Duration::from_secs(1))
495 }
496 }
497
498 Ok(())
499 }
500
501 fn parse_txid(s: &str, label: &str) -> Result<Txid> {
502 s.parse::<bitcoin::Txid>()
503 .map(Txid::from)
504 .map_err(|e| Error::Parse(format!("{label}: {e}")))
505 }
506
507 fn parse_block_hash(s: &str, label: &str) -> Result<BlockHash> {
508 s.parse::<bitcoin::BlockHash>()
509 .map(BlockHash::from)
510 .map_err(|e| Error::Parse(format!("{label}: {e}")))
511 }
512}