Skip to main content

brk_rpc/
methods.rs

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
23/// Bitcoin Core's `-5` (`RPC_INVALID_ADDRESS_OR_KEY`) is the expected
24/// response when querying a confirmed transaction without `-txindex`.
25/// The mempool fetcher tolerates these per-item failures silently.
26const RPC_NOT_FOUND: i32 = -5;
27
28use crate::{BlockTemplateTx, Client};
29
30/// Per-batch request count for `get_block_hashes_range`,
31/// `fetch_new_pool_data`, and `get_raw_transactions`. Sized so the JSON
32/// request body stays well under a megabyte and bitcoind doesn't spend
33/// too long on a single batch before yielding results. For the mixed
34/// `getmempoolentry`+`getrawtransaction` batch this is the *txid* count;
35/// the wire batch is twice that.
36const BATCH_CHUNK: usize = 2000;
37
38/// Mempool snapshot data that survives one fetch cycle: the live
39/// txid set, fee floor, and chain tip. Returned alongside the raw
40/// `block_template` (which Fetcher consumes for GBT synthesis) by
41/// `Client::fetch_mempool_state`.
42pub struct MempoolState {
43    pub live_txids: Vec<Txid>,
44    pub min_fee: FeeRate,
45    /// Chain tip's hash (block-template's `previousblockhash`).
46    /// Compared between cycles to detect newly mined blocks.
47    pub tip_hash: BlockHash,
48    /// Chain tip's height (block-template's `height` minus one).
49    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    // Pass 1: decode bodies and stash the 1-based GBT-array indices aside
70    // so each `data` hex string and `BlockTemplateTransaction` drops as
71    // soon as the tx is pushed.
72    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    // Pass 2: resolve indices to txids now that the array is complete.
94    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
107/// Convert bitcoind's `mempoolminfee` (BTC/kvB f64) to sat/vB. Round-trip
108/// via integer sat/kvB (bitcoind's native CFeeRate unit) so the f64 drift
109/// in the JSON-decoded value can't push 1.0 sat/vB to 1.0...e-13 above 1.0
110/// and trip `ceil_to(0.001)` downstream.
111fn 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    /// Returns the numbers of block in the longest chain.
118    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    /// Returns the numbers of block in the longest chain.
124    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    /// Get every canonical block hash for the inclusive height range
183    /// `start..=end` in a single JSON-RPC batch request. Returns hashes
184    /// in canonical order (`start`, `start+1`, …, `end`). Use this
185    /// whenever resolving more than ~2 heights — one HTTP round-trip
186    /// beats N sequential `get_block_hash` calls once the per-call
187    /// overhead dominates.
188    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    /// Batched `getrawtransaction` over a slice of txids. Returns a map keyed
292    /// by txid containing the deserialized tx. Individual failures (e.g. a
293    /// tx that evicted between the listing and this call) are logged and
294    /// dropped so a single bad entry doesn't kill the batch.
295    ///
296    /// Chunked at `BATCH_CHUNK` requests per round-trip.
297    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                // Bitcoin Core returns RPC error codes for client-side problems
339                // (decode failed, verification failed, already in chain, etc.).
340                // Surface these as 400 (Parse) so HTTP callers see a 4xx, matching
341                // mempool.space's POST /api/tx behavior.
342                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    /// Core's projected next block + live mempool txid set +
353    /// `mempoolminfee`, fetched in a single bitcoind round-trip. GBT
354    /// carries each tx's full body and stats, so block 0 is exact even
355    /// when a tx vanishes from the mempool listing between the GBT and
356    /// `getrawmempool` calls; no follow-up entry fetch can race it.
357    /// Returns the passthrough `MempoolState` and the raw
358    /// `block_template` (consumed downstream by GBT synthesis), in one
359    /// batched round-trip: `getblocktemplate` + `getrawmempool false`
360    /// + `getmempoolinfo`.
361    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    /// Mixed batch of `getmempoolentry` + `getrawtransaction` for the
400    /// same txid set in one round-trip. Returns the entries vec and the
401    /// raw-tx map keyed by txid. Per-item -5 (NOT_FOUND — tx evicted
402    /// between the listing and this call) drops silently for either leg;
403    /// transport-level failures still propagate. Chunked at `BATCH_CHUNK`
404    /// txids per round-trip (2× that on the wire).
405    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(&current)?;
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    /// Bitcoin network the connected node is running on, derived from
478    /// `getblockchaininfo.chain`.
479    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}