diff --git a/Cargo.lock b/Cargo.lock index 7a25ebbca..22695a6a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,6 +558,7 @@ dependencies = [ "brk_types", "color-eyre", "fjall", + "parking_lot", "rayon", "rlimit", "rustc-hash", @@ -2535,8 +2536,6 @@ dependencies = [ [[package]] name = "rawdb" version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fd9f9db42fd2d1adfbd7cf447f021776b3b8fd15e09788988fc18c61e1f6bc" dependencies = [ "libc", "log", @@ -3429,8 +3428,6 @@ checksum = "8f54a172d0620933a27a4360d3db3e2ae0dd6cceae9730751a036bbf182c4b23" [[package]] name = "vecdb" version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5422c45d12de71456700c199f9553319cb99e76311e413316dca7e9efd5133b6" dependencies = [ "itoa", "libc", @@ -3452,8 +3449,6 @@ dependencies = [ [[package]] name = "vecdb_derive" version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b075be4cec2d718d40dc422cef038c10d6fcce4aad594199cc0a301a4985146" dependencies = [ "quote", "syn", diff --git a/Cargo.toml b/Cargo.toml index e017d4067..fbd183b56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,8 +87,8 @@ tower-http = { version = "0.6.8", features = ["catch-panic", "compression-br", " tower-layer = "0.3" tracing = { version = "0.1", default-features = false, features = ["std"] } ureq = { version = "3.3.0", features = ["json"] } -vecdb = { version = "0.9.2", features = ["derive", "serde_json", "pco", "schemars"] } -# vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] } +# vecdb = { version = "0.9.2", features = ["derive", "serde_json", "pco", "schemars"] } +vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] } [workspace.metadata.release] shared-version = true diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index 5177878b3..c7df9e21e 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -6139,6 +6139,8 @@ pub struct SeriesTree_Pools_Minor { pub parasite: BlocksDominancePattern, pub redrockpool: BlocksDominancePattern, pub est3lar: BlocksDominancePattern, + pub braiinssolo: BlocksDominancePattern, + pub solopool: BlocksDominancePattern, } impl SeriesTree_Pools_Minor { @@ -6284,6 +6286,8 @@ impl SeriesTree_Pools_Minor { parasite: BlocksDominancePattern::new(client.clone(), "parasite".to_string()), redrockpool: BlocksDominancePattern::new(client.clone(), "redrockpool".to_string()), est3lar: BlocksDominancePattern::new(client.clone(), "est3lar".to_string()), + braiinssolo: BlocksDominancePattern::new(client.clone(), "braiinssolo".to_string()), + solopool: BlocksDominancePattern::new(client.clone(), "solopool".to_string()), } } } diff --git a/crates/brk_computer/src/pools/mod.rs b/crates/brk_computer/src/pools/mod.rs index ad80ac3c9..9d1a642c2 100644 --- a/crates/brk_computer/src/pools/mod.rs +++ b/crates/brk_computer/src/pools/mod.rs @@ -44,7 +44,7 @@ impl Vecs { let db = open_db(parent_path, DB_NAME, 100_000)?; let pools = pools(); - let version = parent_version + Version::new(3) + Version::new(pools.len() as u32); + let version = parent_version + Version::new(4) + Version::new(pools.len() as u32); let mut major_map = BTreeMap::new(); let mut minor_map = BTreeMap::new(); @@ -123,8 +123,7 @@ impl Vecs { self.pool.len() ); } - self.pool - .validate_computed_version_or_reset(dep_version)?; + self.pool.validate_computed_version_or_reset(dep_version)?; let first_txout_index = indexer.vecs.transactions.first_txout_index.reader(); let output_type = indexer.vecs.outputs.output_type.reader(); diff --git a/crates/brk_indexer/Cargo.toml b/crates/brk_indexer/Cargo.toml index c87877cbf..80ae0d108 100644 --- a/crates/brk_indexer/Cargo.toml +++ b/crates/brk_indexer/Cargo.toml @@ -19,6 +19,7 @@ brk_store = { workspace = true } brk_types = { workspace = true } brk_traversable = { workspace = true } fjall = { workspace = true } +parking_lot = { workspace = true } schemars = { workspace = true } serde = { workspace = true } tracing = { workspace = true } diff --git a/crates/brk_indexer/src/lib.rs b/crates/brk_indexer/src/lib.rs index 7b377a8a5..9d73bd427 100644 --- a/crates/brk_indexer/src/lib.rs +++ b/crates/brk_indexer/src/lib.rs @@ -3,6 +3,7 @@ use std::{ fs, path::{Path, PathBuf}, + sync::Arc, thread::{self, sleep}, time::{Duration, Instant}, }; @@ -10,7 +11,8 @@ use std::{ use brk_error::Result; use brk_reader::{Reader, XORBytes}; use brk_rpc::Client; -use brk_types::Height; +use brk_types::{BlockHash, Height}; +use parking_lot::RwLock; use fjall::PersistMode; use tracing::{debug, info}; use vecdb::{ @@ -36,6 +38,13 @@ pub struct Indexer { path: PathBuf, pub vecs: Vecs, pub stores: Stores, + tip_blockhash: Arc>, +} + +impl Indexer { + pub fn tip_blockhash(&self) -> BlockHash { + self.tip_blockhash.read().clone() + } } impl ReadOnlyClone for Indexer { @@ -46,6 +55,7 @@ impl ReadOnlyClone for Indexer { path: self.path.clone(), vecs: self.vecs.read_only_clone(), stores: self.stores.clone(), + tip_blockhash: self.tip_blockhash.clone(), } } } @@ -77,10 +87,17 @@ impl Indexer { let stores = Stores::forced_import(&indexed_path, VERSION)?; info!("Imported stores in {:?}", i.elapsed()); + let tip_blockhash = vecs + .blocks + .blockhash + .collect_last() + .unwrap_or_default(); + Ok(Self { path: indexed_path.clone(), vecs, stores, + tip_blockhash: Arc::new(RwLock::new(tip_blockhash)), }) }; @@ -288,6 +305,8 @@ impl Indexer { export(stores, vecs, height)?; readers = Readers::new(vecs); } + + *self.tip_blockhash.write() = block.block_hash().into(); } drop(readers); @@ -302,6 +321,9 @@ impl Indexer { sleep(Duration::from_secs(5)); + info!("Exporting..."); + let i = Instant::now(); + if !tasks.is_empty() { let i = Instant::now(); for task in tasks { @@ -317,6 +339,8 @@ impl Indexer { } db.compact()?; + + info!("Exported in {:?}", i.elapsed()); Ok(()) }); diff --git a/crates/brk_query/src/impl/addr.rs b/crates/brk_query/src/impl/addr.rs index bba31ce22..6705f42dd 100644 --- a/crates/brk_query/src/impl/addr.rs +++ b/crates/brk_query/src/impl/addr.rs @@ -4,8 +4,8 @@ use bitcoin::{Network, PublicKey, ScriptBuf}; use brk_error::{Error, Result}; use brk_types::{ Addr, AddrBytes, AddrChainStats, AddrHash, AddrIndexOutPoint, AddrIndexTxIndex, AddrStats, - AnyAddrDataIndexEnum, OutputType, Sats, Transaction, TxIndex, TxStatus, Txid, TypeIndex, Unit, - Utxo, Vout, + AnyAddrDataIndexEnum, Height, OutputType, Transaction, TxIndex, TxStatus, Txid, TypeIndex, + Unit, Utxo, Vout, }; use vecdb::{ReadableVec, VecIndex}; @@ -186,25 +186,19 @@ impl Query { let first_txout_index_reader = vecs.transactions.first_txout_index.reader(); let value_reader = vecs.outputs.value.reader(); let blockhash_reader = vecs.blocks.blockhash.reader(); + let mut height_cursor = vecs.transactions.height.cursor(); + let mut block_ts_cursor = vecs.blocks.timestamp.cursor(); let utxos: Vec = outpoints .into_iter() .map(|(tx_index, vout)| { - let txid: Txid = txid_reader.get(tx_index.to_usize()); - let height = vecs - .transactions - .height - .collect_one_at(tx_index.to_usize()) - .unwrap(); + let txid = txid_reader.get(tx_index.to_usize()); + let height = height_cursor.get(tx_index.to_usize()).unwrap(); let first_txout_index = first_txout_index_reader.get(tx_index.to_usize()); let txout_index = first_txout_index + vout; - let value: Sats = value_reader.get(usize::from(txout_index)); + let value = value_reader.get(usize::from(txout_index)); let block_hash = blockhash_reader.get(usize::from(height)); - let block_time = vecs - .blocks - .timestamp - .collect_one_at(usize::from(height)) - .unwrap(); + let block_time = block_ts_cursor.get(height.to_usize()).unwrap(); Utxo { txid, @@ -247,6 +241,29 @@ impl Query { Ok(txids) } + /// Height of the last on-chain activity for an address (last tx_index → height). + pub fn addr_last_activity_height(&self, addr: &Addr) -> Result { + let (output_type, type_index) = self.resolve_addr(addr)?; + let store = self + .indexer() + .stores + .addr_type_to_addr_index_and_tx_index + .get(output_type) + .unwrap(); + let prefix = u32::from(type_index).to_be_bytes(); + let last_tx_index = store + .prefix(prefix) + .next_back() + .map(|(key, _): (AddrIndexTxIndex, Unit)| key.tx_index()) + .ok_or(Error::UnknownAddr)?; + self.indexer() + .vecs + .transactions + .height + .collect_one(last_tx_index) + .ok_or(Error::UnknownAddr) + } + /// Resolve an address string to its output type and type_index fn resolve_addr(&self, addr: &Addr) -> Result<(OutputType, TypeIndex)> { let stores = &self.indexer().stores; diff --git a/crates/brk_query/src/impl/block/info.rs b/crates/brk_query/src/impl/block/info.rs index ddc5428f5..b31db1873 100644 --- a/crates/brk_query/src/impl/block/info.rs +++ b/crates/brk_query/src/impl/block/info.rs @@ -1,9 +1,10 @@ use bitcoin::consensus::Decodable; use bitcoin::hex::DisplayHex; use brk_error::{Error, Result}; +use brk_reader::Reader; use brk_types::{ - BlockExtras, BlockHash, BlockHashPrefix, BlockHeader, BlockInfo, BlockInfoV1, BlockPool, - FeeRate, Height, Sats, Timestamp, TxIndex, VSize, pools, + BlkPosition, BlockExtras, BlockHash, BlockHashPrefix, BlockHeader, BlockInfo, BlockInfoV1, + BlockPool, FeeRate, Height, Sats, Timestamp, TxIndex, VSize, pools, }; use vecdb::{AnyVec, ReadableVec, VecIndex}; @@ -123,12 +124,16 @@ impl Query { blocks.push(BlockInfo { id: blockhashes[i].clone(), height: Height::from(begin + i), - header, + version: header.version, timestamp: timestamps[i], tx_count, size: *sizes[i], weight: weights[i], + merkle_root: header.merkle_root, + previous_block_hash: header.previous_block_hash, median_time, + nonce: header.nonce, + bits: header.bits, difficulty: *difficulties[i], }); } @@ -138,7 +143,7 @@ impl Query { pub(crate) fn blocks_v1_range(&self, begin: usize, end: usize) -> Result> { if begin >= end { - return Ok(Vec::new()); + return Ok(vec![]); } let count = end - begin; @@ -304,12 +309,16 @@ impl Query { let info = BlockInfo { id: blockhashes[i].clone(), height: Height::from(begin + i), - header, + version: header.version, timestamp: timestamps[i], tx_count, size, weight, + merkle_root: header.merkle_root, + previous_block_hash: header.previous_block_hash, median_time, + nonce: header.nonce, + bits: header.bits, difficulty: *difficulties[i], }; @@ -333,6 +342,7 @@ impl Query { id: pool.unique_id(), name: pool.name.to_string(), slug: pool_slug, + miner_names: None, }, avg_fee: Sats::from(if non_coinbase > 0 { total_fees_u64 / non_coinbase @@ -441,8 +451,8 @@ impl Query { } fn parse_coinbase_tx( - reader: &brk_reader::Reader, - position: brk_types::BlkPosition, + reader: &Reader, + position: BlkPosition, ) -> (String, Option, Vec, String, String) { let raw_bytes = match reader.read_raw_bytes(position, 1000) { Ok(bytes) => bytes, @@ -463,7 +473,14 @@ impl Query { let coinbase_signature_ascii = tx .input .first() - .map(|input| input.script_sig.as_bytes().iter().map(|&b| b as char).collect::()) + .map(|input| { + input + .script_sig + .as_bytes() + .iter() + .map(|&b| b as char) + .collect::() + }) .unwrap_or_default(); let coinbase_addresses: Vec = tx diff --git a/crates/brk_query/src/impl/block/timestamp.rs b/crates/brk_query/src/impl/block/timestamp.rs index 048de3ab2..4c1224196 100644 --- a/crates/brk_query/src/impl/block/timestamp.rs +++ b/crates/brk_query/src/impl/block/timestamp.rs @@ -67,7 +67,7 @@ impl Query { // Convert timestamp to ISO 8601 format let ts_secs: i64 = (*best_ts).into(); let iso_timestamp = JiffTimestamp::from_second(ts_secs) - .map(|t| t.to_string()) + .map(|t| t.strftime("%Y-%m-%dT%H:%M:%S%.3fZ").to_string()) .unwrap_or_else(|_| best_ts.to_string()); Ok(BlockTimestamp { diff --git a/crates/brk_query/src/impl/block/txs.rs b/crates/brk_query/src/impl/block/txs.rs index c05d7fd4c..267ba16c1 100644 --- a/crates/brk_query/src/impl/block/txs.rs +++ b/crates/brk_query/src/impl/block/txs.rs @@ -1,6 +1,12 @@ +use std::io::Cursor; + +use bitcoin::{consensus::Decodable, hex::DisplayHex}; use brk_error::{Error, Result}; -use brk_types::{BlockHash, Height, Transaction, TxIndex, Txid}; -use vecdb::{AnyVec, ReadableVec}; +use brk_types::{ + BlockHash, Height, OutputType, Sats, Timestamp, Transaction, TxIn, TxIndex, TxOut, TxStatus, + Txid, Vout, Weight, +}; +use vecdb::{AnyVec, ReadableVec, VecIndex}; use super::BLOCK_TXS_PAGE_SIZE; use crate::Query; @@ -13,7 +19,13 @@ impl Query { pub fn block_txs(&self, hash: &BlockHash, start_index: TxIndex) -> Result> { let height = self.height_by_hash(hash)?; - self.block_txs_by_height(height, start_index.into()) + let (first, tx_count) = self.block_tx_range(height)?; + let start: usize = start_index.into(); + if start >= tx_count { + return Ok(Vec::new()); + } + let count = BLOCK_TXS_PAGE_SIZE.min(tx_count - start); + self.transactions_by_range(first + start, count) } pub fn block_txid_at_index(&self, hash: &BlockHash, index: TxIndex) -> Result { @@ -21,111 +33,198 @@ impl Query { self.block_txid_at_index_by_height(height, index.into()) } - // === Helper methods === + // === Bulk transaction read === - pub(crate) fn block_txids_by_height(&self, height: Height) -> Result> { - let indexer = self.indexer(); - - let max_height = self.indexed_height(); - if height > max_height { - return Err(Error::OutOfRange("Block height out of range".into())); - } - - let first_tx_index = indexer - .vecs - .transactions - .first_tx_index - .collect_one(height) - .unwrap(); - let next_first_tx_index = indexer - .vecs - .transactions - .first_tx_index - .collect_one(height.incremented()) - .unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len())); - - let first: usize = first_tx_index.into(); - let next: usize = next_first_tx_index.into(); - - let txids: Vec = indexer.vecs.transactions.txid.collect_range_at(first, next); - - Ok(txids) - } - - fn block_txs_by_height(&self, height: Height, start_index: usize) -> Result> { - let indexer = self.indexer(); - - let max_height = self.indexed_height(); - if height > max_height { - return Err(Error::OutOfRange("Block height out of range".into())); - } - - let first_tx_index = indexer - .vecs - .transactions - .first_tx_index - .collect_one(height) - .unwrap(); - let next_first_tx_index = indexer - .vecs - .transactions - .first_tx_index - .collect_one(height.incremented()) - .unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len())); - - let first: usize = first_tx_index.into(); - let next: usize = next_first_tx_index.into(); - let tx_count = next - first; - - if start_index >= tx_count { + /// Batch-read `count` consecutive transactions starting at raw index `start`. + /// Block info is cached per unique height — free for same-block batches. + pub fn transactions_by_range(&self, start: usize, count: usize) -> Result> { + if count == 0 { return Ok(Vec::new()); } - let end_index = (start_index + BLOCK_TXS_PAGE_SIZE).min(tx_count); - let count = end_index - start_index; + let indexer = self.indexer(); + let reader = self.reader(); + let end = start + count; + + // 7 range reads instead of count * 7 point reads + let txids: Vec = indexer.vecs.transactions.txid.collect_range_at(start, end); + let heights: Vec = indexer.vecs.transactions.height.collect_range_at(start, end); + let versions = indexer.vecs.transactions.tx_version.collect_range_at(start, end); + let lock_times = indexer.vecs.transactions.raw_locktime.collect_range_at(start, end); + let total_sizes = indexer.vecs.transactions.total_size.collect_range_at(start, end); + let first_txin_indices = indexer + .vecs + .transactions + .first_txin_index + .collect_range_at(start, end); + let positions = indexer.vecs.transactions.position.collect_range_at(start, end); + + // Readers for prevout lookups (created once) + let txid_reader = indexer.vecs.transactions.txid.reader(); + let first_txout_index_reader = indexer.vecs.transactions.first_txout_index.reader(); + let value_reader = indexer.vecs.outputs.value.reader(); + let output_type_reader = indexer.vecs.outputs.output_type.reader(); + let type_index_reader = indexer.vecs.outputs.type_index.reader(); + let addr_readers = indexer.vecs.addrs.addr_readers(); + + // Block info cache — for same-block batches, read once + let mut cached_block: Option<(Height, BlockHash, Timestamp)> = None; let mut txs = Vec::with_capacity(count); - for i in start_index..end_index { - let tx_index = TxIndex::from(first + i); - let tx = self.transaction_by_index(tx_index)?; - txs.push(tx); + + for i in 0..count { + let height = heights[i]; + + // Reuse block info if same height as previous tx + let (block_hash, block_time) = + if let Some((h, ref bh, bt)) = cached_block && h == height { + (bh.clone(), bt) + } else { + let bh = indexer.vecs.blocks.blockhash.read_once(height)?; + let bt = indexer.vecs.blocks.timestamp.collect_one(height).unwrap(); + cached_block = Some((height, bh.clone(), bt)); + (bh, bt) + }; + + // Decode raw transaction from blk file + let buffer = reader.read_raw_bytes(positions[i], *total_sizes[i] as usize)?; + let tx = bitcoin::Transaction::consensus_decode(&mut Cursor::new(buffer)) + .map_err(|_| Error::Parse("Failed to decode transaction".into()))?; + + // Batch-read outpoints for this tx's inputs + let outpoints = indexer.vecs.inputs.outpoint.collect_range_at( + usize::from(first_txin_indices[i]), + usize::from(first_txin_indices[i]) + tx.input.len(), + ); + + let input: Vec = tx + .input + .iter() + .enumerate() + .map(|(j, txin)| { + let outpoint = outpoints[j]; + let is_coinbase = outpoint.is_coinbase(); + + let (prev_txid, prev_vout, prevout) = if is_coinbase { + (Txid::COINBASE, Vout::MAX, None) + } else { + let prev_tx_index = outpoint.tx_index(); + let prev_vout = outpoint.vout(); + let prev_txid = txid_reader.get(prev_tx_index.to_usize()); + let prev_first_txout_index = + first_txout_index_reader.get(prev_tx_index.to_usize()); + let prev_txout_index = prev_first_txout_index + prev_vout; + let prev_value = value_reader.get(usize::from(prev_txout_index)); + let prev_output_type: OutputType = + output_type_reader.get(usize::from(prev_txout_index)); + let prev_type_index = + type_index_reader.get(usize::from(prev_txout_index)); + let script_pubkey = + addr_readers.script_pubkey(prev_output_type, prev_type_index); + ( + prev_txid, + prev_vout, + Some(TxOut::from((script_pubkey, prev_value))), + ) + }; + + let witness = txin + .witness + .iter() + .map(|w| w.to_lower_hex_string()) + .collect(); + + TxIn { + txid: prev_txid, + vout: prev_vout, + prevout, + script_sig: txin.script_sig.clone(), + script_sig_asm: (), + witness, + is_coinbase, + sequence: txin.sequence.0, + inner_redeem_script_asm: (), + inner_witness_script_asm: (), + } + }) + .collect(); + + let weight = Weight::from(tx.weight()); + let total_sigop_cost = tx.total_sigop_cost(|_| None); + let output: Vec = tx.output.into_iter().map(TxOut::from).collect(); + + let mut transaction = Transaction { + index: Some(TxIndex::from(start + i)), + txid: txids[i].clone(), + version: versions[i], + lock_time: lock_times[i], + total_size: *total_sizes[i] as usize, + weight, + total_sigop_cost, + fee: Sats::ZERO, + input, + output, + status: TxStatus { + confirmed: true, + block_height: Some(height), + block_hash: Some(block_hash), + block_time: Some(block_time), + }, + }; + + transaction.compute_fee(); + txs.push(transaction); } Ok(txs) } - fn block_txid_at_index_by_height(&self, height: Height, index: usize) -> Result { - let indexer = self.indexer(); + // === Helper methods === - let max_height = self.indexed_height(); - if height > max_height { + pub(crate) fn block_txids_by_height(&self, height: Height) -> Result> { + let (first, tx_count) = self.block_tx_range(height)?; + Ok(self + .indexer() + .vecs + .transactions + .txid + .collect_range_at(first, first + tx_count)) + } + + fn block_txid_at_index_by_height(&self, height: Height, index: usize) -> Result { + let (first, tx_count) = self.block_tx_range(height)?; + if index >= tx_count { + return Err(Error::OutOfRange("Transaction index out of range".into())); + } + Ok(self + .indexer() + .vecs + .transactions + .txid + .reader() + .get(first + index)) + } + + /// Returns (first_tx_raw_index, tx_count) for a block at `height`. + fn block_tx_range(&self, height: Height) -> Result<(usize, usize)> { + let indexer = self.indexer(); + if height > self.indexed_height() { return Err(Error::OutOfRange("Block height out of range".into())); } - - let first_tx_index = indexer + let first: usize = indexer .vecs .transactions .first_tx_index .collect_one(height) - .unwrap(); - let next_first_tx_index = indexer + .unwrap() + .into(); + let next: usize = indexer .vecs .transactions .first_tx_index .collect_one(height.incremented()) - .unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len())); - - let first: usize = first_tx_index.into(); - let next: usize = next_first_tx_index.into(); - let tx_count = next - first; - - if index >= tx_count { - return Err(Error::OutOfRange("Transaction index out of range".into())); - } - - let tx_index = first + index; - let txid = indexer.vecs.transactions.txid.reader().get(tx_index); - - Ok(txid) + .unwrap_or_else(|| TxIndex::from(indexer.vecs.transactions.txid.len())) + .into(); + Ok((first, next - first)) } } diff --git a/crates/brk_query/src/impl/mempool.rs b/crates/brk_query/src/impl/mempool.rs index 6470bbb9c..3d0135376 100644 --- a/crates/brk_query/src/impl/mempool.rs +++ b/crates/brk_query/src/impl/mempool.rs @@ -1,7 +1,9 @@ +use std::cmp::Ordering; + use brk_error::{Error, Result}; use brk_types::{ - CpfpEntry, CpfpInfo, MempoolBlock, MempoolInfo, MempoolRecentTx, RecommendedFees, Txid, - TxidParam, TxidPrefix, Weight, + CpfpEntry, CpfpInfo, FeeRate, MempoolBlock, MempoolInfo, MempoolRecentTx, RecommendedFees, + Txid, TxidParam, TxidPrefix, Weight, }; use crate::Query; @@ -86,10 +88,22 @@ impl Query { let effective_fee_per_vsize = entry.effective_fee_rate(); + let best_descendant = descendants + .iter() + .max_by(|a, b| { + FeeRate::from((a.fee, a.weight)) + .partial_cmp(&FeeRate::from((b.fee, b.weight))) + .unwrap_or(Ordering::Equal) + }) + .cloned(); + Ok(CpfpInfo { ancestors, + best_descendant, descendants, effective_fee_per_vsize, + fee: entry.fee, + adjusted_vsize: entry.vsize, }) } diff --git a/crates/brk_query/src/impl/mining/block_fees.rs b/crates/brk_query/src/impl/mining/block_fees.rs index 756af985a..6b8eb8e58 100644 --- a/crates/brk_query/src/impl/mining/block_fees.rs +++ b/crates/brk_query/src/impl/mining/block_fees.rs @@ -1,49 +1,22 @@ use brk_error::Result; -use brk_types::{BlockFeesEntry, Height, Sats, TimePeriod}; -use vecdb::{ReadableVec, VecIndex}; +use brk_types::{BlockFeesEntry, TimePeriod}; -use super::day1_iter::Day1Iter; +use super::block_window::BlockWindow; use crate::Query; impl Query { pub fn block_fees(&self, time_period: TimePeriod) -> Result> { - let computer = self.computer(); - let current_height = self.height(); - let start = current_height - .to_usize() - .saturating_sub(time_period.block_count()); - - let iter = Day1Iter::new(computer, start, current_height.to_usize()); - - let cumulative = &computer.mining.rewards.fees.cumulative.sats.height; - let first_height = &computer.indexes.day1.first_height; - - Ok(iter.collect(|di, ts, h| { - let h_start = first_height.collect_one(di)?; - let h_end = first_height - .collect_one(di + 1_usize) - .unwrap_or(Height::from(current_height.to_usize() + 1)); - let block_count = h_end.to_usize() - h_start.to_usize(); - if block_count == 0 { - return None; - } - - let cumulative_end = cumulative.collect_one_at(h_end.to_usize() - 1)?; - let cumulative_start = if h_start.to_usize() > 0 { - cumulative - .collect_one_at(h_start.to_usize() - 1) - .unwrap_or(Sats::ZERO) - } else { - Sats::ZERO - }; - let daily_sum = cumulative_end - cumulative_start; - let avg_fees = Sats::from(*daily_sum / block_count as u64); - - Some(BlockFeesEntry { - avg_height: h, - timestamp: ts, - avg_fees, + let bw = BlockWindow::new(self, time_period); + let cumulative = &self.computer().mining.rewards.fees.cumulative.sats.height; + Ok(bw + .cumulative_averages(self, cumulative) + .into_iter() + .map(|w| BlockFeesEntry { + avg_height: w.avg_height, + timestamp: w.timestamp, + avg_fees: w.avg_value, + usd: w.usd, }) - })) + .collect()) } } diff --git a/crates/brk_query/src/impl/mining/block_rewards.rs b/crates/brk_query/src/impl/mining/block_rewards.rs index c48ba08e5..b64d56a73 100644 --- a/crates/brk_query/src/impl/mining/block_rewards.rs +++ b/crates/brk_query/src/impl/mining/block_rewards.rs @@ -1,86 +1,22 @@ use brk_error::Result; -use brk_types::{BlockRewardsEntry, Height, Sats, TimePeriod}; -use vecdb::{ReadableVec, VecIndex}; +use brk_types::{BlockRewardsEntry, TimePeriod}; +use super::block_window::BlockWindow; use crate::Query; impl Query { pub fn block_rewards(&self, time_period: TimePeriod) -> Result> { - let computer = self.computer(); - let indexer = self.indexer(); - let current_height = self.height().to_usize(); - let start = current_height.saturating_sub(time_period.block_count()); - - let coinbase_vec = &computer.mining.rewards.coinbase.block.sats; - let timestamp_vec = &indexer.vecs.blocks.timestamp; - - match time_period { - // Per-block, exact rewards - TimePeriod::Day | TimePeriod::ThreeDays => { - let rewards: Vec = coinbase_vec.collect_range_at(start, current_height + 1); - let timestamps: Vec = - timestamp_vec.collect_range_at(start, current_height + 1); - - Ok(rewards - .iter() - .zip(timestamps.iter()) - .enumerate() - .map(|(i, (reward, ts))| BlockRewardsEntry { - avg_height: (start + i) as u32, - timestamp: **ts, - avg_rewards: **reward, - }) - .collect()) - } - // Daily averages, sampled to ~200 points - _ => { - let first_height_vec = &computer.indexes.day1.first_height; - let day1_vec = &computer.indexes.height.day1; - - let start_di = day1_vec - .collect_one(Height::from(start)) - .unwrap_or_default(); - let end_di = day1_vec - .collect_one(Height::from(current_height)) - .unwrap_or_default(); - - let total_days = end_di.to_usize().saturating_sub(start_di.to_usize()) + 1; - let step = (total_days / 200).max(1); - - let mut entries = Vec::with_capacity(total_days / step + 1); - let mut di = start_di.to_usize(); - - while di <= end_di.to_usize() { - let day = brk_types::Day1::from(di); - let next_day = brk_types::Day1::from(di + 1); - - if let Some(first_h) = first_height_vec.collect_one(day) { - let next_h = first_height_vec - .collect_one(next_day) - .unwrap_or(Height::from(current_height + 1)); - - let block_count = next_h.to_usize() - first_h.to_usize(); - if block_count > 0 { - let sum = - coinbase_vec - .fold_range(first_h, next_h, Sats::ZERO, |acc, v| acc + v); - let avg = *sum / block_count as u64; - - if let Some(ts) = timestamp_vec.collect_one(first_h) { - entries.push(BlockRewardsEntry { - avg_height: first_h.to_usize() as u32, - timestamp: *ts, - avg_rewards: avg, - }); - } - } - } - - di += step; - } - - Ok(entries) - } - } + let bw = BlockWindow::new(self, time_period); + let cumulative = &self.computer().mining.rewards.coinbase.cumulative.sats.height; + Ok(bw + .cumulative_averages(self, cumulative) + .into_iter() + .map(|w| BlockRewardsEntry { + avg_height: w.avg_height, + timestamp: w.timestamp, + avg_rewards: w.avg_value, + usd: w.usd, + }) + .collect()) } } diff --git a/crates/brk_query/src/impl/mining/block_sizes.rs b/crates/brk_query/src/impl/mining/block_sizes.rs index 50ef95bc8..8c47b83f2 100644 --- a/crates/brk_query/src/impl/mining/block_sizes.rs +++ b/crates/brk_query/src/impl/mining/block_sizes.rs @@ -1,22 +1,18 @@ use brk_error::Result; -use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod}; -use vecdb::{ReadableOptionVec, VecIndex}; +use brk_types::{BlockSizeEntry, BlockSizesWeights, BlockWeightEntry, TimePeriod, Weight}; +use vecdb::ReadableVec; -use super::day1_iter::Day1Iter; +use super::block_window::BlockWindow; use crate::Query; impl Query { pub fn block_sizes_weights(&self, time_period: TimePeriod) -> Result { let computer = self.computer(); - let current_height = self.height(); - let start = current_height - .to_usize() - .saturating_sub(time_period.block_count()); + let bw = BlockWindow::new(self, time_period); + let timestamps = bw.timestamps(self); - let iter = Day1Iter::new(computer, start, current_height.to_usize()); - - // Rolling 24h median, sampled at day1 boundaries - let sizes_vec = &computer + // Batch read per-block rolling 24h medians for the range + let all_sizes = computer .blocks .size .size @@ -24,8 +20,9 @@ impl Query { .distribution .median ._24h - .day1; - let weights_vec = &computer + .height + .collect_range_at(bw.start, bw.end); + let all_weights = computer .blocks .weight .weight @@ -33,35 +30,30 @@ impl Query { .distribution .median ._24h - .day1; + .height + .collect_range_at(bw.start, bw.end); - let entries: Vec<_> = iter.collect(|di, ts, h| { - let size: Option = sizes_vec.collect_one_flat(di).map(|s| *s); - let weight: Option = weights_vec.collect_one_flat(di).map(|w| *w); - Some((u32::from(h), (*ts), size, weight)) - }); + // Sample at window midpoints + let mut sizes = Vec::with_capacity(timestamps.len()); + let mut weights = Vec::with_capacity(timestamps.len()); - let sizes = entries - .iter() - .filter_map(|(h, ts, size, _)| { - size.map(|s| BlockSizeEntry { - avg_height: *h, + for ((avg_height, start, _end), ts) in bw.iter().zip(×tamps) { + let mid = start - bw.start + (bw.window / 2).min(all_sizes.len().saturating_sub(1)); + if let Some(&size) = all_sizes.get(mid) { + sizes.push(BlockSizeEntry { + avg_height, timestamp: *ts, - avg_size: s, - }) - }) - .collect(); - - let weights = entries - .iter() - .filter_map(|(h, ts, _, weight)| { - weight.map(|w| BlockWeightEntry { - avg_height: *h, + avg_size: *size, + }); + } + if let Some(&weight) = all_weights.get(mid) { + weights.push(BlockWeightEntry { + avg_height, timestamp: *ts, - avg_weight: w, - }) - }) - .collect(); + avg_weight: Weight::from(*weight), + }); + } + } Ok(BlockSizesWeights { sizes, weights }) } diff --git a/crates/brk_query/src/impl/mining/block_window.rs b/crates/brk_query/src/impl/mining/block_window.rs new file mode 100644 index 000000000..d3c1dffb2 --- /dev/null +++ b/crates/brk_query/src/impl/mining/block_window.rs @@ -0,0 +1,154 @@ +use brk_types::{Cents, Dollars, Height, Sats, Timestamp, TimePeriod}; +use vecdb::{ReadableVec, VecIndex}; + +use crate::Query; + +/// Number of blocks per aggregation window, matching mempool.space's granularity. +fn block_window(period: TimePeriod) -> usize { + match period { + TimePeriod::Day | TimePeriod::ThreeDays | TimePeriod::Week => 1, + TimePeriod::Month => 3, + TimePeriod::ThreeMonths => 12, + TimePeriod::SixMonths => 18, + TimePeriod::Year | TimePeriod::TwoYears => 48, + TimePeriod::ThreeYears => 72, + } +} + +/// Per-window average with metadata. +pub struct WindowAvg { + pub avg_height: Height, + pub timestamp: Timestamp, + pub avg_value: Sats, + pub usd: Dollars, +} + +/// Block range and window size for a time period. +pub struct BlockWindow { + pub start: usize, + pub end: usize, + pub window: usize, +} + +impl BlockWindow { + pub fn new(query: &Query, time_period: TimePeriod) -> Self { + let current_height = query.height(); + let computer = query.computer(); + let lookback = &computer.blocks.lookback; + + // Use pre-computed timestamp-based lookback for accurate time boundaries. + // 24h, 1w, 1m, 1y use in-memory CachedVec; others fall back to PcoVec. + let cached = &lookback.cached_window_starts.0; + let start_height = match time_period { + TimePeriod::Day => cached._24h.collect_one(current_height), + TimePeriod::ThreeDays => lookback._3d.collect_one(current_height), + TimePeriod::Week => cached._1w.collect_one(current_height), + TimePeriod::Month => cached._1m.collect_one(current_height), + TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height), + TimePeriod::SixMonths => lookback._6m.collect_one(current_height), + TimePeriod::Year => cached._1y.collect_one(current_height), + TimePeriod::TwoYears => lookback._2y.collect_one(current_height), + TimePeriod::ThreeYears => lookback._3y.collect_one(current_height), + } + .unwrap_or_default(); + + Self { + start: start_height.to_usize(), + end: current_height.to_usize() + 1, + window: block_window(time_period), + } + } + + /// Compute per-window averages from a cumulative sats vec. + /// Batch-reads timestamps, prices, and the cumulative in one pass. + pub fn cumulative_averages( + &self, + query: &Query, + cumulative: &impl ReadableVec, + ) -> Vec { + let indexer = query.indexer(); + let computer = query.computer(); + + // Batch read all needed data for the range + let all_ts = indexer + .vecs + .blocks + .timestamp + .collect_range_at(self.start, self.end); + let all_prices: Vec = computer + .prices + .spot + .cents + .height + .collect_range_at(self.start, self.end); + let read_start = self.start.saturating_sub(1).max(0); + let all_cum = cumulative.collect_range_at(read_start, self.end); + let offset = if self.start > 0 { 1 } else { 0 }; + + let mut results = Vec::with_capacity(self.count()); + let mut pos = 0; + let total = all_ts.len(); + + while pos < total { + let window_end = (pos + self.window).min(total); + let block_count = (window_end - pos) as u64; + if block_count > 0 { + let mid = (pos + window_end) / 2; + let cum_end = all_cum[window_end - 1 + offset]; + let cum_start = if pos + offset > 0 { + all_cum[pos + offset - 1] + } else { + Sats::ZERO + }; + let total_sats = cum_end - cum_start; + results.push(WindowAvg { + avg_height: Height::from(self.start + mid), + timestamp: all_ts[mid], + avg_value: Sats::from(*total_sats / block_count), + usd: Dollars::from(all_prices[mid]), + }); + } + pos = window_end; + } + + results + } + + /// Batch-read timestamps for the midpoint of each window. + pub fn timestamps(&self, query: &Query) -> Vec { + let all_ts = query + .indexer() + .vecs + .blocks + .timestamp + .collect_range_at(self.start, self.end); + let mut timestamps = Vec::with_capacity(self.count()); + let mut pos = 0; + while pos < all_ts.len() { + let window_end = (pos + self.window).min(all_ts.len()); + timestamps.push(all_ts[(pos + window_end) / 2]); + pos = window_end; + } + timestamps + } + + /// Number of windows in this range. + fn count(&self) -> usize { + (self.end - self.start + self.window - 1) / self.window + } + + /// Iterate windows, yielding (avg_height, window_start, window_end) for each. + pub fn iter(&self) -> impl Iterator + '_ { + let mut pos = self.start; + std::iter::from_fn(move || { + if pos >= self.end { + return None; + } + let window_end = (pos + self.window).min(self.end); + let avg_height = Height::from((pos + window_end) / 2); + let start = pos; + pos = window_end; + Some((avg_height, start, window_end)) + }) + } +} diff --git a/crates/brk_query/src/impl/mining/day1_iter.rs b/crates/brk_query/src/impl/mining/day1_iter.rs deleted file mode 100644 index c760a51d4..000000000 --- a/crates/brk_query/src/impl/mining/day1_iter.rs +++ /dev/null @@ -1,67 +0,0 @@ -use brk_computer::Computer; -use brk_types::{Day1, Height, Timestamp}; -use vecdb::{ReadableVec, Ro, VecIndex}; - -/// Helper for iterating over day1 ranges with sampling. -pub struct Day1Iter<'a> { - computer: &'a Computer, - start_di: Day1, - end_di: Day1, - step: usize, -} - -impl<'a> Day1Iter<'a> { - pub fn new(computer: &'a Computer, start_height: usize, end_height: usize) -> Self { - let start_di = computer - .indexes - .height - .day1 - .collect_one(Height::from(start_height)) - .unwrap_or_default(); - let end_di = computer - .indexes - .height - .day1 - .collect_one(Height::from(end_height)) - .unwrap_or_default(); - - let total = end_di.to_usize().saturating_sub(start_di.to_usize()) + 1; - let step = (total / 200).max(1); - - Self { - computer, - start_di, - end_di, - step, - } - } - - /// Iterate and collect entries using the provided transform function. - pub fn collect(&self, mut transform: F) -> Vec - where - F: FnMut(Day1, Timestamp, Height) -> Option, - { - let total = self - .end_di - .to_usize() - .saturating_sub(self.start_di.to_usize()) - + 1; - let timestamps = &self.computer.indexes.timestamp.day1; - let heights = &self.computer.indexes.day1.first_height; - - let mut entries = Vec::with_capacity(total / self.step + 1); - let mut i = self.start_di.to_usize(); - - while i <= self.end_di.to_usize() { - let di = Day1::from(i); - if let (Some(ts), Some(h)) = (timestamps.collect_one(di), heights.collect_one(di)) - && let Some(entry) = transform(di, ts, h) - { - entries.push(entry); - } - i += self.step; - } - - entries - } -} diff --git a/crates/brk_query/src/impl/mining/difficulty.rs b/crates/brk_query/src/impl/mining/difficulty.rs index 3f353e82c..4ec559ffa 100644 --- a/crates/brk_query/src/impl/mining/difficulty.rs +++ b/crates/brk_query/src/impl/mining/difficulty.rs @@ -85,7 +85,7 @@ impl Query { let time_offset = expected_time as i64 - elapsed_time as i64; // Calculate previous retarget using stored difficulty values - let previous_retarget = if current_epoch_usize > 0 { + let (previous_retarget, previous_time) = if current_epoch_usize > 0 { let prev_epoch = Epoch::from(current_epoch_usize - 1); let prev_epoch_start = computer .indexes @@ -107,26 +107,33 @@ impl Query { .collect_one(epoch_start_height) .unwrap(); - if *prev_difficulty > 0.0 { + let retarget = if *prev_difficulty > 0.0 { ((*curr_difficulty / *prev_difficulty) - 1.0) * 100.0 } else { 0.0 - } + }; + + (retarget, epoch_start_timestamp) } else { - 0.0 + (0.0, epoch_start_timestamp) }; + // Expected blocks based on wall clock time since epoch start + let expected_blocks = elapsed_time as f64 / TARGET_BLOCK_TIME as f64; + Ok(DifficultyAdjustment { progress_percent, difficulty_change, - estimated_retarget_date, + estimated_retarget_date: estimated_retarget_date * 1000, remaining_blocks, - remaining_time, + remaining_time: remaining_time * 1000, previous_retarget, + previous_time, next_retarget_height: Height::from(next_retarget_height), - time_avg, - adjusted_time_avg: time_avg, + time_avg: time_avg * 1000, + adjusted_time_avg: time_avg * 1000, time_offset, + expected_blocks, }) } } diff --git a/crates/brk_query/src/impl/mining/epochs.rs b/crates/brk_query/src/impl/mining/epochs.rs index 4d58078ca..e53ee84a4 100644 --- a/crates/brk_query/src/impl/mining/epochs.rs +++ b/crates/brk_query/src/impl/mining/epochs.rs @@ -42,7 +42,7 @@ pub fn iter_difficulty_epochs( let epoch_difficulty = *epoch_to_difficulty.collect_one(epoch).unwrap_or_default(); let change_percent = match prev_difficulty { - Some(prev) if prev > 0.0 => ((epoch_difficulty / prev) - 1.0) * 100.0, + Some(prev) if prev > 0.0 => epoch_difficulty / prev, _ => 0.0, }; diff --git a/crates/brk_query/src/impl/mining/hashrate.rs b/crates/brk_query/src/impl/mining/hashrate.rs index b8ff57d81..79a545616 100644 --- a/crates/brk_query/src/impl/mining/hashrate.rs +++ b/crates/brk_query/src/impl/mining/hashrate.rs @@ -79,9 +79,10 @@ impl Query { let difficulty: Vec = iter_difficulty_epochs(computer, start, end) .into_iter() .map(|e| DifficultyEntry { - timestamp: e.timestamp, - difficulty: e.difficulty, + time: e.timestamp, height: e.height, + difficulty: e.difficulty, + adjustment: e.change_percent, }) .collect(); diff --git a/crates/brk_query/src/impl/mining/mod.rs b/crates/brk_query/src/impl/mining/mod.rs index 57b07986b..34c1863bf 100644 --- a/crates/brk_query/src/impl/mining/mod.rs +++ b/crates/brk_query/src/impl/mining/mod.rs @@ -2,7 +2,7 @@ mod block_fee_rates; mod block_fees; mod block_rewards; mod block_sizes; -mod day1_iter; +mod block_window; mod difficulty; mod difficulty_adjustments; mod epochs; diff --git a/crates/brk_query/src/impl/mining/pools.rs b/crates/brk_query/src/impl/mining/pools.rs index a14644112..2eb470ef2 100644 --- a/crates/brk_query/src/impl/mining/pools.rs +++ b/crates/brk_query/src/impl/mining/pools.rs @@ -1,17 +1,30 @@ use brk_error::{Error, Result}; use brk_types::{ - BlockInfoV1, Height, PoolBlockCounts, PoolBlockShares, PoolDetail, PoolDetailInfo, - PoolHashrateEntry, PoolInfo, PoolSlug, PoolStats, PoolsSummary, TimePeriod, pools, + BlockInfoV1, Day1, Height, Pool, PoolBlockCounts, PoolBlockShares, PoolDetail, + PoolDetailInfo, PoolHashrateEntry, PoolInfo, PoolSlug, PoolStats, PoolsSummary, StoredF64, + StoredU64, TimePeriod, pools, }; use vecdb::{AnyVec, ReadableVec, VecIndex}; use crate::Query; +/// 7-day lookback for share computation (matching mempool.space) +const LOOKBACK_DAYS: usize = 7; +/// Weekly sample interval (matching mempool.space's 604800s interval) +const SAMPLE_WEEKLY: usize = 7; + +/// Pre-read shared data for hashrate computation. +struct HashrateSharedData { + start_day: usize, + end_day: usize, + daily_hashrate: Vec>, + first_heights: Vec, +} + impl Query { pub fn mining_pools(&self, time_period: TimePeriod) -> Result { let computer = self.computer(); let current_height = self.height(); - let end = current_height.to_usize(); // No blocks indexed yet if computer.pools.pool.len() == 0 { @@ -19,14 +32,29 @@ impl Query { pools: vec![], block_count: 0, last_estimated_hashrate: 0, + last_estimated_hashrate3d: 0, + last_estimated_hashrate1w: 0, }); } - // Calculate start height based on time period - let start = end.saturating_sub(time_period.block_count()); + // Use timestamp-based lookback for accurate time boundaries + let lookback = &computer.blocks.lookback; + let start = match time_period { + TimePeriod::Day => lookback.cached_window_starts.0._24h.collect_one(current_height), + TimePeriod::ThreeDays => lookback._3d.collect_one(current_height), + TimePeriod::Week => lookback.cached_window_starts.0._1w.collect_one(current_height), + TimePeriod::Month => lookback.cached_window_starts.0._1m.collect_one(current_height), + TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height), + TimePeriod::SixMonths => lookback._6m.collect_one(current_height), + TimePeriod::Year => lookback.cached_window_starts.0._1y.collect_one(current_height), + TimePeriod::TwoYears => lookback._2y.collect_one(current_height), + TimePeriod::ThreeYears => lookback._3y.collect_one(current_height), + } + .unwrap_or_default() + .to_usize(); let pools = pools(); - let mut pool_data: Vec<(&'static brk_types::Pool, u64)> = Vec::new(); + let mut pool_data: Vec<(&'static Pool, u64)> = Vec::new(); // For each pool, get cumulative count at end and start, subtract to get range count for (pool_id, cumulative) in computer @@ -78,13 +106,33 @@ impl Query { }) .collect(); - // TODO: Calculate actual hashrate from difficulty - let last_estimated_hashrate = 0u128; + let hashrate_at = |height: Height| -> u128 { + let day = computer.indexes.height.day1.collect_one(height).unwrap_or_default(); + computer + .mining + .hashrate + .rate + .base + .day1 + .collect_one(day) + .flatten() + .map(|v| *v as u128) + .unwrap_or(0) + }; + + let lookback = &computer.blocks.lookback; + let last_estimated_hashrate = hashrate_at(current_height); + let last_estimated_hashrate3d = + hashrate_at(lookback._3d.collect_one(current_height).unwrap_or_default()); + let last_estimated_hashrate1w = + hashrate_at(lookback._1w.collect_one(current_height).unwrap_or_default()); Ok(PoolsSummary { pools: pool_stats, block_count: total_blocks, last_estimated_hashrate, + last_estimated_hashrate3d, + last_estimated_hashrate1w, }) } @@ -118,8 +166,15 @@ impl Query { // Get total blocks (all time) let total_all: u64 = *cumulative.collect_one(current_height).unwrap_or_default(); - // Get blocks for 24h (144 blocks) - let start_24h = end.saturating_sub(144); + // Use timestamp-based lookback for accurate time boundaries + let lookback = &computer.blocks.lookback; + let start_24h = lookback + .cached_window_starts + .0 + ._24h + .collect_one(current_height) + .unwrap_or_default() + .to_usize(); let count_before_24h: u64 = if start_24h == 0 { 0 } else { @@ -129,8 +184,13 @@ impl Query { }; let total_24h = total_all.saturating_sub(count_before_24h); - // Get blocks for 1w (1008 blocks) - let start_1w = end.saturating_sub(1008); + let start_1w = lookback + .cached_window_starts + .0 + ._1w + .collect_one(current_height) + .unwrap_or_default() + .to_usize(); let count_before_1w: u64 = if start_1w == 0 { 0 } else { @@ -191,11 +251,12 @@ impl Query { let reader = computer.pools.pool.reader(); let end = start.min(reader.len().saturating_sub(1)); - let mut heights = Vec::with_capacity(10); + const POOL_BLOCKS_LIMIT: usize = 100; + let mut heights = Vec::with_capacity(POOL_BLOCKS_LIMIT); for h in (0..=end).rev() { if reader.get(h) == slug { heights.push(h); - if heights.len() >= 10 { + if heights.len() >= POOL_BLOCKS_LIMIT { break; } } @@ -211,98 +272,166 @@ impl Query { } pub fn pool_hashrate(&self, slug: PoolSlug) -> Result> { - let pools_list = pools(); - let pool = pools_list.get(slug); - let entries = self.compute_pool_hashrate_entries(slug, 0)?; - Ok(entries - .into_iter() - .map(|(ts, hr, share)| PoolHashrateEntry { - timestamp: ts, - avg_hashrate: hr, - share, - pool_name: pool.name.to_string(), - }) - .collect()) + let pool_name = pools().get(slug).name.to_string(); + let shared = self.hashrate_shared_data(0)?; + let pool_cum = self.pool_daily_cumulative(slug, shared.start_day, shared.end_day)?; + Ok(Self::compute_hashrate_entries( + &shared, &pool_cum, &pool_name, SAMPLE_WEEKLY, + )) } pub fn pools_hashrate( &self, time_period: Option, ) -> Result> { - let current_height = self.height().to_usize(); - let start = match time_period { - Some(tp) => current_height.saturating_sub(tp.block_count()), + let start_height = match time_period { + Some(tp) => { + let lookback = &self.computer().blocks.lookback; + let current_height = self.height(); + match tp { + TimePeriod::Day => lookback.cached_window_starts.0._24h.collect_one(current_height), + TimePeriod::ThreeDays => lookback._3d.collect_one(current_height), + TimePeriod::Week => lookback.cached_window_starts.0._1w.collect_one(current_height), + TimePeriod::Month => lookback.cached_window_starts.0._1m.collect_one(current_height), + TimePeriod::ThreeMonths => lookback._3m.collect_one(current_height), + TimePeriod::SixMonths => lookback._6m.collect_one(current_height), + TimePeriod::Year => lookback.cached_window_starts.0._1y.collect_one(current_height), + TimePeriod::TwoYears => lookback._2y.collect_one(current_height), + TimePeriod::ThreeYears => lookback._3y.collect_one(current_height), + } + .unwrap_or_default() + .to_usize() + } None => 0, }; + + let shared = self.hashrate_shared_data(start_height)?; let pools_list = pools(); let mut entries = Vec::new(); for pool in pools_list.iter() { - if let Ok(pool_entries) = self.compute_pool_hashrate_entries(pool.slug, start) { - for (ts, hr, share) in pool_entries { - if share > 0.0 { - entries.push(PoolHashrateEntry { - timestamp: ts, - avg_hashrate: hr, - share, - pool_name: pool.name.to_string(), - }); - } - } - } + let Ok(pool_cum) = + self.pool_daily_cumulative(pool.slug, shared.start_day, shared.end_day) + else { + continue; + }; + entries.extend(Self::compute_hashrate_entries( + &shared, + &pool_cum, + &pool.name, + SAMPLE_WEEKLY, + )); } Ok(entries) } - /// Compute (timestamp, hashrate, share) tuples for a pool from `start_height`. - fn compute_pool_hashrate_entries( + /// Shared data needed for hashrate computation (read once, reuse across pools). + fn hashrate_shared_data(&self, start_height: usize) -> Result { + let computer = self.computer(); + let current_height = self.height(); + let start_day = computer + .indexes + .height + .day1 + .collect_one_at(start_height) + .unwrap_or_default() + .to_usize(); + let end_day = computer + .indexes + .height + .day1 + .collect_one(current_height) + .unwrap_or_default() + .to_usize() + + 1; + let daily_hashrate = computer + .mining + .hashrate + .rate + .base + .day1 + .collect_range_at(start_day, end_day); + let first_heights = computer + .indexes + .day1 + .first_height + .collect_range_at(start_day, end_day); + + Ok(HashrateSharedData { + start_day, + end_day, + daily_hashrate, + first_heights, + }) + } + + /// Read daily cumulative blocks mined for a pool. + fn pool_daily_cumulative( &self, slug: PoolSlug, - start_height: usize, - ) -> Result> { + start_day: usize, + end_day: usize, + ) -> Result>> { let computer = self.computer(); - let indexer = self.indexer(); - let end = self.height().to_usize() + 1; - let start = start_height; - - let dominance_bps = computer + computer .pools .major .get(&slug) - .map(|v| &v.base.dominance.bps.height) + .map(|v| v.base.blocks_mined.cumulative.day1.collect_range_at(start_day, end_day)) .or_else(|| { computer .pools .minor .get(&slug) - .map(|v| &v.dominance.bps.height) + .map(|v| v.blocks_mined.cumulative.day1.collect_range_at(start_day, end_day)) }) - .ok_or_else(|| Error::NotFound("Pool not found".into()))?; + .ok_or_else(|| Error::NotFound("Pool not found".into())) + } - let total = end - start; - let step = (total / 200).max(1); + /// Compute hashrate entries from daily cumulative blocks + shared data. + /// Uses 7-day windowed share: pool_blocks_in_week / total_blocks_in_week. + fn compute_hashrate_entries( + shared: &HashrateSharedData, + pool_cum: &[Option], + pool_name: &str, + sample_days: usize, + ) -> Vec { + let total = pool_cum.len(); + if total <= LOOKBACK_DAYS { + return vec![]; + } - // Batch read everything for the range - let timestamps = indexer.vecs.blocks.timestamp.collect_range_at(start, end); - let bps_values = dominance_bps.collect_range_at(start, end); - let day1_values = computer.indexes.height.day1.collect_range_at(start, end); - let hashrate_vec = &computer.mining.hashrate.rate.base.day1; + let mut entries = Vec::new(); + let mut i = LOOKBACK_DAYS; + while i < total { + if let (Some(cum_now), Some(cum_prev)) = + (pool_cum[i], pool_cum[i - LOOKBACK_DAYS]) + { + let pool_blocks = (*cum_now).saturating_sub(*cum_prev); + if pool_blocks > 0 { + let h_now = shared.first_heights[i].to_usize(); + let h_prev = shared.first_heights[i - LOOKBACK_DAYS].to_usize(); + let total_blocks = h_now.saturating_sub(h_prev); - // Pre-read all needed hashrates by collecting unique day1 values - let max_day = day1_values.iter().map(|d| d.to_usize()).max().unwrap_or(0); - let min_day = day1_values.iter().map(|d| d.to_usize()).min().unwrap_or(0); - let hashrates = hashrate_vec.collect_range_dyn(min_day, max_day + 1); + if total_blocks > 0 { + if let Some(hr) = shared.daily_hashrate[i].as_ref() { + let network_hr = f64::from(**hr); + let share = pool_blocks as f64 / total_blocks as f64; + let day = Day1::from(shared.start_day + i); + entries.push(PoolHashrateEntry { + timestamp: day.to_timestamp(), + avg_hashrate: (network_hr * share) as u128, + share, + pool_name: pool_name.to_string(), + }); + } + } + } + } + i += sample_days; + } - Ok((0..total) - .step_by(step) - .filter_map(|i| { - let bps = *bps_values[i]; - let share = bps as f64 / 10000.0; - let day_idx = day1_values[i].to_usize() - min_day; - let network_hr = f64::from(*hashrates.get(day_idx)?.as_ref()?); - Some((timestamps[i], (network_hr * share) as u128, share)) - }) - .collect()) + entries } } diff --git a/crates/brk_query/src/impl/price.rs b/crates/brk_query/src/impl/price.rs index 424f04366..9a7d30972 100644 --- a/crates/brk_query/src/impl/price.rs +++ b/crates/brk_query/src/impl/price.rs @@ -1,6 +1,6 @@ use brk_error::Result; -use brk_types::{Dollars, ExchangeRates, HistoricalPrice, HistoricalPriceEntry, Timestamp}; -use vecdb::{ReadableVec, VecIndex}; +use brk_types::{Dollars, ExchangeRates, HistoricalPrice, HistoricalPriceEntry, Hour4, Timestamp}; +use vecdb::ReadableVec; use crate::Query; @@ -21,38 +21,41 @@ impl Query { } pub fn historical_price(&self, timestamp: Option) -> Result { - let indexer = self.indexer(); - let computer = self.computer(); - let max_height = self.height().to_usize(); - let end = max_height + 1; - - let timestamps = indexer.vecs.blocks.timestamp.collect(); - let all_prices = computer.prices.spot.cents.height.collect(); - - let prices = if let Some(target_ts) = timestamp { - let target = usize::from(target_ts); - let h = timestamps - .binary_search_by_key(&target, |t| usize::from(*t)) - .unwrap_or_else(|i| i.min(max_height)); - - vec![HistoricalPriceEntry { - time: usize::from(timestamps[h]) as u64, - usd: Dollars::from(all_prices[h]), - }] - } else { - let step = (max_height / 200).max(1); - (0..end) - .step_by(step) - .map(|h| HistoricalPriceEntry { - time: usize::from(timestamps[h]) as u64, - usd: Dollars::from(all_prices[h]), - }) - .collect() + let prices = match timestamp { + Some(ts) => self.price_at(ts)?, + None => self.all_prices()?, }; - Ok(HistoricalPrice { prices, exchange_rates: ExchangeRates {}, }) } + + fn price_at(&self, target: Timestamp) -> Result> { + let h4 = Hour4::from_timestamp(target); + let cents = self.computer().prices.spot.cents.hour4.collect_one(h4); + Ok(vec![HistoricalPriceEntry { + time: usize::from(h4.to_timestamp()) as u64, + usd: Dollars::from(cents.flatten().unwrap_or_default()), + }]) + } + + fn all_prices(&self) -> Result> { + let computer = self.computer(); + Ok(computer + .prices + .spot + .cents + .hour4 + .collect() + .into_iter() + .enumerate() + .filter_map(|(i, cents)| { + Some(HistoricalPriceEntry { + time: usize::from(Hour4::from(i).to_timestamp()) as u64, + usd: Dollars::from(cents?), + }) + }) + .collect()) + } } diff --git a/crates/brk_query/src/impl/tx.rs b/crates/brk_query/src/impl/tx.rs index 754b779af..3008317cc 100644 --- a/crates/brk_query/src/impl/tx.rs +++ b/crates/brk_query/src/impl/tx.rs @@ -1,10 +1,8 @@ -use std::io::Cursor; - -use bitcoin::{consensus::Decodable, hex::DisplayHex}; +use bitcoin::hex::DisplayHex; use brk_error::{Error, Result}; use brk_types::{ - Height, MerkleProof, OutputType, Sats, Transaction, TxIn, TxInIndex, TxIndex, TxOut, - TxOutspend, TxStatus, Txid, TxidParam, TxidPrefix, Vin, Vout, Weight, + BlockHash, Height, MerkleProof, Timestamp, TxInIndex, TxIndex, TxOutspend, TxStatus, + Transaction, Txid, TxidParam, TxidPrefix, Vin, Vout, }; use vecdb::{ReadableVec, VecIndex}; @@ -109,43 +107,11 @@ impl Query { self.transaction_hex_by_index(tx_index) } - pub fn outspend(&self, TxidParam { txid }: TxidParam, vout: Vout) -> Result { - // Mempool outputs are unspent in on-chain terms - if let Some(mempool) = self.mempool() - && mempool.get_txs().contains_key(&txid) - { - return Ok(TxOutspend::UNSPENT); - } - - // Look up confirmed transaction - let prefix = TxidPrefix::from(&txid); - let indexer = self.indexer(); - let Ok(Some(tx_index)) = indexer - .stores - .txid_prefix_to_tx_index - .get(&prefix) - .map(|opt| opt.map(|cow| cow.into_owned())) - else { - return Err(Error::UnknownTxid); - }; - - // Calculate txout_index - let first_txout_index = indexer - .vecs - .transactions - .first_txout_index - .read_once(tx_index)?; - let txout_index = first_txout_index + vout; - - // Look up spend status - let computer = self.computer(); - let txin_index = computer.outputs.spent.txin_index.read_once(txout_index)?; - - if txin_index == TxInIndex::UNSPENT { - return Ok(TxOutspend::UNSPENT); - } - - self.outspend_details(txin_index) + pub fn outspend(&self, txid: TxidParam, vout: Vout) -> Result { + let all = self.outspends(txid)?; + all.into_iter() + .nth(usize::from(vout)) + .ok_or(Error::OutOfRange("Output index out of range".into())) } pub fn outspends(&self, TxidParam { txid }: TxidParam) -> Result> { @@ -185,6 +151,16 @@ impl Query { // Get spend status for each output let computer = self.computer(); let txin_index_reader = computer.outputs.spent.txin_index.reader(); + let txid_reader = indexer.vecs.transactions.txid.reader(); + + // Cursors for PcoVec reads — buffer chunks so nearby indices share decompression + let mut input_tx_cursor = indexer.vecs.inputs.tx_index.cursor(); + let mut first_txin_cursor = indexer.vecs.transactions.first_txin_index.cursor(); + let mut height_cursor = indexer.vecs.transactions.height.cursor(); + let mut block_ts_cursor = indexer.vecs.blocks.timestamp.cursor(); + + // Block info cache — spending txs in the same block share block hash/time + let mut cached_block: Option<(Height, BlockHash, Timestamp)> = None; let mut outspends = Vec::with_capacity(output_count); for i in 0..output_count { @@ -193,9 +169,38 @@ impl Query { if txin_index == TxInIndex::UNSPENT { outspends.push(TxOutspend::UNSPENT); - } else { - outspends.push(self.outspend_details(txin_index)?); + continue; } + + let spending_tx_index = input_tx_cursor.get(usize::from(txin_index)).unwrap(); + let spending_first_txin_index = + first_txin_cursor.get(spending_tx_index.to_usize()).unwrap(); + let vin = + Vin::from(usize::from(txin_index) - usize::from(spending_first_txin_index)); + let spending_txid = txid_reader.get(spending_tx_index.to_usize()); + let spending_height = height_cursor.get(spending_tx_index.to_usize()).unwrap(); + + let (block_hash, block_time) = + if let Some((h, ref bh, bt)) = cached_block && h == spending_height { + (bh.clone(), bt) + } else { + let bh = indexer.vecs.blocks.blockhash.read_once(spending_height)?; + let bt = block_ts_cursor.get(spending_height.to_usize()).unwrap(); + cached_block = Some((spending_height, bh.clone(), bt)); + (bh, bt) + }; + + outspends.push(TxOutspend { + spent: true, + txid: Some(spending_txid), + vin: Some(vin), + status: Some(TxStatus { + confirmed: true, + block_height: Some(spending_height), + block_hash: Some(block_hash), + block_time: Some(block_time), + }), + }); } Ok(outspends) @@ -204,155 +209,10 @@ impl Query { // === Helper methods === pub fn transaction_by_index(&self, tx_index: TxIndex) -> Result { - let indexer = self.indexer(); - let reader = self.reader(); - - // Get tx metadata using collect_one for PcoVec, read_once for BytesVec - let txid = indexer.vecs.transactions.txid.read_once(tx_index)?; - let height = indexer - .vecs - .transactions - .height - .collect_one(tx_index) - .unwrap(); - let version = indexer - .vecs - .transactions - .tx_version - .collect_one(tx_index) - .unwrap(); - let lock_time = indexer - .vecs - .transactions - .raw_locktime - .collect_one(tx_index) - .unwrap(); - let total_size = indexer - .vecs - .transactions - .total_size - .collect_one(tx_index) - .unwrap(); - let first_txin_index = indexer - .vecs - .transactions - .first_txin_index - .collect_one(tx_index) - .unwrap(); - let position = indexer - .vecs - .transactions - .position - .collect_one(tx_index) - .unwrap(); - - // Get block info for status - let block_hash = indexer.vecs.blocks.blockhash.read_once(height)?; - let block_time = indexer.vecs.blocks.timestamp.collect_one(height).unwrap(); - - // Read and decode the raw transaction from blk file - let buffer = reader.read_raw_bytes(position, *total_size as usize)?; - let mut cursor = Cursor::new(buffer); - let tx = bitcoin::Transaction::consensus_decode(&mut cursor) - .map_err(|_| Error::Parse("Failed to decode transaction".into()))?; - - // Create readers for random access lookups - let txid_reader = indexer.vecs.transactions.txid.reader(); - let first_txout_index_reader = indexer.vecs.transactions.first_txout_index.reader(); - let value_reader = indexer.vecs.outputs.value.reader(); - let output_type_reader = indexer.vecs.outputs.output_type.reader(); - let type_index_reader = indexer.vecs.outputs.type_index.reader(); - let addr_readers = indexer.vecs.addrs.addr_readers(); - - // Batch-read outpoints for all inputs (avoids per-input PcoVec page decompression) - let outpoints: Vec<_> = indexer.vecs.inputs.outpoint.collect_range_at( - usize::from(first_txin_index), - usize::from(first_txin_index) + tx.input.len(), - ); - - // Build inputs with prevout information - let input: Vec = tx - .input - .iter() - .enumerate() - .map(|(i, txin)| { - let outpoint = outpoints[i]; - - let is_coinbase = outpoint.is_coinbase(); - - // Get prevout info if not coinbase - let (prev_txid, prev_vout, prevout) = if is_coinbase { - (Txid::COINBASE, Vout::MAX, None) - } else { - let prev_tx_index = outpoint.tx_index(); - let prev_vout = outpoint.vout(); - let prev_txid = txid_reader.get(prev_tx_index.to_usize()); - - // Calculate the txout_index for the prevout - let prev_first_txout_index = - first_txout_index_reader.get(prev_tx_index.to_usize()); - let prev_txout_index = prev_first_txout_index + prev_vout; - - let prev_value = value_reader.get(usize::from(prev_txout_index)); - let prev_output_type: OutputType = - output_type_reader.get(usize::from(prev_txout_index)); - let prev_type_index = type_index_reader.get(usize::from(prev_txout_index)); - let script_pubkey = - addr_readers.script_pubkey(prev_output_type, prev_type_index); - - let prevout = Some(TxOut::from((script_pubkey, prev_value))); - - (prev_txid, prev_vout, prevout) - }; - - TxIn { - txid: prev_txid, - vout: prev_vout, - prevout, - script_sig: txin.script_sig.clone(), - script_sig_asm: (), - is_coinbase, - sequence: txin.sequence.0, - inner_redeem_script_asm: (), - } - }) - .collect(); - - // Calculate weight before consuming tx.output - let weight = Weight::from(tx.weight()); - - // Calculate sigop cost - let total_sigop_cost = tx.total_sigop_cost(|_| None); - - // Build outputs - let output: Vec = tx.output.into_iter().map(TxOut::from).collect(); - - // Build status - let status = TxStatus { - confirmed: true, - block_height: Some(height), - block_hash: Some(block_hash), - block_time: Some(block_time), - }; - - let mut transaction = Transaction { - index: Some(tx_index), - txid, - version, - lock_time, - total_size: *total_size as usize, - weight, - total_sigop_cost, - fee: Sats::ZERO, // Will be computed below - input, - output, - status, - }; - - // Compute fee from inputs - outputs - transaction.compute_fee(); - - Ok(transaction) + self.transactions_by_range(tx_index.to_usize(), 1)? + .into_iter() + .next() + .ok_or(Error::NotFound("Transaction not found".into())) } fn transaction_raw_by_index(&self, tx_index: TxIndex) -> Result> { @@ -366,60 +226,7 @@ impl Query { Ok(self.transaction_raw_by_index(tx_index)?.to_lower_hex_string()) } - fn outspend_details(&self, txin_index: TxInIndex) -> Result { - let indexer = self.indexer(); - - // Look up spending tx_index directly - let spending_tx_index = indexer - .vecs - .inputs - .tx_index - .collect_one(txin_index) - .unwrap(); - - // Calculate vin - let spending_first_txin_index = indexer - .vecs - .transactions - .first_txin_index - .collect_one(spending_tx_index) - .unwrap(); - let vin = Vin::from(usize::from(txin_index) - usize::from(spending_first_txin_index)); - - // Get spending tx details - let spending_txid = indexer - .vecs - .transactions - .txid - .read_once(spending_tx_index)?; - let spending_height = indexer - .vecs - .transactions - .height - .collect_one(spending_tx_index) - .unwrap(); - let block_hash = indexer.vecs.blocks.blockhash.read_once(spending_height)?; - let block_time = indexer - .vecs - .blocks - .timestamp - .collect_one(spending_height) - .unwrap(); - - Ok(TxOutspend { - spent: true, - txid: Some(spending_txid), - vin: Some(vin), - status: Some(TxStatus { - confirmed: true, - block_height: Some(spending_height), - block_hash: Some(block_hash), - block_time: Some(block_time), - }), - }) - } - - fn resolve_tx(&self, txid: &Txid) -> Result<(TxIndex, Height)> { + pub fn resolve_tx(&self, txid: &Txid) -> Result<(TxIndex, Height)> { let indexer = self.indexer(); let prefix = TxidPrefix::from(txid); let tx_index: TxIndex = indexer diff --git a/crates/brk_query/src/lib.rs b/crates/brk_query/src/lib.rs index fbd4a7bae..5739d9f3a 100644 --- a/crates/brk_query/src/lib.rs +++ b/crates/brk_query/src/lib.rs @@ -8,7 +8,7 @@ use brk_indexer::Indexer; use brk_mempool::Mempool; use brk_reader::Reader; use brk_rpc::Client; -use brk_types::{Height, SyncStatus}; +use brk_types::{BlockHash, BlockHashPrefix, Height, SyncStatus}; use vecdb::{AnyVec, ReadOnlyClone, ReadableVec, Ro}; #[cfg(feature = "tokio")] @@ -72,6 +72,16 @@ impl Query { self.indexed_height().min(self.computed_height()) } + /// Tip block hash, cached in the indexer. + pub fn tip_blockhash(&self) -> BlockHash { + self.indexer().tip_blockhash() + } + + /// Tip block hash prefix for cache etags. + pub fn tip_hash_prefix(&self) -> BlockHashPrefix { + BlockHashPrefix::from(&self.tip_blockhash()) + } + /// Build sync status with the given tip height pub fn sync_status(&self, tip_height: Height) -> SyncStatus { let indexed_height = self.indexed_height(); diff --git a/crates/brk_rpc/src/lib.rs b/crates/brk_rpc/src/lib.rs index 140852506..4bbfe3aa9 100644 --- a/crates/brk_rpc/src/lib.rs +++ b/crates/brk_rpc/src/lib.rs @@ -137,6 +137,12 @@ impl Client { None }; + let witness = txin + .witness + .iter() + .map(|w| bitcoin::hex::DisplayHex::to_lower_hex_string(w)) + .collect(); + Ok(TxIn { is_coinbase, prevout: txout, @@ -144,8 +150,10 @@ impl Client { vout: txin.previous_output.vout.into(), script_sig: txin.script_sig, script_sig_asm: (), + witness, sequence: txin.sequence.into(), inner_redeem_script_asm: (), + inner_witness_script_asm: (), }) }) .collect::>>()?; diff --git a/crates/brk_server/src/api/mempool_space/addrs.rs b/crates/brk_server/src/api/mempool_space/addrs.rs index f177d1887..d3f36a88f 100644 --- a/crates/brk_server/src/api/mempool_space/addrs.rs +++ b/crates/brk_server/src/api/mempool_space/addrs.rs @@ -7,7 +7,7 @@ use axum::{ }; use brk_types::{ AddrParam, AddrStats, AddrTxidsParam, AddrValidation, Transaction, Txid, Utxo, - ValidateAddrParam, + ValidateAddrParam, Version, }; use crate::{AppState, CacheStrategy, extended::TransformResponseExtended}; @@ -29,7 +29,8 @@ impl AddrRoutes for ApiRouter { Path(path): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.addr(path.addr)).await + let strategy = state.addr_cache(Version::ONE, &path.addr); + state.cached_json(&headers, strategy, &uri, move |q| q.addr(path.addr)).await }, |op| op .id("get_address") .addrs_tag() @@ -51,7 +52,8 @@ impl AddrRoutes for ApiRouter { Query(params): Query, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 25)).await + let strategy = state.addr_cache(Version::ONE, &path.addr); + state.cached_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 50)).await }, |op| op .id("get_address_txs") .addrs_tag() @@ -73,7 +75,8 @@ impl AddrRoutes for ApiRouter { Query(params): Query, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 25)).await + let strategy = state.addr_cache(Version::ONE, &path.addr); + state.cached_json(&headers, strategy, &uri, move |q| q.addr_txs(path.addr, params.after_txid, 25)).await }, |op| op .id("get_address_confirmed_txs") .addrs_tag() @@ -115,7 +118,8 @@ impl AddrRoutes for ApiRouter { Path(path): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.addr_utxos(path.addr)).await + let strategy = state.addr_cache(Version::ONE, &path.addr); + state.cached_json(&headers, strategy, &uri, move |q| q.addr_utxos(path.addr)).await }, |op| op .id("get_address_utxos") .addrs_tag() diff --git a/crates/brk_server/src/api/mempool_space/blocks.rs b/crates/brk_server/src/api/mempool_space/blocks.rs index 5a64e00cd..980221d05 100644 --- a/crates/brk_server/src/api/mempool_space/blocks.rs +++ b/crates/brk_server/src/api/mempool_space/blocks.rs @@ -6,7 +6,7 @@ use axum::{ use brk_query::BLOCK_TXS_PAGE_SIZE; use brk_types::{ BlockHashParam, BlockHashStartIndex, BlockHashTxIndex, BlockInfo, BlockInfoV1, BlockStatus, - BlockTimestamp, HeightParam, TimestampParam, Transaction, TxIndex, Txid, + BlockTimestamp, HeightParam, TimestampParam, Transaction, TxIndex, Txid, Version, }; use crate::{AppState, CacheStrategy, extended::TransformResponseExtended}; @@ -24,7 +24,8 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block(&path.hash)).await + let strategy = state.block_cache(Version::ONE, &path.hash); + state.cached_json(&headers, strategy, &uri, move |q| q.block(&path.hash)).await }, |op| { op.id("get_block") @@ -45,7 +46,8 @@ impl BlockRoutes for ApiRouter { "/api/v1/block/{hash}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + let strategy = state.block_cache(Version::ONE, &path.hash); + state.cached_json(&headers, strategy, &uri, move |q| { let height = q.height_by_hash(&path.hash)?; q.block_by_height_v1(height) }).await @@ -66,7 +68,8 @@ impl BlockRoutes for ApiRouter { "/api/block/{hash}/header", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.block_header_hex(&path.hash)).await + let strategy = state.block_cache(Version::ONE, &path.hash); + state.cached_text(&headers, strategy, &uri, move |q| q.block_header_hex(&path.hash)).await }, |op| { op.id("get_block_header") @@ -87,7 +90,7 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.block_hash_by_height(path.height).map(|h| h.to_string())).await + state.cached_text(&headers, state.height_cache(Version::ONE, path.height), &uri, move |q| q.block_hash_by_height(path.height).map(|h| h.to_string())).await }, |op| { op.id("get_block_by_height") @@ -111,7 +114,7 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_by_timestamp(path.timestamp)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_by_timestamp(path.timestamp)).await }, |op| { op.id("get_block_by_timestamp") @@ -133,7 +136,8 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_bytes(&headers, CacheStrategy::Static, &uri, move |q| q.block_raw(&path.hash)).await + let strategy = state.block_cache(Version::ONE, &path.hash); + state.cached_bytes(&headers, strategy, &uri, move |q| q.block_raw(&path.hash)).await }, |op| { op.id("get_block_raw") @@ -157,7 +161,7 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_status(&path.hash)).await + state.cached_json(&headers, state.block_status_cache(Version::ONE, &path.hash), &uri, move |q| q.block_status(&path.hash)).await }, |op| { op.id("get_block_status") @@ -178,7 +182,7 @@ impl BlockRoutes for ApiRouter { "/api/blocks/tip/height", get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { - state.cached_text(&headers, CacheStrategy::Height, &uri, |q| Ok(q.height().to_string())).await + state.cached_text(&headers, CacheStrategy::Tip, &uri, |q| Ok(q.height().to_string())).await }, |op| { op.id("get_block_tip_height") @@ -195,7 +199,7 @@ impl BlockRoutes for ApiRouter { "/api/blocks/tip/hash", get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { - state.cached_text(&headers, CacheStrategy::Height, &uri, |q| q.block_hash_by_height(q.height()).map(|h| h.to_string())).await + state.cached_text(&headers, CacheStrategy::Tip, &uri, |q| q.block_hash_by_height(q.height()).map(|h| h.to_string())).await }, |op| { op.id("get_block_tip_hash") @@ -215,7 +219,8 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_text(&headers, CacheStrategy::Static, &uri, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await + let strategy = state.block_cache(Version::ONE, &path.hash); + state.cached_text(&headers, strategy, &uri, move |q| q.block_txid_at_index(&path.hash, path.index).map(|t| t.to_string())).await }, |op| { op.id("get_block_txid") @@ -239,7 +244,8 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txids(&path.hash)).await + let strategy = state.block_cache(Version::ONE, &path.hash); + state.cached_json(&headers, strategy, &uri, move |q| q.block_txids(&path.hash)).await }, |op| { op.id("get_block_txids") @@ -263,7 +269,8 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txs(&path.hash, TxIndex::default())).await + let strategy = state.block_cache(Version::ONE, &path.hash); + state.cached_json(&headers, strategy, &uri, move |q| q.block_txs(&path.hash, TxIndex::default())).await }, |op| { op.id("get_block_txs") @@ -288,7 +295,8 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Static, &uri, move |q| q.block_txs(&path.hash, path.start_index)).await + let strategy = state.block_cache(Version::ONE, &path.hash); + state.cached_json(&headers, strategy, &uri, move |q| q.block_txs(&path.hash, path.start_index)).await }, |op| { op.id("get_block_txs_from_index") @@ -311,7 +319,7 @@ impl BlockRoutes for ApiRouter { get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(None)) + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.blocks(None)) .await }, |op| { @@ -332,7 +340,7 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks(Some(path.height))).await + state.cached_json(&headers, state.height_cache(Version::ONE, path.height), &uri, move |q| q.blocks(Some(path.height))).await }, |op| { op.id("get_blocks_from_height") @@ -353,7 +361,7 @@ impl BlockRoutes for ApiRouter { get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks_v1(None)) + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.blocks_v1(None)) .await }, |op| { @@ -374,7 +382,7 @@ impl BlockRoutes for ApiRouter { headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.blocks_v1(Some(path.height))).await + state.cached_json(&headers, state.height_cache(Version::ONE, path.height), &uri, move |q| q.blocks_v1(Some(path.height))).await }, |op| { op.id("get_blocks_v1_from_height") diff --git a/crates/brk_server/src/api/mempool_space/general.rs b/crates/brk_server/src/api/mempool_space/general.rs index 3ebe32edc..818c2ad17 100644 --- a/crates/brk_server/src/api/mempool_space/general.rs +++ b/crates/brk_server/src/api/mempool_space/general.rs @@ -18,7 +18,7 @@ impl GeneralRoutes for ApiRouter { get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, &uri, |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, |q| { q.difficulty_adjustment() }) .await @@ -65,7 +65,7 @@ impl GeneralRoutes for ApiRouter { Query(params): Query, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.historical_price(params.timestamp) }) .await diff --git a/crates/brk_server/src/api/mempool_space/mining.rs b/crates/brk_server/src/api/mempool_space/mining.rs index ca8968a8f..c098a6e18 100644 --- a/crates/brk_server/src/api/mempool_space/mining.rs +++ b/crates/brk_server/src/api/mempool_space/mining.rs @@ -45,7 +45,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/pools/{time_period}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.mining_pools(path.time_period)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.mining_pools(path.time_period)).await }, |op| { op.id("get_pool_stats") @@ -62,7 +62,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/pool/{slug}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_detail(path.slug)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_detail(path.slug)).await }, |op| { op.id("get_pool") @@ -80,7 +80,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/hashrate/pools", get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.pools_hashrate(None)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, |q| q.pools_hashrate(None)).await }, |op| { op.id("get_pools_hashrate") @@ -97,7 +97,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/hashrate/pools/{time_period}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pools_hashrate(Some(path.time_period))).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pools_hashrate(Some(path.time_period))).await }, |op| { op.id("get_pools_hashrate_by_period") @@ -114,7 +114,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/pool/{slug}/hashrate", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_hashrate(path.slug)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_hashrate(path.slug)).await }, |op| { op.id("get_pool_hashrate") @@ -132,7 +132,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/pool/{slug}/blocks", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_blocks(path.slug, None)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_blocks(path.slug, None)).await }, |op| { op.id("get_pool_blocks") @@ -150,7 +150,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/pool/{slug}/blocks/{height}", get_with( async |uri: Uri, headers: HeaderMap, Path(PoolSlugAndHeightParam {slug, height}): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.pool_blocks(slug, Some(height))).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.pool_blocks(slug, Some(height))).await }, |op| { op.id("get_pool_blocks_from") @@ -168,7 +168,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/hashrate", get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.hashrate(None)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, |q| q.hashrate(None)).await }, |op| { op.id("get_hashrate") @@ -185,7 +185,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/hashrate/{time_period}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.hashrate(Some(path.time_period))).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.hashrate(Some(path.time_period))).await }, |op| { op.id("get_hashrate_by_period") @@ -202,7 +202,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/difficulty-adjustments", get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, |q| q.difficulty_adjustments(None)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, |q| q.difficulty_adjustments(None)).await }, |op| { op.id("get_difficulty_adjustments") @@ -219,7 +219,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/difficulty-adjustments/{time_period}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.difficulty_adjustments(Some(path.time_period))).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.difficulty_adjustments(Some(path.time_period))).await }, |op| { op.id("get_difficulty_adjustments_by_period") @@ -236,7 +236,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/reward-stats/{block_count}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.reward_stats(path.block_count)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.reward_stats(path.block_count)).await }, |op| { op.id("get_reward_stats") @@ -253,7 +253,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/blocks/fees/{time_period}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_fees(path.time_period)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_fees(path.time_period)).await }, |op| { op.id("get_block_fees") @@ -270,7 +270,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/blocks/rewards/{time_period}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_rewards(path.time_period)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_rewards(path.time_period)).await }, |op| { op.id("get_block_rewards") @@ -302,7 +302,7 @@ impl MiningRoutes for ApiRouter { "/api/v1/mining/blocks/sizes-weights/{time_period}", get_with( async |uri: Uri, headers: HeaderMap, Path(path): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.block_sizes_weights(path.time_period)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.block_sizes_weights(path.time_period)).await }, |op| { op.id("get_block_sizes_weights") diff --git a/crates/brk_server/src/api/mempool_space/transactions.rs b/crates/brk_server/src/api/mempool_space/transactions.rs index 9e21eeecb..55a465690 100644 --- a/crates/brk_server/src/api/mempool_space/transactions.rs +++ b/crates/brk_server/src/api/mempool_space/transactions.rs @@ -7,7 +7,8 @@ use axum::{ http::{HeaderMap, Uri}, }; use brk_types::{ - CpfpInfo, MerkleProof, Transaction, TxOutspend, TxStatus, Txid, TxidParam, TxidVout, TxidsParam, + CpfpInfo, MerkleProof, Transaction, TxOutspend, TxStatus, Txid, TxidParam, TxidVout, + TxidsParam, Version, }; use crate::{AppState, CacheStrategy, extended::TransformResponseExtended}; @@ -22,8 +23,8 @@ impl TxRoutes for ApiRouter { .api_route( "/api/v1/cpfp/{txid}", get_with( - async |uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State| { - state.cached_json(&headers, state.mempool_cache(), &uri, move |q| q.cpfp(txid)).await + async |uri: Uri, headers: HeaderMap, Path(param): Path, State(state): State| { + state.cached_json(&headers, state.tx_cache(Version::ONE, ¶m.txid), &uri, move |q| q.cpfp(param)).await }, |op| op .id("get_cpfp") @@ -41,10 +42,10 @@ impl TxRoutes for ApiRouter { async | uri: Uri, headers: HeaderMap, - Path(txid): Path, + Path(param): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.transaction(txid)).await + state.cached_json(&headers, state.tx_cache(Version::ONE, ¶m.txid), &uri, move |q| q.transaction(param)).await }, |op| op .id("get_tx") @@ -69,7 +70,7 @@ impl TxRoutes for ApiRouter { Path(txid): Path, State(state): State | { - state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_hex(txid)).await + state.cached_text(&headers, state.tx_cache(Version::ONE, &txid.txid), &uri, move |q| q.transaction_hex(txid)).await }, |op| op .id("get_tx_hex") @@ -89,7 +90,7 @@ impl TxRoutes for ApiRouter { "/api/tx/{txid}/merkleblock-proof", get_with( async |uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State| { - state.cached_text(&headers, CacheStrategy::Height, &uri, move |q| q.merkleblock_proof(txid)).await + state.cached_text(&headers, state.tx_cache(Version::ONE, &txid.txid), &uri, move |q| q.merkleblock_proof(txid)).await }, |op| op .id("get_tx_merkleblock_proof") @@ -107,7 +108,7 @@ impl TxRoutes for ApiRouter { "/api/tx/{txid}/merkle-proof", get_with( async |uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State| { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.merkle_proof(txid)).await + state.cached_json(&headers, state.tx_cache(Version::ONE, &txid.txid), &uri, move |q| q.merkle_proof(txid)).await }, |op| op .id("get_tx_merkle_proof") @@ -131,7 +132,7 @@ impl TxRoutes for ApiRouter { State(state): State | { let txid = TxidParam { txid: path.txid }; - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.outspend(txid, path.vout)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.outspend(txid, path.vout)).await }, |op| op .id("get_tx_outspend") @@ -156,7 +157,7 @@ impl TxRoutes for ApiRouter { Path(txid): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.outspends(txid)).await + state.cached_json(&headers, CacheStrategy::Tip, &uri, move |q| q.outspends(txid)).await }, |op| op .id("get_tx_outspends") @@ -176,7 +177,7 @@ impl TxRoutes for ApiRouter { "/api/tx/{txid}/raw", get_with( async |uri: Uri, headers: HeaderMap, Path(txid): Path, State(state): State| { - state.cached_bytes(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_raw(txid)).await + state.cached_bytes(&headers, state.tx_cache(Version::ONE, &txid.txid), &uri, move |q| q.transaction_raw(txid)).await }, |op| op .id("get_tx_raw") @@ -196,10 +197,10 @@ impl TxRoutes for ApiRouter { async | uri: Uri, headers: HeaderMap, - Path(txid): Path, + Path(param): Path, State(state): State | { - state.cached_json(&headers, CacheStrategy::Height, &uri, move |q| q.transaction_status(txid)).await + state.cached_json(&headers, state.tx_cache(Version::ONE, ¶m.txid), &uri, move |q| q.transaction_status(param)).await }, |op| op .id("get_tx_status") diff --git a/crates/brk_server/src/api/metrics/mod.rs b/crates/brk_server/src/api/metrics/mod.rs index 140506c0c..68e782c07 100644 --- a/crates/brk_server/src/api/metrics/mod.rs +++ b/crates/brk_server/src/api/metrics/mod.rs @@ -275,7 +275,7 @@ impl ApiMetricsLegacyRoutes for ApiRouter { State(state): State, Path(path): Path| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.latest(&path.metric, path.index) }) .await @@ -301,7 +301,7 @@ impl ApiMetricsLegacyRoutes for ApiRouter { State(state): State, Path(path): Path| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.len(&path.metric, path.index) }) .await @@ -327,7 +327,7 @@ impl ApiMetricsLegacyRoutes for ApiRouter { State(state): State, Path(path): Path| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.version(&path.metric, path.index) }) .await @@ -376,7 +376,7 @@ impl ApiMetricsLegacyRoutes for ApiRouter { Path(params): Path, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.cost_basis_dates(¶ms.cohort) }) .await diff --git a/crates/brk_server/src/api/series/mod.rs b/crates/brk_server/src/api/series/mod.rs index f7d7edf15..02748af0f 100644 --- a/crates/brk_server/src/api/series/mod.rs +++ b/crates/brk_server/src/api/series/mod.rs @@ -246,7 +246,7 @@ impl ApiSeriesRoutes for ApiRouter { State(state): State, Path(path): Path| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.latest(&path.series, path.index) }) .await @@ -270,7 +270,7 @@ impl ApiSeriesRoutes for ApiRouter { State(state): State, Path(path): Path| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.len(&path.series, path.index) }) .await @@ -292,7 +292,7 @@ impl ApiSeriesRoutes for ApiRouter { State(state): State, Path(path): Path| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.version(&path.series, path.index) }) .await @@ -352,7 +352,7 @@ impl ApiSeriesRoutes for ApiRouter { Path(params): Path, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { q.cost_basis_dates(¶ms.cohort) }) .await diff --git a/crates/brk_server/src/api/server/mod.rs b/crates/brk_server/src/api/server/mod.rs index dd58a42da..af13e77af 100644 --- a/crates/brk_server/src/api/server/mod.rs +++ b/crates/brk_server/src/api/server/mod.rs @@ -77,7 +77,7 @@ impl ServerRoutes for ApiRouter { get_with( async |uri: Uri, headers: HeaderMap, State(state): State| { state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { let tip_height = q.client().get_last_height()?; Ok(q.sync_status(tip_height)) }) @@ -102,7 +102,7 @@ impl ServerRoutes for ApiRouter { async |uri: Uri, headers: HeaderMap, State(state): State| { let brk_path = state.data_path.clone(); state - .cached_json(&headers, CacheStrategy::Height, &uri, move |q| { + .cached_json(&headers, CacheStrategy::Tip, &uri, move |q| { let brk_bytes = dir_size(&brk_path)?; let bitcoin_bytes = dir_size(q.blocks_dir())?; Ok(DiskUsage::new(brk_bytes, bitcoin_bytes)) diff --git a/crates/brk_server/src/cache.rs b/crates/brk_server/src/cache.rs index d5de5d0e9..eef737553 100644 --- a/crates/brk_server/src/cache.rs +++ b/crates/brk_server/src/cache.rs @@ -1,19 +1,28 @@ use axum::http::HeaderMap; +use brk_types::{BlockHashPrefix, Version}; use crate::{VERSION, extended::HeaderMapExtended}; /// Cache strategy for HTTP responses. pub enum CacheStrategy { - /// Data that changes with each new block (addresses, mining stats, txs, outspends) - /// Etag = VERSION-{height}, Cache-Control: must-revalidate - Height, + /// Chain-dependent data (addresses, mining stats, txs, outspends). + /// Etag = {tip_hash_prefix:x}. Invalidates on any tip change including reorgs. + Tip, - /// Static/immutable data (blocks by hash, validate-address, series catalog) - /// Etag = VERSION only, Cache-Control: must-revalidate + /// Immutable data identified by hash in the URL (blocks by hash, confirmed tx data). + /// Etag = {version}. Permanent; only bumped when response format changes. + Immutable(Version), + + /// Static non-chain data (validate-address, series catalog, pool list). + /// Etag = CARGO_PKG_VERSION. Invalidates on deploy. Static, - /// Mempool data - etag from next projected block hash + short max-age - /// Etag = VERSION-m{hash:x}, Cache-Control: max-age=1, must-revalidate + /// Immutable data bound to a specific block (confirmed tx data, block status). + /// Etag = {version}-{block_hash_prefix:x}. Invalidates naturally on reorg. + BlockBound(Version, BlockHashPrefix), + + /// Mempool data — etag from next projected block hash. + /// Etag = m{hash:x}. Invalidates on mempool change. MempoolHash(u64), } @@ -24,9 +33,12 @@ pub struct CacheParams { } impl CacheParams { - /// Cache params using VERSION as etag - pub fn version() -> Self { - Self::resolve(&CacheStrategy::Static, || unreachable!()) + /// Cache params using CARGO_PKG_VERSION as etag (for openapi.json etc.) + pub fn static_version() -> Self { + Self { + etag: Some(format!("s{VERSION}")), + cache_control: "public, max-age=1, must-revalidate".into(), + } } pub fn etag_str(&self) -> &str { @@ -39,20 +51,28 @@ impl CacheParams { .is_some_and(|etag| headers.has_etag(etag)) } - pub fn resolve(strategy: &CacheStrategy, height: impl FnOnce() -> u32) -> Self { - use CacheStrategy::*; + pub fn resolve(strategy: &CacheStrategy, tip: impl FnOnce() -> BlockHashPrefix) -> Self { + let cache_control = "public, max-age=1, must-revalidate".into(); match strategy { - Height => Self { - etag: Some(format!("{VERSION}-{}", height())), - cache_control: "public, max-age=1, must-revalidate".into(), + CacheStrategy::Tip => Self { + etag: Some(format!("t{:x}", *tip())), + cache_control, }, - Static => Self { - etag: Some(VERSION.to_string()), - cache_control: "public, max-age=1, must-revalidate".into(), + CacheStrategy::Immutable(v) => Self { + etag: Some(format!("i{v}")), + cache_control, }, - MempoolHash(hash) => Self { - etag: Some(format!("{VERSION}-m{hash:x}")), - cache_control: "public, max-age=1, must-revalidate".into(), + CacheStrategy::BlockBound(v, prefix) => Self { + etag: Some(format!("b{v}-{:x}", **prefix)), + cache_control, + }, + CacheStrategy::Static => Self { + etag: Some(format!("s{VERSION}")), + cache_control, + }, + CacheStrategy::MempoolHash(hash) => Self { + etag: Some(format!("m{hash:x}")), + cache_control, }, } } diff --git a/crates/brk_server/src/extended/response.rs b/crates/brk_server/src/extended/response.rs index b9eead3b3..a38f98493 100644 --- a/crates/brk_server/src/extended/response.rs +++ b/crates/brk_server/src/extended/response.rs @@ -47,7 +47,7 @@ impl ResponseExtended for Response { where T: Serialize, { - let params = CacheParams::version(); + let params = CacheParams::static_version(); if params.matches_etag(headers) { return Self::new_not_modified(); } diff --git a/crates/brk_server/src/state.rs b/crates/brk_server/src/state.rs index c08774948..621349613 100644 --- a/crates/brk_server/src/state.rs +++ b/crates/brk_server/src/state.rs @@ -5,13 +5,13 @@ use std::{ time::{Duration, Instant}, }; -use derive_more::Deref; - use axum::{ body::{Body, Bytes}, http::{HeaderMap, HeaderValue, Response, Uri, header}, }; use brk_query::AsyncQuery; +use brk_types::{Addr, BlockHash, BlockHashPrefix, Height, Txid, Version}; +use derive_more::Deref; use jiff::Timestamp; use quick_cache::sync::{Cache, GuardResult}; use serde::Serialize; @@ -33,6 +33,80 @@ pub struct AppState { } impl AppState { + /// `Immutable` if height is >6 deep, `Tip` otherwise. + pub fn height_cache(&self, version: Version, height: Height) -> CacheStrategy { + let is_deep = self.sync(|q| (*q.height()).saturating_sub(*height) > 6); + if is_deep { + CacheStrategy::Immutable(version) + } else { + CacheStrategy::Tip + } + } + + /// Smart address caching: checks mempool activity first, then on-chain. + /// - Address has mempool txs → `MempoolHash(addr_specific_hash)` + /// - No mempool, has on-chain activity → `BlockBound(last_activity_block)` + /// - Unknown address → `Tip` + pub fn addr_cache(&self, version: Version, addr: &Addr) -> CacheStrategy { + self.sync(|q| { + let mempool_hash = q.addr_mempool_hash(addr); + if mempool_hash != 0 { + return CacheStrategy::MempoolHash(mempool_hash); + } + q.addr_last_activity_height(addr) + .and_then(|h| { + let block_hash = q.block_hash_by_height(h)?; + Ok(CacheStrategy::BlockBound( + version, + BlockHashPrefix::from(&block_hash), + )) + }) + .unwrap_or(CacheStrategy::Tip) + }) + } + + /// `Immutable` if the block is >6 deep (status stable), `Tip` otherwise. + /// For block status which changes when the next block arrives. + pub fn block_status_cache(&self, version: Version, hash: &BlockHash) -> CacheStrategy { + self.sync(|q| { + q.height_by_hash(hash) + .map(|h| { + if (*q.height()).saturating_sub(*h) > 6 { + CacheStrategy::Immutable(version) + } else { + CacheStrategy::Tip + } + }) + .unwrap_or(CacheStrategy::Tip) + }) + } + + /// `BlockBound` if the block exists (reorg-safe via block hash), `Tip` if not found. + pub fn block_cache(&self, version: Version, hash: &BlockHash) -> CacheStrategy { + self.sync(|q| { + if q.height_by_hash(hash).is_ok() { + CacheStrategy::BlockBound(version, BlockHashPrefix::from(hash)) + } else { + CacheStrategy::Tip + } + }) + } + + /// Mempool → `MempoolHash`, confirmed → `BlockBound`, unknown → `Tip`. + pub fn tx_cache(&self, version: Version, txid: &Txid) -> CacheStrategy { + self.sync(|q| { + if q.mempool().is_some_and(|m| m.get_txs().contains(txid)) { + let hash = q.mempool().map(|m| m.next_block_hash()).unwrap_or(0); + return CacheStrategy::MempoolHash(hash); + } else if let Ok((_, height)) = q.resolve_tx(txid) + && let Ok(block_hash) = q.block_hash_by_height(height) + { + return CacheStrategy::BlockBound(version, BlockHashPrefix::from(&block_hash)); + } + CacheStrategy::Tip + }) + } + pub fn mempool_cache(&self) -> CacheStrategy { let hash = self.sync(|q| q.mempool().map(|m| m.next_block_hash()).unwrap_or(0)); CacheStrategy::MempoolHash(hash) @@ -51,7 +125,7 @@ impl AppState { F: FnOnce(&brk_query::Query, ContentEncoding) -> brk_error::Result + Send + 'static, { let encoding = ContentEncoding::negotiate(headers); - let params = CacheParams::resolve(&strategy, || self.sync(|q| q.height().into())); + let params = CacheParams::resolve(&strategy, || self.sync(|q| q.tip_hash_prefix())); if params.matches_etag(headers) { return ResponseExtended::new_not_modified(); } diff --git a/crates/brk_types/pools-v2.json b/crates/brk_types/pools-v2.json index 6949bf1ee..7d6c07c1f 100644 --- a/crates/brk_types/pools-v2.json +++ b/crates/brk_types/pools-v2.json @@ -2,12 +2,8 @@ { "id": 1, "name": "BlockFills", - "addresses": [ - "1PzVut5X6Nx7Mv4JHHKPtVM9Jr9LJ4Rbry" - ], - "tags": [ - "/BlockfillsPool/" - ], + "addresses": ["1PzVut5X6Nx7Mv4JHHKPtVM9Jr9LJ4Rbry"], + "tags": ["/BlockfillsPool/"], "link": "https://www.blockfills.com/mining" }, { @@ -17,9 +13,7 @@ "1EMVSMe1VJUuqv7D7SFzctnVXk4KdjXATi", "3C9sAKXrBVpJVe3b738yik4LPHpPmceBgd" ], - "tags": [ - "/ultimus/" - ], + "tags": ["/ultimus/"], "link": "https://www.ultimuspool.com" }, { @@ -30,10 +24,7 @@ "3Qqp7LwxmSjPwRaKkDToysJsM3xA4ThqFk", "bc1q39dled8an7enuxtmjql3pk7ny8kzvsxhd924sl" ], - "tags": [ - "terrapool.io", - "Validated with Clean Energy" - ], + "tags": ["terrapool.io", "Validated with Clean Energy"], "link": "https://terrapool.io" }, { @@ -44,10 +35,7 @@ "39bitUyBcUu3y3hRTtYprKbTp712t4ZWqK", "32BfKjhByDSxx3BM5vUkQ3NQq9csZR6nt6" ], - "tags": [ - "/LUXOR/", - "Luxor Tech" - ], + "tags": ["/LUXOR/", "Luxor Tech"], "link": "https://mining.luxor.tech" }, { @@ -57,10 +45,7 @@ "147SwRQdpCfj5p8PnfsXV2SsVVpVcz3aPq", "15vgygQ7ZsWdvZpctmTZK4673QBHsos6Sh" ], - "tags": [ - "/1THash&58COIN/", - "/1THash/" - ], + "tags": ["/1THash&58COIN/", "/1THash/"], "link": "https://www.1thash.top" }, { @@ -73,22 +58,14 @@ "3NA8hsjfdgVkmmVS9moHmkZsVCoLxUkvvv", "bc1qjl8uwezzlech723lpnyuza0h2cdkvxvh54v3dn" ], - "tags": [ - "/BTC.COM/", - "/BTC.com/", - "btccom" - ], + "tags": ["/BTC.COM/", "/BTC.com/", "btccom"], "link": "https://pool.btc.com" }, { "id": 7, "name": "Bitfarms", - "addresses": [ - "3GvEGtnvgeBJ3p3EpdZhvUkxY4pDARkbjd" - ], - "tags": [ - "BITFARMS" - ], + "addresses": ["3GvEGtnvgeBJ3p3EpdZhvUkxY4pDARkbjd"], + "tags": ["BITFARMS"], "link": "https://www.bitfarms.io" }, { @@ -100,100 +77,70 @@ "1MvYASoHjqynMaMnP7SBmenyEWiLsTqoU6", "3HuobiNg2wHjdPU2mQczL9on8WF7hZmaGd" ], - "tags": [ - "/HuoBi/", - "/Huobi/" - ], + "tags": ["/HuoBi/", "/Huobi/"], "link": "https://www.hpt.com" }, { "id": 9, "name": "WAYI.CN", "addresses": [], - "tags": [ - "/E2M & BTC.TOP/" - ], + "tags": ["/E2M & BTC.TOP/"], "link": "https://www.easy2mine.com" }, { "id": 10, "name": "CanoePool", - "addresses": [ - "1GP8eWArgpwRum76saJS4cZKCHWJHs9PQo" - ], - "tags": [ - "/CANOE/", - "/canoepool/" - ], + "addresses": ["1GP8eWArgpwRum76saJS4cZKCHWJHs9PQo"], + "tags": ["/CANOE/", "/canoepool/"], "link": "https://btc.canoepool.com" }, { "id": 11, "name": "BTC.TOP", - "addresses": [ - "1Hz96kJKF2HLPGY15JWLB5m9qGNxvt8tHJ" - ], - "tags": [ - "/BTC.TOP/" - ], + "addresses": ["1Hz96kJKF2HLPGY15JWLB5m9qGNxvt8tHJ"], + "tags": ["/BTC.TOP/"], "link": "https://btc.top" }, { "id": 12, "name": "Bitcoin.com", "addresses": [], - "tags": [ - "pool.bitcoin.com" - ], + "tags": ["pool.bitcoin.com"], "link": "https://www.bitcoin.com" }, { "id": 13, "name": "175btc", "addresses": [], - "tags": [ - "Mined By 175btc.com" - ], + "tags": ["Mined By 175btc.com"], "link": "https://www.175btc.com" }, { "id": 14, "name": "GBMiners", "addresses": [], - "tags": [ - "/mined by gbminers/" - ], + "tags": ["/mined by gbminers/"], "link": "https://gbminers.com" }, { "id": 15, "name": "A-XBT", - "addresses": [ - "1MFsp2txCPwMMBJjNNeKaduGGs8Wi1Ce7X" - ], - "tags": [ - "/A-XBT/" - ], + "addresses": ["1MFsp2txCPwMMBJjNNeKaduGGs8Wi1Ce7X"], + "tags": ["/A-XBT/"], "link": "https://www.a-xbt.com" }, { "id": 16, "name": "ASICMiner", "addresses": [], - "tags": [ - "ASICMiner" - ], + "tags": ["ASICMiner"], "link": "https://www.asicminer.co" }, { "id": 17, "name": "BitMinter", - "addresses": [ - "19PkHafEN18mquJ9ChwZt5YEFoCdPP5vYB" - ], - "tags": [ - "BitMinter" - ], + "addresses": ["19PkHafEN18mquJ9ChwZt5YEFoCdPP5vYB"], + "tags": ["BitMinter"], "link": "https://bitminter.com" }, { @@ -203,55 +150,42 @@ "14R2r9FkyDmyxGB9xUVwVLdgsX9YfdVamk", "165GCEAx81wce33FWEnPCRhdjcXCrBJdKn" ], - "tags": [ - "/Bitcoin-Russia.ru/" - ], + "tags": ["/Bitcoin-Russia.ru/"], "link": "https://bitcoin-russia.ru" }, { "id": 19, "name": "BTCServ", "addresses": [], - "tags": [ - "btcserv" - ], + "tags": ["btcserv"], "link": "https://btcserv.net" }, { "id": 20, "name": "simplecoin.us", "addresses": [], - "tags": [ - "simplecoin" - ], + "tags": ["simplecoin"], "link": "https://simplecoin.us" }, { "id": 21, "name": "BTC Guild", "addresses": [], - "tags": [ - "BTC Guild" - ], + "tags": ["BTC Guild"], "link": "https://www.btcguild.com" }, { "id": 22, "name": "Eligius", "addresses": [], - "tags": [ - "Eligius" - ], + "tags": ["Eligius"], "link": "https://eligius.st" }, { "id": 23, "name": "OzCoin", "addresses": [], - "tags": [ - "ozco.in", - "ozcoin" - ], + "tags": ["ozco.in", "ozcoin"], "link": "https://ozcoin.net" }, { @@ -261,85 +195,63 @@ "15xiShqUqerfjFdyfgBH1K7Gwp6cbYmsTW", "18M9o2mXNjNR96yKe7eyY6pfP6Nx4Nso3d" ], - "tags": [ - "EMC ", - "EMC:" - ], + "tags": ["EMC ", "EMC:"], "link": "https://eclipsemc.com" }, { "id": 25, "name": "MaxBTC", "addresses": [], - "tags": [ - "MaxBTC" - ], + "tags": ["MaxBTC"], "link": "https://maxbtc.com" }, { "id": 26, "name": "TripleMining", "addresses": [], - "tags": [ - "Triplemining.com", - "triplemining" - ], + "tags": ["Triplemining.com", "triplemining"], "link": "https://www.triplemining.com" }, { "id": 27, "name": "CoinLab", "addresses": [], - "tags": [ - "CoinLab" - ], + "tags": ["CoinLab"], "link": "https://coinlab.com" }, { "id": 28, "name": "50BTC", "addresses": [], - "tags": [ - "50BTC" - ], + "tags": ["50BTC"], "link": "https://www.50btc.com" }, { "id": 29, "name": "GHash.IO", - "addresses": [ - "1CjPR7Z5ZSyWk6WtXvSFgkptmpoi4UM9BC" - ], - "tags": [ - "ghash.io" - ], + "addresses": ["1CjPR7Z5ZSyWk6WtXvSFgkptmpoi4UM9BC"], + "tags": ["ghash.io"], "link": "https://ghash.io" }, { "id": 30, "name": "ST Mining Corp", "addresses": [], - "tags": [ - "st mining corp" - ], + "tags": ["st mining corp"], "link": "https://bitcointalk.org/index.php?topic=77000.msg3207708#msg3207708" }, { "id": 31, "name": "Bitparking", "addresses": [], - "tags": [ - "bitparking" - ], + "tags": ["bitparking"], "link": "https://mmpool.bitparking.com" }, { "id": 32, "name": "mmpool", "addresses": [], - "tags": [ - "mmpool" - ], + "tags": ["mmpool"], "link": "https://mmpool.org/pool" }, { @@ -353,30 +265,21 @@ "1JrYhdhP2jCY6JwuVzdk9jUwc4pctcSes7", "1Nsvmnv8VcTMD643xMYAo35Aco3XA5YPpe" ], - "tags": [ - "by polmine.pl", - "bypmneU" - ], + "tags": ["by polmine.pl", "bypmneU"], "link": "https://polmine.pl" }, { "id": 34, "name": "KnCMiner", "addresses": [], - "tags": [ - "KnCMiner" - ], + "tags": ["KnCMiner"], "link": "https://portal.kncminer.com/pool" }, { "id": 35, "name": "Bitalo", - "addresses": [ - "1HTejfsPZQGi3afCMEZTn2xdmoNzp13n3F" - ], - "tags": [ - "Bitalo" - ], + "addresses": ["1HTejfsPZQGi3afCMEZTn2xdmoNzp13n3F"], + "tags": ["Bitalo"], "link": "https://bitalo.com/mining" }, { @@ -386,67 +289,49 @@ "1KFHE7w8BhaENAswwryaoccDb6qcT6DbYY", "bc1qf274x7penhcd8hsv3jcmwa5xxzjl2a6pa9pxwm" ], - "tags": [ - "七彩神仙鱼", - "F2Pool", - "🐟" - ], + "tags": ["七彩神仙鱼", "F2Pool", "🐟"], "link": "https://www.f2pool.com" }, { "id": 37, "name": "HHTT", "addresses": [], - "tags": [ - "HHTT" - ], + "tags": ["HHTT"], "link": "https://hhtt.1209k.com" }, { "id": 38, "name": "MegaBigPower", - "addresses": [ - "1K7znxRfkS8R1hcmyMvHDum1hAQreS4VQ4" - ], - "tags": [ - "megabigpower.com" - ], + "addresses": ["1K7znxRfkS8R1hcmyMvHDum1hAQreS4VQ4"], + "tags": ["megabigpower.com"], "link": "https://megabigpower.com" }, { "id": 39, "name": "Mt Red", "addresses": [], - "tags": [ - "/mtred/" - ], + "tags": ["/mtred/"], "link": "https://mtred.com" }, { "id": 40, "name": "NMCbit", "addresses": [], - "tags": [ - "nmcbit.com" - ], + "tags": ["nmcbit.com"], "link": "https://nmcbit.com" }, { "id": 41, "name": "Yourbtc.net", "addresses": [], - "tags": [ - "yourbtc.net" - ], + "tags": ["yourbtc.net"], "link": "https://yourbtc.net" }, { "id": 42, "name": "Give Me Coins", "addresses": [], - "tags": [ - "Give-Me-Coins" - ], + "tags": ["Give-Me-Coins"], "link": "https://give-me-coins.com" }, { @@ -456,9 +341,7 @@ "1AqTMY7kmHZxBuLUR5wJjPFUvqGs23sesr", "1CK6KHY6MHgYvmRQ4PAafKYDrg1ejbH1cE" ], - "tags": [ - "/slush/" - ], + "tags": ["/slush/"], "link": "https://braiins.com/pool" }, { @@ -503,144 +386,98 @@ "1jLVpwtNMfXWaHY4eiLDmGuBxokYLgv1X", "3FaYVQF6wCMUB9NCeRe4tUp1zZx8qqM7H1" ], - "tags": [ - "/AntPool/", - "Mined By AntPool", - "Mined by AntPool" - ], + "tags": ["/AntPool/", "Mined By AntPool", "Mined by AntPool"], "link": "https://www.antpool.com" }, { "id": 45, "name": "MultiCoin.co", "addresses": [], - "tags": [ - "Mined by MultiCoin.co" - ], + "tags": ["Mined by MultiCoin.co"], "link": "https://multicoin.co" }, { "id": 46, "name": "bcpool.io", "addresses": [], - "tags": [ - "bcpool.io" - ], + "tags": ["bcpool.io"], "link": "https://bcpool.io" }, { "id": 47, "name": "Cointerra", - "addresses": [ - "1BX5YoLwvqzvVwSrdD4dC32vbouHQn2tuF" - ], - "tags": [ - "cointerra" - ], + "addresses": ["1BX5YoLwvqzvVwSrdD4dC32vbouHQn2tuF"], + "tags": ["cointerra"], "link": "https://cointerra.com" }, { "id": 48, "name": "KanoPool", "addresses": [], - "tags": [ - "Kano" - ], + "tags": ["Kano"], "link": "https://kano.is" }, { "id": 49, "name": "Solo CK", "addresses": [], - "tags": [ - "/solo.ckpool.org/" - ], + "tags": ["/solo.ckpool.org/"], "link": "https://solo.ckpool.org" }, { "id": 50, "name": "CKPool", "addresses": [], - "tags": [ - "/ckpool.org/" - ], + "tags": ["/ckpool.org/"], "link": "https://ckpool.org" }, { "id": 51, "name": "NiceHash", "addresses": [], - "tags": [ - "/NiceHashSolo", - "/NiceHash/", - "/NiceHashMining/" - ], + "tags": ["/NiceHashSolo", "/NiceHash/", "/NiceHashMining/"], "link": "https://www.nicehash.com" }, { "id": 52, "name": "BitClub", - "addresses": [ - "155fzsEBHy9Ri2bMQ8uuuR3tv1YzcDywd4" - ], - "tags": [ - "/BitClub Network/" - ], + "addresses": ["155fzsEBHy9Ri2bMQ8uuuR3tv1YzcDywd4"], + "tags": ["/BitClub Network/"], "link": "https://bitclubpool.com" }, { "id": 53, "name": "Bitcoin Affiliate Network", "addresses": [], - "tags": [ - "bitcoinaffiliatenetwork.com" - ], + "tags": ["bitcoinaffiliatenetwork.com"], "link": "https://mining.bitcoinaffiliatenetwork.com" }, { "id": 54, "name": "BTCC", - "addresses": [ - "152f1muMCNa7goXYhYAQC61hxEgGacmncB" - ], - "tags": [ - "/BTCC/", - "BTCChina Pool", - "BTCChina.com", - "btcchina.com" - ], + "addresses": ["152f1muMCNa7goXYhYAQC61hxEgGacmncB"], + "tags": ["/BTCC/", "BTCChina Pool", "BTCChina.com", "btcchina.com"], "link": "https://pool.btcc.com" }, { "id": 55, "name": "BWPool", - "addresses": [ - "1JLRXD8rjRgQtTS9MvfQALfHgGWau9L9ky" - ], - "tags": [ - "BW Pool", - "BWPool" - ], + "addresses": ["1JLRXD8rjRgQtTS9MvfQALfHgGWau9L9ky"], + "tags": ["BW Pool", "BWPool"], "link": "https://bwpool.net" }, { "id": 56, "name": "EXX&BW", "addresses": [], - "tags": [ - "xbtc.exx.com&bw.com" - ], + "tags": ["xbtc.exx.com&bw.com"], "link": "https://xbtc.exx.com" }, { "id": 57, "name": "Bitsolo", - "addresses": [ - "18zRehBcA2YkYvsC7dfQiFJNyjmWvXsvon" - ], - "tags": [ - "Bitsolo Pool" - ], + "addresses": ["18zRehBcA2YkYvsC7dfQiFJNyjmWvXsvon"], + "tags": ["Bitsolo Pool"], "link": "https://bitsolo.net" }, { @@ -650,10 +487,7 @@ "14yfxkcpHnju97pecpM7fjuTkVdtbkcfE6", "1AcAj9p6zJn4xLXdvmdiuPCtY7YkBPTAJo" ], - "tags": [ - "/BitFury/", - "/Bitfury/" - ], + "tags": ["/BitFury/", "/Bitfury/"], "link": "https://bitfury.com" }, { @@ -664,124 +498,84 @@ "1CdJi2xRTXJF6CEJqNHYyQDNEcM3X7fUhD", "1GC6HxDvnchDdb5cGkFXsJMZBFRsKAXfwi" ], - "tags": [ - "/pool34/" - ], + "tags": ["/pool34/"], "link": "https://21.co" }, { "id": 60, "name": "digitalBTC", - "addresses": [ - "1MimPd6LrPKGftPRHWdfk8S3KYBfN4ELnD" - ], - "tags": [ - "/agentD/" - ], + "addresses": ["1MimPd6LrPKGftPRHWdfk8S3KYBfN4ELnD"], + "tags": ["/agentD/"], "link": "https://digitalbtc.com" }, { "id": 61, "name": "8baochi", - "addresses": [ - "1Hk9gD8xMo2XBUhE73y5zXEM8xqgffTB5f" - ], - "tags": [ - "/八宝池 8baochi.com/" - ], + "addresses": ["1Hk9gD8xMo2XBUhE73y5zXEM8xqgffTB5f"], + "tags": ["/八宝池 8baochi.com/"], "link": "https://8baochi.com" }, { "id": 62, "name": "myBTCcoin Pool", - "addresses": [ - "151T7r1MhizzJV6dskzzUkUdr7V8JxV2Dx" - ], - "tags": [ - "myBTCcoin Pool" - ], + "addresses": ["151T7r1MhizzJV6dskzzUkUdr7V8JxV2Dx"], + "tags": ["myBTCcoin Pool"], "link": "https://mybtccoin.com" }, { "id": 63, "name": "TBDice", - "addresses": [ - "1BUiW44WuJ2jiJgXiyxJVFMN8bc1GLdXRk" - ], - "tags": [ - "TBDice" - ], + "addresses": ["1BUiW44WuJ2jiJgXiyxJVFMN8bc1GLdXRk"], + "tags": ["TBDice"], "link": "https://tbdice.org" }, { "id": 64, "name": "HASHPOOL", "addresses": [], - "tags": [ - "HASHPOOL" - ], + "tags": ["HASHPOOL"], "link": "https://hashpool.com" }, { "id": 65, "name": "Nexious", - "addresses": [ - "1GBo1f2tzVx5jScV9kJXPUP9RjvYXuNzV7" - ], - "tags": [ - "/Nexious/" - ], + "addresses": ["1GBo1f2tzVx5jScV9kJXPUP9RjvYXuNzV7"], + "tags": ["/Nexious/"], "link": "https://nexious.com" }, { "id": 66, "name": "Bravo Mining", "addresses": [], - "tags": [ - "/bravo-mining/" - ], + "tags": ["/bravo-mining/"], "link": "https://www.bravo-mining.com" }, { "id": 67, "name": "HotPool", - "addresses": [ - "17judvK4AC2M6KhaBbAEGw8CTKc9Pg8wup" - ], - "tags": [ - "/HotPool/" - ], + "addresses": ["17judvK4AC2M6KhaBbAEGw8CTKc9Pg8wup"], + "tags": ["/HotPool/"], "link": "https://hotpool.co" }, { "id": 68, "name": "OKExPool", "addresses": [], - "tags": [ - "/www.okex.com/" - ], + "tags": ["/www.okex.com/"], "link": "https://www.okex.com" }, { "id": 69, "name": "BCMonster", - "addresses": [ - "1E18BNyobcoiejcDYAz5SjbrzifNDEpM88" - ], - "tags": [ - "/BCMonster/" - ], + "addresses": ["1E18BNyobcoiejcDYAz5SjbrzifNDEpM88"], + "tags": ["/BCMonster/"], "link": "https://www.bcmonster.com" }, { "id": 70, "name": "1Hash", - "addresses": [ - "1F1xcRt8H8Wa623KqmkEontwAAVqDSAWCV" - ], - "tags": [ - "Mined by 1hash.com" - ], + "addresses": ["1F1xcRt8H8Wa623KqmkEontwAAVqDSAWCV"], + "tags": ["Mined by 1hash.com"], "link": "https://www.1hash.com" }, { @@ -791,110 +585,77 @@ "13hQVEstgo4iPQZv9C7VELnLWF7UWtF4Q3", "1KsFhYKLs8qb1GHqrPxHoywNQpet2CtP9t" ], - "tags": [ - "/Bixin/", - "/HaoBTC/", - "HAOBTC" - ], + "tags": ["/Bixin/", "/HaoBTC/", "HAOBTC"], "link": "https://haopool.com" }, { "id": 72, "name": "TATMAS Pool", "addresses": [], - "tags": [ - "/ViaBTC/TATMAS Pool/" - ], + "tags": ["/ViaBTC/TATMAS Pool/"], "link": "https://tmsminer.com" }, { "id": 73, "name": "ViaBTC", "addresses": [], - "tags": [ - "/ViaBTC/", - "viabtc.com deploy" - ], + "tags": ["/ViaBTC/", "viabtc.com deploy"], "link": "https://viabtc.com" }, { "id": 74, "name": "ConnectBTC", - "addresses": [ - "1KPQkehgYAqwiC6UCcbojM3mbGjURrQJF2" - ], - "tags": [ - "/ConnectBTC - Home for Miners/" - ], + "addresses": ["1KPQkehgYAqwiC6UCcbojM3mbGjURrQJF2"], + "tags": ["/ConnectBTC - Home for Miners/"], "link": "https://www.connectbtc.com" }, { "id": 75, "name": "BATPOOL", - "addresses": [ - "167ApWWxUSFQmz2jdz9xop3oAKdLejvMML" - ], - "tags": [ - "/BATPOOL/" - ], + "addresses": ["167ApWWxUSFQmz2jdz9xop3oAKdLejvMML"], + "tags": ["/BATPOOL/"], "link": "https://www.batpool.com" }, { "id": 76, "name": "Waterhole", - "addresses": [ - "1FLH1SoLv4U68yUERhDiWzrJn5TggMqkaZ" - ], - "tags": [ - "/WATERHOLE.IO/" - ], + "addresses": ["1FLH1SoLv4U68yUERhDiWzrJn5TggMqkaZ"], + "tags": ["/WATERHOLE.IO/"], "link": "https://btc.waterhole.io" }, { "id": 77, "name": "DCExploration", "addresses": [], - "tags": [ - "/DCExploration/" - ], + "tags": ["/DCExploration/"], "link": "https://dcexploration.cn" }, { "id": 78, "name": "DCEX", "addresses": [], - "tags": [ - "/DCEX/" - ], + "tags": ["/DCEX/"], "link": "https://dcexploration.cn" }, { "id": 79, "name": "BTPOOL", "addresses": [], - "tags": [ - "/BTPOOL/" - ], + "tags": ["/BTPOOL/"], "link": "" }, { "id": 80, "name": "58COIN", - "addresses": [ - "199EDJoCpqV672qESEkfFgEqNT1iR2gj3t" - ], - "tags": [ - "/58coin.com/" - ], + "addresses": ["199EDJoCpqV672qESEkfFgEqNT1iR2gj3t"], + "tags": ["/58coin.com/"], "link": "https://www.58coin.com" }, { "id": 81, "name": "Bitcoin India", "addresses": [], - "tags": [ - "/Bitcoin-India/" - ], + "tags": ["/Bitcoin-India/"], "link": "https://bitcoin-india.org" }, { @@ -904,52 +665,35 @@ "12znnESiJ3bgCLftwwrg9wzQKN8fJtoBDa", "18HEMWFXM9UGPVZHUMdBPD3CMFWYn2NPRX" ], - "tags": [ - "--Nug--" - ], + "tags": ["--Nug--"], "link": "https://www.brainofshawn.com" }, { "id": 83, "name": "PHash.IO", "addresses": [], - "tags": [ - "/phash.cn/", - "/phash.io/" - ], + "tags": ["/phash.cn/", "/phash.io/"], "link": "https://phash.io" }, { "id": 84, "name": "RigPool", - "addresses": [ - "1JpKmtspBJQVXK67DJP64eBJcAPhDvJ9Er" - ], - "tags": [ - "/RigPool.com/" - ], + "addresses": ["1JpKmtspBJQVXK67DJP64eBJcAPhDvJ9Er"], + "tags": ["/RigPool.com/"], "link": "https://www.rigpool.com" }, { "id": 85, "name": "HAOZHUZHU", - "addresses": [ - "19qa95rTbDziNCS9EexUbh2hVY4viUU9tt" - ], - "tags": [ - "/haozhuzhu/" - ], + "addresses": ["19qa95rTbDziNCS9EexUbh2hVY4viUU9tt"], + "tags": ["/haozhuzhu/"], "link": "https://haozhuzhu.com" }, { "id": 86, "name": "7pool", - "addresses": [ - "1JLc3JxvpdL1g5zoX8sKLP4BkJQiwnJftU" - ], - "tags": [ - "/$Mined by 7pool.com/" - ], + "addresses": ["1JLc3JxvpdL1g5zoX8sKLP4BkJQiwnJftU"], + "tags": ["/$Mined by 7pool.com/"], "link": "https://7pool.com" }, { @@ -960,29 +704,21 @@ "1EowSPumj9D9AMTpE64Jr7vT3PJDNopVcz", "1KGbsDDAgJN2HDNBjmMHp9828qATo5B9c9" ], - "tags": [ - "/mined by poopbut/" - ], + "tags": ["/mined by poopbut/"], "link": "https://miningkings.com" }, { "id": 88, "name": "HashBX", "addresses": [], - "tags": [ - "/Mined by HashBX.io/" - ], + "tags": ["/Mined by HashBX.io/"], "link": "https://hashbx.io" }, { "id": 89, "name": "DPOOL", - "addresses": [ - "1ACAgPuFFidYzPMXbiKptSrwT74Dg8hq2v" - ], - "tags": [ - "/DPOOL.TOP/" - ], + "addresses": ["1ACAgPuFFidYzPMXbiKptSrwT74Dg8hq2v"], + "tags": ["/DPOOL.TOP/"], "link": "https://www.dpool.top" }, { @@ -998,36 +734,28 @@ "bc1qru8mtv3e3u7ms6ecjmwgeakdakclemvhnw00q9", "bc1qwlrsvgtn99rqp3fgaxq6f6jkgms80rnej0a8tc" ], - "tags": [ - "/Rawpool.com/" - ], + "tags": ["/Rawpool.com/"], "link": "https://www.rawpool.com" }, { "id": 91, "name": "haominer", "addresses": [], - "tags": [ - "/haominer/" - ], + "tags": ["/haominer/"], "link": "https://haominer.com" }, { "id": 92, "name": "Helix", "addresses": [], - "tags": [ - "/Helix/" - ], + "tags": ["/Helix/"], "link": "" }, { "id": 93, "name": "Bitcoin-Ukraine", "addresses": [], - "tags": [ - "/Bitcoin-Ukraine.com.ua/" - ], + "tags": ["/Bitcoin-Ukraine.com.ua/"], "link": "https://bitcoin-ukraine.com.ua" }, { @@ -1042,80 +770,56 @@ "3JQSigWTCHyBLRD979JWgEtWP5YiiFwcQB", "3KJrsjfg1dD6CrsTeHdHVH3KqMpvL2XWQn" ], - "tags": [ - "/poolin.com", - "/poolin/" - ], + "tags": ["/poolin.com", "/poolin/"], "link": "https://www.poolin.com" }, { "id": 95, "name": "SecretSuperstar", "addresses": [], - "tags": [ - "/SecretSuperstar/" - ], + "tags": ["/SecretSuperstar/"], "link": "" }, { "id": 96, "name": "tigerpool.net", "addresses": [], - "tags": [ - "/tigerpool.net" - ], + "tags": ["/tigerpool.net"], "link": "" }, { "id": 97, "name": "Sigmapool.com", - "addresses": [ - "12cKiMNhCtBhZRUBCnYXo8A4WQzMUtYjmR" - ], - "tags": [ - "/Sigmapool.com/" - ], + "addresses": ["12cKiMNhCtBhZRUBCnYXo8A4WQzMUtYjmR"], + "tags": ["/Sigmapool.com/"], "link": "https://sigmapool.com" }, { "id": 98, "name": "okpool.top", "addresses": [], - "tags": [ - "/www.okpool.top/" - ], + "tags": ["/www.okpool.top/"], "link": "https://www.okpool.top" }, { "id": 99, "name": "Hummerpool", "addresses": [], - "tags": [ - "HummerPool", - "Hummerpool" - ], + "tags": ["HummerPool", "Hummerpool"], "link": "https://www.hummerpool.com" }, { "id": 100, "name": "Tangpool", - "addresses": [ - "12Taz8FFXQ3E2AGn3ZW1SZM5bLnYGX4xR6" - ], - "tags": [ - "/Tangpool/" - ], + "addresses": ["12Taz8FFXQ3E2AGn3ZW1SZM5bLnYGX4xR6"], + "tags": ["/Tangpool/"], "link": "https://www.tangpool.com" }, { "id": 101, "name": "BytePool", - "addresses": [ - "39m5Wvn9ZqyhYmCYpsyHuGMt5YYw4Vmh1Z" - ], - "tags": [ - "/bytepool.com/" - ], + "addresses": ["39m5Wvn9ZqyhYmCYpsyHuGMt5YYw4Vmh1Z"], + "tags": ["/bytepool.com/"], "link": "https://www.bytepool.com" }, { @@ -1126,31 +830,21 @@ "38u1srayb1oybVB43UWKBJsrwJbdHGtPx2", "1BM1sAcrfV6d4zPKytzziu4McLQDsFC2Qc" ], - "tags": [ - "SpiderPool" - ], + "tags": ["SpiderPool"], "link": "https://www.spiderpool.com" }, { "id": 103, "name": "NovaBlock", - "addresses": [ - "3Bmb9Jig8A5kHdDSxvDZ6eryj3AXd3swuJ" - ], - "tags": [ - "/NovaBlock/" - ], + "addresses": ["3Bmb9Jig8A5kHdDSxvDZ6eryj3AXd3swuJ"], + "tags": ["/NovaBlock/"], "link": "https://novablock.com" }, { "id": 104, "name": "MiningCity", - "addresses": [ - "11wC5KcbgrWRBb43cwADdVrxgyF8mndVC" - ], - "tags": [ - "MiningCity" - ], + "addresses": ["11wC5KcbgrWRBb43cwADdVrxgyF8mndVC"], + "tags": ["MiningCity"], "link": "https://www.miningcity.com" }, { @@ -1165,68 +859,42 @@ "bc1qx9t2l3pyny2spqpqlye8svce70nppwtaxwdrp4", "3G7jcEELKh38L6kaSV8K35pTqsh5bgZW2D" ], - "tags": [ - "/Binance/", - "binance" - ], + "tags": ["/Binance/", "binance"], "link": "https://pool.binance.com" }, { "id": 106, "name": "Minerium", "addresses": [], - "tags": [ - "/Mined in the USA by: /Minerium.com/", - "/Minerium.com/" - ], + "tags": ["/Mined in the USA by: /Minerium.com/", "/Minerium.com/"], "link": "https://www.minerium.com" }, { "id": 107, "name": "Lubian.com", - "addresses": [ - "34Jpa4Eu3ApoPVUKNTN2WeuXVVq1jzxgPi" - ], - "tags": [ - "/Buffett/", - "/lubian.com/" - ], + "addresses": ["34Jpa4Eu3ApoPVUKNTN2WeuXVVq1jzxgPi"], + "tags": ["/Buffett/", "/lubian.com/"], "link": "https://www.lubian.com" }, { "id": 108, "name": "OKKONG", - "addresses": [ - "16JHXJ7M2MubWNX9grnqbjUqJ5PHwcCWw2" - ], - "tags": [ - "/hash.okkong.com/" - ], + "addresses": ["16JHXJ7M2MubWNX9grnqbjUqJ5PHwcCWw2"], + "tags": ["/hash.okkong.com/"], "link": "https://hash.okkong.com" }, { "id": 109, "name": "AAO Pool", - "addresses": [ - "12QVFmJH2b4455YUHkMpEnWLeRY3eJ4Jb5" - ], - "tags": [ - "/AAOPOOL/" - ], + "addresses": ["12QVFmJH2b4455YUHkMpEnWLeRY3eJ4Jb5"], + "tags": ["/AAOPOOL/"], "link": "https://btc.tmspool.top" }, { "id": 110, "name": "EMCDPool", - "addresses": [ - "1BDbsWi3Mrcjp1wdop3PWFNCNZtu4R7Hjy" - ], - "tags": [ - "/EMCD/", - "/one_more_mcd/", - "get___emcd", - "emcd" - ], + "addresses": ["1BDbsWi3Mrcjp1wdop3PWFNCNZtu4R7Hjy"], + "tags": ["/EMCD/", "/one_more_mcd/", "get___emcd", "emcd"], "link": "https://pool.emcd.io" }, { @@ -1238,41 +906,28 @@ "bc1qxhmdufsvnuaaaer4ynz88fspdsxq2h9e9cetdj", "bc1p8k4v4xuz55dv49svzjg43qjxq2whur7ync9tm0xgl5t4wjl9ca9snxgmlt" ], - "tags": [ - "/2cDw/", - "Foundry USA Pool" - ], + "tags": ["/2cDw/", "Foundry USA Pool"], "link": "https://foundrydigital.com" }, { "id": 112, "name": "SBI Crypto", "addresses": [], - "tags": [ - "/SBICrypto.com Pool/", - "SBI Crypto", - "SBICrypto" - ], + "tags": ["/SBICrypto.com Pool/", "SBI Crypto", "SBICrypto"], "link": "https://sbicrypto.com" }, { "id": 113, "name": "ArkPool", - "addresses": [ - "1QEiAhdHdMhBgVbDM7zUXWGkNhgEEJ6uLd" - ], - "tags": [ - "/ArkPool/" - ], + "addresses": ["1QEiAhdHdMhBgVbDM7zUXWGkNhgEEJ6uLd"], + "tags": ["/ArkPool/"], "link": "https://www.arkpool.com" }, { "id": 114, "name": "PureBTC.COM", "addresses": [], - "tags": [ - "/PureBTC.COM/" - ], + "tags": ["/PureBTC.COM/"], "link": "https://purebtc.com" }, { @@ -1282,89 +937,62 @@ "15MdAHnkxt9TMC2Rj595hsg8Hnv693pPBB", "1A32KFEX7JNPmU1PVjrtiXRrTQcesT3Nf1" ], - "tags": [ - "MARA Pool", - "MARA Made in USA" - ], + "tags": ["MARA Pool", "MARA Made in USA"], "link": "https://marapool.com" }, { "id": 116, "name": "KuCoinPool", - "addresses": [ - "1ArTPjj6pV3aNRhLPjJVPYoxB98VLBzUmb" - ], - "tags": [ - "KuCoinPool" - ], + "addresses": ["1ArTPjj6pV3aNRhLPjJVPYoxB98VLBzUmb"], + "tags": ["KuCoinPool"], "link": "https://www.kucoin.com/mining-pool" }, { "id": 117, "name": "Entrust Charity Pool", "addresses": [], - "tags": [ - "Entrustus" - ], + "tags": ["Entrustus"], "link": "pool.entustus.org" }, { "id": 118, "name": "OKMINER", - "addresses": [ - "15xcAZ2HfaSwYbCV6GGbasBSAekBRRC5Q2" - ], - "tags": [ - "okminer.com/euz" - ], + "addresses": ["15xcAZ2HfaSwYbCV6GGbasBSAekBRRC5Q2"], + "tags": ["okminer.com/euz"], "link": "https://okminer.com" }, { "id": 119, "name": "Titan", - "addresses": [ - "14hLEtxozmmih6Gg5xrGZLfx51bEMj21NW" - ], - "tags": [ - "Titan.io" - ], + "addresses": ["14hLEtxozmmih6Gg5xrGZLfx51bEMj21NW"], + "tags": ["Titan.io"], "link": "https://titan.io" }, { "id": 120, "name": "PEGA Pool", - "addresses": [ - "1BGFwRzjCfRR7EvRHnzfHyFjGR8XiBDFKa" - ], - "tags": [ - "/pegapool/" - ], + "addresses": ["1BGFwRzjCfRR7EvRHnzfHyFjGR8XiBDFKa"], + "tags": ["/pegapool/"], "link": "https://www.pega-pool.com" }, { "id": 121, "name": "BTC Nuggets", - "addresses": [ - "1BwZeHJo7b7M2op7VDfYnsmcpXsUYEcVHm" - ], + "addresses": ["1BwZeHJo7b7M2op7VDfYnsmcpXsUYEcVHm"], "tags": [], "link": "https://104.197.8.250" }, { "id": 122, "name": "CloudHashing", - "addresses": [ - "1ALA5v7h49QT7WYLcRsxcXqXUqEqaWmkvw" - ], + "addresses": ["1ALA5v7h49QT7WYLcRsxcXqXUqEqaWmkvw"], "tags": [], "link": "https://cloudhashing.com" }, { "id": 123, "name": "digitalX Mintsy", - "addresses": [ - "1NY15MK947MLzmPUa2gL7UgyR8prLh2xfu" - ], + "addresses": ["1NY15MK947MLzmPUa2gL7UgyR8prLh2xfu"], "tags": [], "link": "https://www.mintsy.co" }, @@ -1391,54 +1019,42 @@ { "id": 125, "name": "BTC Pool Party", - "addresses": [ - "1PmRrdp1YSkp1LxPyCfcmBHDEipG5X4eJB" - ], + "addresses": ["1PmRrdp1YSkp1LxPyCfcmBHDEipG5X4eJB"], "tags": [], "link": "https://btcpoolparty.com" }, { "id": 126, "name": "Multipool", - "addresses": [ - "1MeffGLauEj2CZ18hRQqUauTXb9JAuLbGw" - ], + "addresses": ["1MeffGLauEj2CZ18hRQqUauTXb9JAuLbGw"], "tags": [], "link": "https://www.multipool.us" }, { "id": 127, "name": "transactioncoinmining", - "addresses": [ - "1qtKetXKgqa7j1KrB19HbvfRiNUncmakk" - ], + "addresses": ["1qtKetXKgqa7j1KrB19HbvfRiNUncmakk"], "tags": [], "link": "https://sha256.transactioncoinmining.com" }, { "id": 128, "name": "BTCDig", - "addresses": [ - "15MxzsutVroEE9XiDckLxUHTCDAEZgPZJi" - ], + "addresses": ["15MxzsutVroEE9XiDckLxUHTCDAEZgPZJi"], "tags": [], "link": "https://btcdig.com" }, { "id": 129, "name": "Tricky's BTC Pool", - "addresses": [ - "1AePMyovoijxvHuKhTqWvpaAkRCF4QswC6" - ], + "addresses": ["1AePMyovoijxvHuKhTqWvpaAkRCF4QswC6"], "tags": [], "link": "https://pool.wemine.uk" }, { "id": 130, "name": "BTCMP", - "addresses": [ - "1jKSjMLnDNup6NPgCjveeP9tUn4YpT94Y" - ], + "addresses": ["1jKSjMLnDNup6NPgCjveeP9tUn4YpT94Y"], "tags": [], "link": "https://www.btcmp.com" }, @@ -1455,9 +1071,7 @@ { "id": 132, "name": "UNOMP", - "addresses": [ - "1BRY8AD7vSNUEE75NjzfgiG18mWjGQSRuJ" - ], + "addresses": ["1BRY8AD7vSNUEE75NjzfgiG18mWjGQSRuJ"], "tags": [], "link": "https://199.115.116.7:8925" }, @@ -1474,100 +1088,71 @@ { "id": 134, "name": "GoGreenLight", - "addresses": [ - "18EPLvrs2UE11kWBB3ABS7Crwj5tTBYPoa" - ], + "addresses": ["18EPLvrs2UE11kWBB3ABS7Crwj5tTBYPoa"], "tags": [], "link": "https://www.gogreenlight.se" }, { "id": 135, "name": "BitcoinIndia", - "addresses": [ - "1AZ6BkCo4zgTuuLpRStJH8iNsehXTMp456" - ], + "addresses": ["1AZ6BkCo4zgTuuLpRStJH8iNsehXTMp456"], "tags": [], "link": "https://pool.bitcoin-india.org" }, { "id": 136, "name": "EkanemBTC", - "addresses": [ - "1Cs5RT9SRk1hxsdzivAfkjesNmVVJqfqkw" - ], + "addresses": ["1Cs5RT9SRk1hxsdzivAfkjesNmVVJqfqkw"], "tags": [], "link": "https://ekanembtc.com" }, { "id": 137, "name": "CANOE", - "addresses": [ - "1Afcpc2FpPnREU6i52K3cicmHdvYRAH9Wo" - ], + "addresses": ["1Afcpc2FpPnREU6i52K3cicmHdvYRAH9Wo"], "tags": [], "link": "https://www.canoepool.com" }, { "id": 138, "name": "tiger", - "addresses": [ - "1LsFmhnne74EmU4q4aobfxfrWY4wfMVd8w" - ], + "addresses": ["1LsFmhnne74EmU4q4aobfxfrWY4wfMVd8w"], "tags": [], "link": "" }, { "id": 139, "name": "1M1X", - "addresses": [ - "1M1Xw2rczxkF3p3wiNHaTmxvbpZZ7M6vaa" - ], + "addresses": ["1M1Xw2rczxkF3p3wiNHaTmxvbpZZ7M6vaa"], "tags": [], "link": "" }, { "id": 140, "name": "Zulupool", - "addresses": [ - "1ZULUPooLEQfkrTgynLV4uHyMgQYx71ip" - ], - "tags": [ - "ZULUPooL", - "ZU_test" - ], + "addresses": ["1ZULUPooLEQfkrTgynLV4uHyMgQYx71ip"], + "tags": ["ZULUPooL", "ZU_test"], "link": "https://beta.zulupool.com/" }, { "id": 141, "name": "SECPOOL", - "addresses": [ - "3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9" - ], - "tags": [ - "SecPool" - ], + "addresses": ["3Awm3FNpmwrbvAFVThRUFqgpbVuqWisni9"], + "tags": ["SecPool"], "link": "https://www.secpool.com" }, { "id": 142, "name": "OCEAN", - "addresses": [ - "37dvwZZoT3D7RXpTCpN2yKzMmNs2i2Fd1n" - ], - "tags": [ - "OCEAN.XYZ" - ], + "addresses": ["37dvwZZoT3D7RXpTCpN2yKzMmNs2i2Fd1n"], + "tags": ["OCEAN.XYZ"], "link": "https://ocean.xyz/" }, { "id": 143, "name": "WhitePool", - "addresses": [ - "14VkxDwSAUWrzYTxV49HnYhKLWTJ3pCoUS" - ], - "tags": [ - "WhitePool" - ], + "addresses": ["14VkxDwSAUWrzYTxV49HnYhKLWTJ3pCoUS"], + "tags": ["WhitePool"], "link": "https://whitebit.com/mining-pool" }, { @@ -1577,74 +1162,49 @@ "1wizSAYSbuyXbt9d8JV8ytm5acqq2TorC", "tb1q548z58kqvwyjqwy8vc2ntmg33d7s2wyfv7ukq4" ], - "tags": [ - "/@wiz/" - ], + "tags": ["/@wiz/"], "link": "https://wiz.biz/" }, { "id": 145, "name": "mononaut", - "addresses": [ - "mjP97q5BWtdpdsJLkEJvQWgLe9zw4MMVU6" - ], - "tags": [ - "🐵🚀" - ], + "addresses": ["mjP97q5BWtdpdsJLkEJvQWgLe9zw4MMVU6"], + "tags": ["🐵🚀"], "link": "https://twitter.com/mononautical" }, { "id": 146, "name": "rijndael", - "addresses": [ - "tb1qg8zlznrvns9u46muxamxjh7sa8wry3vutzaujm" - ], - "tags": [ - "rijndael's toaster" - ], + "addresses": ["tb1qg8zlznrvns9u46muxamxjh7sa8wry3vutzaujm"], + "tags": ["rijndael's toaster"], "link": "https://twitter.com/rot13maxi" }, { "id": 147, "name": "wk057", - "addresses": [ - "1WizkidqARMLvjGUpfDQFRcEbnHpL55kK" - ], - "tags": [ - "wizkid057's block" - ], + "addresses": ["1WizkidqARMLvjGUpfDQFRcEbnHpL55kK"], + "tags": ["wizkid057's block"], "link": "" }, { "id": 148, "name": "FutureBit Apollo Solo", "addresses": [], - "tags": [ - "Apollo", - "/mined by a Solo FutureBit Apollo/" - ], + "tags": ["Apollo", "FutureBit", "/mined by a Solo FutureBit Apollo/"], "link": "https://www.futurebit.io" }, { "id": 149, "name": "emzy", - "addresses": [ - "tb1qmf7xdqc5nvzhturuzc46qtq5kywdf3p76cpq53" - ], - "tags": [ - "Emzy was here." - ], + "addresses": ["tb1qmf7xdqc5nvzhturuzc46qtq5kywdf3p76cpq53"], + "tags": ["Emzy was here."], "link": "https://twitter.com/emzy" }, { "id": 150, "name": "knorrium", - "addresses": [ - "tb1qtfqp4g7n7wc3sr6c2cuzsq62px4pfsxgsv2krx" - ], - "tags": [ - "knorrium" - ], + "addresses": ["tb1qtfqp4g7n7wc3sr6c2cuzsq62px4pfsxgsv2krx"], + "tags": ["knorrium"], "link": "https://twitter.com/knorrium" }, { @@ -1660,8 +1220,7 @@ { "id": 152, "name": "Portland.HODL", - "addresses": [ - ], + "addresses": [], "tags": ["Portland.HODL"], "link": "" }, @@ -1679,56 +1238,42 @@ { "id": 154, "name": "Neopool", - "addresses": [ - "1HCAb2h89bUinm6QZrAPpfbk4ySBrT2V4w" - ], + "addresses": ["1HCAb2h89bUinm6QZrAPpfbk4ySBrT2V4w"], "tags": ["/Neopool/"], "link": "https://neopool.com/" }, { "id": 155, "name": "MaxiPool", - "addresses": [ - "36r3YqAXWpyqNcczjCBdHrYZ3m8X56WDzx" - ], + "addresses": ["36r3YqAXWpyqNcczjCBdHrYZ3m8X56WDzx"], "tags": ["/MaxiPool/"], "link": "https://maxipool.org/" }, { "id": 156, "name": "DrDetroit", - "addresses": [ - "tb1qtcruplnz89xw5f86kw8sj7x9r23d5yffrysx2p" - ], - "tags": [ - "DrDetroit" - ], + "addresses": ["tb1qtcruplnz89xw5f86kw8sj7x9r23d5yffrysx2p"], + "tags": ["DrDetroit"], "link": "https://x.com/bankhatin" }, { "id": 157, "name": "BitFuFuPool", - "addresses": [ - "3JP3zF7LoeoAotqkNGdvX5szUyNPwd937d" - ], + "addresses": ["3JP3zF7LoeoAotqkNGdvX5szUyNPwd937d"], "tags": ["/BitFuFu/"], "link": "https://www.bitfufu.com/pool" }, { "id": 158, "name": "GDPool", - "addresses": [ - "1DnPPFQPrfyNTiHPXhDFyqNnW9T62GEhB1" - ], + "addresses": ["1DnPPFQPrfyNTiHPXhDFyqNnW9T62GEhB1"], "tags": ["Lucky pool", "GDPool"], "link": "" }, { "id": 159, "name": "Mining-Dutch", - "addresses": [ - "1AfPSq5ZbqBaxU5QAayLQJMcXV8HZt92eq" - ], + "addresses": ["1AfPSq5ZbqBaxU5QAayLQJMcXV8HZt92eq"], "tags": ["/Mining-Dutch/"], "link": "https://www.mining-dutch.nl/" }, @@ -1736,10 +1281,7 @@ "id": 160, "name": "Public Pool", "addresses": [], - "tags": [ - "Public-Pool", - "Public Pool on Umbrel" - ], + "tags": ["Public-Pool", "Public Pool on Umbrel"], "link": "https://web.public-pool.io/" }, { @@ -1755,18 +1297,14 @@ { "id": 162, "name": "Innopolis Tech", - "addresses": [ - "bc1q75t4wewkmf3l9qg097zvtlh05v5pdz6699kv8k" - ], + "addresses": ["bc1q75t4wewkmf3l9qg097zvtlh05v5pdz6699kv8k"], "tags": ["Innopolis", "Innopolis.tech"], "link": "https://innopolis.tech/" }, { "id": 163, "name": "nymkappa", - "addresses": [ - "tb1qdyy39724wqnqqqduv6zxsf56s2ec9lgypxs59h" - ], + "addresses": ["tb1qdyy39724wqnqqqduv6zxsf56s2ec9lgypxs59h"], "tags": ["/@nymkappa/"], "link": "https://github.com/nymkappa" }, @@ -1774,10 +1312,7 @@ "id": 164, "name": "BTCLab", "addresses": [], - "tags": [ - "BTCLab", - "BTCLab.dev" - ], + "tags": ["BTCLab", "BTCLab.dev"], "link": "https://btclab.dev/" }, { @@ -1790,19 +1325,29 @@ { "id": 166, "name": "RedRock Pool", - "addresses": [ - "3554kSaWNnP3B49Xyybert7gmxq2YSnfnx" - ], + "addresses": ["3554kSaWNnP3B49Xyybert7gmxq2YSnfnx"], "tags": ["RedRock"], "link": "https://redrock.pro/" }, { "id": 167, "name": "Est3lar", - "addresses": [ - "34qGNFx6uQv6SzjYPbYVWtjvuy5DSGugt8" - ], + "addresses": ["34qGNFx6uQv6SzjYPbYVWtjvuy5DSGugt8"], "tags": ["/Est3lar/", "est3lar", "Est3lar", "EST3LAR"], "link": "https://est3lar.io" + }, + { + "id": 168, + "name": "Braiins Solo", + "addresses": [], + "tags": ["/braiinssolo/"], + "link": "https://solo.braiins.com" + }, + { + "id": 169, + "name": "SoloPool.com", + "addresses": ["bc1qreaftg3lr53nv84dnxhcvchmswevzlp9tdj2jd"], + "tags": ["/Mined @ SoloPool.Com/"], + "link": "https://solopool.com" } ] diff --git a/crates/brk_types/src/addr_validation.rs b/crates/brk_types/src/addr_validation.rs index 8b1234554..bdf1b7bba 100644 --- a/crates/brk_types/src/addr_validation.rs +++ b/crates/brk_types/src/addr_validation.rs @@ -35,11 +35,19 @@ pub struct AddrValidation { /// Witness program in hex #[serde(skip_serializing_if = "Option::is_none")] pub witness_program: Option, + + /// Error locations (empty array for most errors) + #[serde(skip_serializing_if = "Option::is_none")] + pub error_locations: Option>, + + /// Error message for invalid addresses + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, } impl AddrValidation { - /// Returns an invalid validation result - pub fn invalid() -> Self { + /// Returns an invalid validation result with error detail + pub fn invalid(error: String) -> Self { Self { isvalid: false, addr: None, @@ -48,13 +56,16 @@ impl AddrValidation { iswitness: None, witness_version: None, witness_program: None, + error_locations: Some(vec![]), + error: Some(error), } } /// Validate a Bitcoin address string and return details pub fn from_addr(addr: &str) -> Self { - let Ok(script) = AddrBytes::addr_to_script(addr) else { - return Self::invalid(); + let script = match AddrBytes::addr_to_script(addr) { + Ok(s) => s, + Err(e) => return Self::invalid(e.to_string()), }; let output_type = OutputType::from(&script); @@ -86,6 +97,8 @@ impl AddrValidation { iswitness: Some(is_witness), witness_version, witness_program, + error_locations: None, + error: None, } } } diff --git a/crates/brk_types/src/block_fees_entry.rs b/crates/brk_types/src/block_fees_entry.rs index 6e2590a0d..e9a67b460 100644 --- a/crates/brk_types/src/block_fees_entry.rs +++ b/crates/brk_types/src/block_fees_entry.rs @@ -1,7 +1,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{Height, Sats, Timestamp}; +use crate::{Dollars, Height, Sats, Timestamp}; /// A single block fees data point. #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -10,4 +10,7 @@ pub struct BlockFeesEntry { pub avg_height: Height, pub timestamp: Timestamp, pub avg_fees: Sats, + /// BTC/USD price at that height + #[serde(rename = "USD")] + pub usd: Dollars, } diff --git a/crates/brk_types/src/block_header.rs b/crates/brk_types/src/block_header.rs index ece021b3c..dc27af8e3 100644 --- a/crates/brk_types/src/block_header.rs +++ b/crates/brk_types/src/block_header.rs @@ -19,9 +19,6 @@ pub struct BlockHeader { /// Merkle root of the transaction tree pub merkle_root: String, - /// Block timestamp as claimed by the miner (Unix time) - pub time: u32, - /// Compact target (bits) pub bits: u32, @@ -35,7 +32,6 @@ impl From
for BlockHeader { version: h.version.to_consensus() as u32, previous_block_hash: BlockHash::from(h.prev_blockhash), merkle_root: h.merkle_root.to_string(), - time: h.time, bits: h.bits.to_consensus(), nonce: h.nonce, } diff --git a/crates/brk_types/src/block_info.rs b/crates/brk_types/src/block_info.rs index 71e231157..3ed62e094 100644 --- a/crates/brk_types/src/block_info.rs +++ b/crates/brk_types/src/block_info.rs @@ -1,37 +1,37 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{BlockHash, BlockHeader, Height, Timestamp, Weight}; +use crate::{BlockHash, Height, Timestamp, Weight}; /// Block information matching mempool.space /api/block/{hash} #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct BlockInfo { /// Block hash pub id: BlockHash, - /// Block height pub height: Height, - - /// Block header fields - #[serde(flatten)] - pub header: BlockHeader, - + /// Block version + pub version: u32, /// Block timestamp (Unix time) pub timestamp: Timestamp, - - /// Number of transactions in the block + /// Number of transactions pub tx_count: u32, - /// Block size in bytes pub size: u64, - /// Block weight in weight units pub weight: Weight, - + /// Merkle root of the transaction tree + pub merkle_root: String, + /// Previous block hash + #[serde(rename = "previousblockhash")] + pub previous_block_hash: BlockHash, /// Median time of the last 11 blocks #[serde(rename = "mediantime")] pub median_time: Timestamp, - + /// Nonce + pub nonce: u32, + /// Compact target (bits) + pub bits: u32, /// Block difficulty pub difficulty: f64, } diff --git a/crates/brk_types/src/block_pool.rs b/crates/brk_types/src/block_pool.rs index 80e3d7c28..0d36d122d 100644 --- a/crates/brk_types/src/block_pool.rs +++ b/crates/brk_types/src/block_pool.rs @@ -5,6 +5,7 @@ use crate::PoolSlug; /// Mining pool identification for a block #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] pub struct BlockPool { /// Unique pool identifier pub id: u8, @@ -14,4 +15,7 @@ pub struct BlockPool { /// URL-friendly pool identifier pub slug: PoolSlug, + + /// Alternative miner names (if identified) + pub miner_names: Option, } diff --git a/crates/brk_types/src/block_rewards_entry.rs b/crates/brk_types/src/block_rewards_entry.rs index 01405cf4f..fb7e0d9cd 100644 --- a/crates/brk_types/src/block_rewards_entry.rs +++ b/crates/brk_types/src/block_rewards_entry.rs @@ -1,11 +1,16 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::{Dollars, Height, Sats, Timestamp}; + /// A single block rewards data point. #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct BlockRewardsEntry { - pub avg_height: u32, - pub timestamp: u32, - pub avg_rewards: u64, + pub avg_height: Height, + pub timestamp: Timestamp, + pub avg_rewards: Sats, + /// BTC/USD price at that height + #[serde(rename = "USD")] + pub usd: Dollars, } diff --git a/crates/brk_types/src/block_size_entry.rs b/crates/brk_types/src/block_size_entry.rs index 36031506c..f5541284c 100644 --- a/crates/brk_types/src/block_size_entry.rs +++ b/crates/brk_types/src/block_size_entry.rs @@ -1,11 +1,13 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::{Height, Timestamp}; + /// A single block size data point. #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct BlockSizeEntry { - pub avg_height: u32, - pub timestamp: u32, + pub avg_height: Height, + pub timestamp: Timestamp, pub avg_size: u64, } diff --git a/crates/brk_types/src/block_status.rs b/crates/brk_types/src/block_status.rs index 2159f6e58..1d59c8d50 100644 --- a/crates/brk_types/src/block_status.rs +++ b/crates/brk_types/src/block_status.rs @@ -13,8 +13,7 @@ pub struct BlockStatus { #[serde(skip_serializing_if = "Option::is_none")] pub height: Option, - /// Hash of the next block in the best chain (only if in best chain and not tip) - #[serde(skip_serializing_if = "Option::is_none")] + /// Hash of the next block in the best chain (null if tip) pub next_best: Option, } diff --git a/crates/brk_types/src/block_weight_entry.rs b/crates/brk_types/src/block_weight_entry.rs index 559b8f90f..fc001d41c 100644 --- a/crates/brk_types/src/block_weight_entry.rs +++ b/crates/brk_types/src/block_weight_entry.rs @@ -1,11 +1,13 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use crate::{Height, Timestamp, Weight}; + /// A single block weight data point. #[derive(Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct BlockWeightEntry { - pub avg_height: u32, - pub timestamp: u32, - pub avg_weight: u64, + pub avg_height: Height, + pub timestamp: Timestamp, + pub avg_weight: Weight, } diff --git a/crates/brk_types/src/blockhash.rs b/crates/brk_types/src/blockhash.rs index 71dca3638..b7e4cf6d0 100644 --- a/crates/brk_types/src/blockhash.rs +++ b/crates/brk_types/src/blockhash.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize, Serializer, de}; use vecdb::{Bytes, Formattable}; /// Block hash -#[derive(Debug, Deref, Clone, PartialEq, Eq, Bytes, JsonSchema)] +#[derive(Default, Debug, Deref, Clone, PartialEq, Eq, Bytes, JsonSchema)] #[repr(C)] #[schemars( transparent, diff --git a/crates/brk_types/src/blockhash_prefix.rs b/crates/brk_types/src/blockhash_prefix.rs index f9257c3f9..d9f0603a7 100644 --- a/crates/brk_types/src/blockhash_prefix.rs +++ b/crates/brk_types/src/blockhash_prefix.rs @@ -29,6 +29,13 @@ impl From for BlockHashPrefix { } } +impl From for BlockHashPrefix { + #[inline] + fn from(value: u64) -> Self { + Self(value) + } +} + impl From for ByteView { #[inline] fn from(value: BlockHashPrefix) -> Self { diff --git a/crates/brk_types/src/cpfp.rs b/crates/brk_types/src/cpfp.rs index ef90c5b60..fdca68d9d 100644 --- a/crates/brk_types/src/cpfp.rs +++ b/crates/brk_types/src/cpfp.rs @@ -1,21 +1,33 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::{FeeRate, Sats, Txid, Weight}; +use crate::{FeeRate, Sats, Txid, VSize, Weight}; /// CPFP (Child Pays For Parent) information for a transaction #[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] pub struct CpfpInfo { + /// Ancestor transactions in the CPFP chain pub ancestors: Vec, + /// Best (highest fee rate) descendant, if any + pub best_descendant: Option, + /// Descendant transactions in the CPFP chain pub descendants: Vec, - #[serde(rename = "effectiveFeePerVsize")] + /// Effective fee rate considering CPFP relationships (sat/vB) pub effective_fee_per_vsize: FeeRate, + /// Transaction fee (sats) + pub fee: Sats, + /// Adjusted virtual size (accounting for sigops) + pub adjusted_vsize: VSize, } /// A transaction in a CPFP relationship -#[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct CpfpEntry { + /// Transaction ID pub txid: Txid, + /// Transaction weight pub weight: Weight, + /// Transaction fee (sats) pub fee: Sats, } diff --git a/crates/brk_types/src/difficulty_adjustment.rs b/crates/brk_types/src/difficulty_adjustment.rs index edba7a7eb..a82300b40 100644 --- a/crates/brk_types/src/difficulty_adjustment.rs +++ b/crates/brk_types/src/difficulty_adjustment.rs @@ -1,7 +1,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::Height; +use crate::{Height, Timestamp}; /// Difficulty adjustment information. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] @@ -15,35 +15,43 @@ pub struct DifficultyAdjustment { #[schemars(example = 2.5)] pub difficulty_change: f64, - /// Estimated Unix timestamp of next retarget - #[schemars(example = 1627762478)] + /// Estimated timestamp of next retarget (milliseconds) + #[schemars(example = 1627762478000_u64)] pub estimated_retarget_date: u64, /// Blocks remaining until retarget #[schemars(example = 1121)] pub remaining_blocks: u32, - /// Estimated seconds until retarget - #[schemars(example = 665977)] + /// Estimated time until retarget (milliseconds) + #[schemars(example = 665977000_u64)] pub remaining_time: u64, /// Previous difficulty adjustment (%) #[schemars(example = -4.8)] pub previous_retarget: f64, + /// Timestamp of most recent retarget (seconds) + #[schemars(example = 1627000000_u64)] + pub previous_time: Timestamp, + /// Height of next retarget #[schemars(example = 741888)] pub next_retarget_height: Height, - /// Average block time in current epoch (seconds) - #[schemars(example = 580)] + /// Average block time in current epoch (milliseconds) + #[schemars(example = 580000_u64)] pub time_avg: u64, - /// Time-adjusted average (accounting for timestamp manipulation) - #[schemars(example = 580)] + /// Time-adjusted average (milliseconds) + #[schemars(example = 580000_u64)] pub adjusted_time_avg: u64, /// Time offset from expected schedule (seconds) #[schemars(example = 0)] pub time_offset: i64, + + /// Expected blocks based on wall clock time since epoch start + #[schemars(example = 1827.21)] + pub expected_blocks: f64, } diff --git a/crates/brk_types/src/difficulty_entry.rs b/crates/brk_types/src/difficulty_entry.rs index 55161fe71..06518cfb7 100644 --- a/crates/brk_types/src/difficulty_entry.rs +++ b/crates/brk_types/src/difficulty_entry.rs @@ -3,13 +3,15 @@ use serde::{Deserialize, Serialize}; use super::{Height, Timestamp}; -/// A single difficulty data point. +/// A single difficulty data point in the hashrate summary. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct DifficultyEntry { - /// Unix timestamp of the difficulty adjustment. - pub timestamp: Timestamp, - /// Difficulty value. - pub difficulty: f64, - /// Block height of the adjustment. + /// Unix timestamp of the difficulty adjustment + pub time: Timestamp, + /// Block height of the adjustment pub height: Height, + /// Difficulty value + pub difficulty: f64, + /// Adjustment ratio (new/previous, e.g. 1.068 = +6.8%) + pub adjustment: f64, } diff --git a/crates/brk_types/src/feerate.rs b/crates/brk_types/src/feerate.rs index c4263e09f..f28d06b8e 100644 --- a/crates/brk_types/src/feerate.rs +++ b/crates/brk_types/src/feerate.rs @@ -7,7 +7,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{CheckedSub, Formattable, Pco}; -use super::{Sats, VSize}; +use super::{Sats, VSize, Weight}; /// Fee rate in sats/vB #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)] @@ -36,6 +36,13 @@ impl From<(Sats, VSize)> for FeeRate { } } +impl From<(Sats, Weight)> for FeeRate { + #[inline] + fn from((sats, weight): (Sats, Weight)) -> Self { + Self::from((sats, VSize::from(weight.to_vbytes_ceil()))) + } +} + impl From for FeeRate { #[inline] fn from(value: f64) -> Self { diff --git a/crates/brk_types/src/pool_detail.rs b/crates/brk_types/src/pool_detail.rs index 185914a81..f3870a321 100644 --- a/crates/brk_types/src/pool_detail.rs +++ b/crates/brk_types/src/pool_detail.rs @@ -31,7 +31,7 @@ pub struct PoolDetail { /// Pool information for detail view #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct PoolDetailInfo { - /// Unique pool identifier + /// Pool identifier pub id: u8, /// Pool name @@ -41,13 +41,16 @@ pub struct PoolDetailInfo { pub link: Cow<'static, str>, /// Known payout addresses - pub addrs: Vec>, + pub addresses: Vec>, /// Coinbase tag patterns (regexes) pub regexes: Vec>, /// URL-friendly pool identifier pub slug: PoolSlug, + + /// Unique pool identifier + pub unique_id: u8, } impl From<&'static Pool> for PoolDetailInfo { @@ -56,9 +59,10 @@ impl From<&'static Pool> for PoolDetailInfo { id: pool.unique_id(), name: Cow::Borrowed(pool.name), link: Cow::Borrowed(pool.link), - addrs: pool.addrs.iter().map(|&s| Cow::Borrowed(s)).collect(), + addresses: pool.addrs.iter().map(|&s| Cow::Borrowed(s)).collect(), regexes: pool.tags.iter().map(|&s| Cow::Borrowed(s)).collect(), slug: pool.slug(), + unique_id: pool.unique_id(), } } } diff --git a/crates/brk_types/src/pool_slug.rs b/crates/brk_types/src/pool_slug.rs index a316aacb3..392346a72 100644 --- a/crates/brk_types/src/pool_slug.rs +++ b/crates/brk_types/src/pool_slug.rs @@ -198,10 +198,8 @@ pub enum PoolSlug { Parasite, RedRockPool, Est3lar, - #[serde(skip)] - Dummy168, - #[serde(skip)] - Dummy169, + BraiinsSolo, + SoloPool, #[serde(skip)] Dummy170, #[serde(skip)] diff --git a/crates/brk_types/src/pool_stats.rs b/crates/brk_types/src/pool_stats.rs index f98b623d8..98a5ba65f 100644 --- a/crates/brk_types/src/pool_stats.rs +++ b/crates/brk_types/src/pool_stats.rs @@ -34,20 +34,26 @@ pub struct PoolStats { /// Pool's share of total blocks (0.0 - 1.0) pub share: f64, + + /// Unique pool identifier + #[serde(rename = "poolUniqueId")] + pub pool_unique_id: u8, } impl PoolStats { /// Create a new PoolStats from a Pool reference pub fn new(pool: &'static Pool, block_count: u64, rank: u32, share: f64) -> Self { + let id = pool.unique_id(); Self { - pool_id: pool.unique_id(), + pool_id: id, name: Cow::Borrowed(pool.name), link: Cow::Borrowed(pool.link), block_count, rank, - empty_blocks: 0, // TODO: track empty blocks if needed + empty_blocks: 0, slug: pool.slug(), share, + pool_unique_id: id, } } } diff --git a/crates/brk_types/src/pools.rs b/crates/brk_types/src/pools.rs index 37e9846b3..8d1f7ee46 100644 --- a/crates/brk_types/src/pools.rs +++ b/crates/brk_types/src/pools.rs @@ -7,7 +7,7 @@ use crate::PoolSlug; use super::Pool; const JSON_DATA: &str = include_str!("../pools-v2.json"); -const POOL_COUNT: usize = 168; +const POOL_COUNT: usize = 170; const TESTNET_IDS: &[u16] = &[145, 146, 149, 150, 156, 163]; #[derive(Deserialize)] diff --git a/crates/brk_types/src/pools_summary.rs b/crates/brk_types/src/pools_summary.rs index 98509bdef..aa2cc580c 100644 --- a/crates/brk_types/src/pools_summary.rs +++ b/crates/brk_types/src/pools_summary.rs @@ -5,15 +5,16 @@ use crate::PoolStats; /// Mining pools response for a time period #[derive(Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "camelCase")] pub struct PoolsSummary { /// List of pools sorted by block count descending pub pools: Vec, - /// Total blocks in the time period - #[serde(rename = "blockCount")] pub block_count: u64, - /// Estimated network hashrate (hashes per second) - #[serde(rename = "lastEstimatedHashrate")] pub last_estimated_hashrate: u128, + /// Estimated network hashrate over last 3 days + pub last_estimated_hashrate3d: u128, + /// Estimated network hashrate over last 1 week + pub last_estimated_hashrate1w: u128, } diff --git a/crates/brk_types/src/tx.rs b/crates/brk_types/src/tx.rs index d9f13c577..3c1de9114 100644 --- a/crates/brk_types/src/tx.rs +++ b/crates/brk_types/src/tx.rs @@ -21,6 +21,14 @@ pub struct Transaction { #[serde(rename = "locktime")] pub lock_time: RawLockTime, + /// Transaction inputs + #[serde(rename = "vin")] + pub input: Vec, + + /// Transaction outputs + #[serde(rename = "vout")] + pub output: Vec, + /// Transaction size in bytes #[schemars(example = 222)] #[serde(rename = "size")] @@ -39,14 +47,6 @@ pub struct Transaction { #[schemars(example = Sats::new(31))] pub fee: Sats, - /// Transaction inputs - #[serde(rename = "vin")] - pub input: Vec, - - /// Transaction outputs - #[serde(rename = "vout")] - pub output: Vec, - pub status: TxStatus, } diff --git a/crates/brk_types/src/txin.rs b/crates/brk_types/src/txin.rs index 4c1e120f0..4e8dee9e8 100644 --- a/crates/brk_types/src/txin.rs +++ b/crates/brk_types/src/txin.rs @@ -10,6 +10,7 @@ pub struct TxIn { #[schemars(example = "0000000000000000000000000000000000000000000000000000000000000000")] pub txid: Txid, + /// Output index being spent #[schemars(example = 0)] pub vout: Vout, @@ -17,55 +18,32 @@ pub struct TxIn { #[schemars(example = None as Option)] pub prevout: Option, - /// Signature script (for non-SegWit inputs) - #[schemars( - rename = "scriptsig", - with = "String", - example = "04ffff001d0104455468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73" - )] + /// Signature script (hex, for non-SegWit inputs) + #[schemars(rename = "scriptsig", with = "String")] pub script_sig: ScriptBuf, /// Signature script in assembly format - #[schemars( - rename = "scriptsig_asm", - with = "String", - example = "OP_PUSHBYTES_4 ffff001d OP_PUSHBYTES_1 04 OP_PUSHBYTES_69 5468652054696d65732030332f4a616e2f32303039204368616e63656c6c6f72206f6e206272696e6b206f66207365636f6e64206261696c6f757420666f722062616e6b73" - )] + #[schemars(rename = "scriptsig_asm", with = "String")] pub script_sig_asm: (), - // /// Witness data (for SegWit inputs) - // #[schemars(example = vec!["3045022100d0c9936990bf00bdba15f425f0f360a223d5cbf81f4bf8477fe6c6d838fb5fae02207e42a8325a4dd41702bf065aa6e0a1b7b0b8ee92a5e6c182da018b0afc82c40601".to_string()])] - // pub witness: Vec, - // + /// Witness data (hex-encoded stack items, present for SegWit inputs) + pub witness: Vec, + /// Whether this input is a coinbase (block reward) input #[schemars(example = false)] pub is_coinbase: bool, /// Input sequence number - #[schemars(example = 429496729)] + #[schemars(example = 4294967293_u32)] pub sequence: u32, - /// Inner redeemscript in assembly format (for P2SH-wrapped SegWit) - #[allow(dead_code)] - #[schemars( - rename = "inner_redeemscript_asm", - with = "Option", - example = Some("OP_0 OP_PUSHBYTES_20 992a1f7420fc5285070d19c71ff2efb1e356ad2f".to_string()) - )] + /// Inner redeemscript in assembly (for P2SH-wrapped SegWit: scriptsig + witness both present) + #[schemars(rename = "inner_redeemscript_asm", with = "String")] pub inner_redeem_script_asm: (), -} -impl TxIn { - pub fn script_sig_asm(&self) -> String { - self.script_sig.to_asm_string() - } - - pub fn inner_redeemscript_asm(&self) -> String { - self.script_sig - .redeem_script() - .map(|s| s.to_asm_string()) - .unwrap_or_default() - } + /// Inner witnessscript in assembly (for P2WSH: last witness item decoded as script) + #[schemars(rename = "inner_witnessscript_asm", with = "String")] + pub inner_witness_script_asm: (), } impl Serialize for TxIn { @@ -73,16 +51,55 @@ impl Serialize for TxIn { where S: Serializer, { - let mut state = serializer.serialize_struct("TxIn", 8)?; + let has_witness = !self.witness.is_empty(); + let has_scriptsig = !self.script_sig.is_empty(); + + // P2SH-wrapped SegWit: both scriptsig and witness present + let inner_redeem = if has_scriptsig && has_witness { + self.script_sig + .redeem_script() + .map(|s| s.to_asm_string()) + .unwrap_or_default() + } else { + String::new() + }; + + // P2WSH: witness has >2 items, last is the witnessScript + let inner_witness = if has_witness && !has_scriptsig && self.witness.len() > 2 { + if let Some(last) = self.witness.last() { + let bytes: Vec = + bitcoin::hex::FromHex::from_hex(last).unwrap_or_default(); + ScriptBuf::from(bytes).to_asm_string() + } else { + String::new() + } + } else { + String::new() + }; + + let has_inner_redeem = !inner_redeem.is_empty(); + let has_inner_witness = !inner_witness.is_empty(); + let field_count = + 7 + has_witness as usize + has_inner_redeem as usize + has_inner_witness as usize; + + let mut state = serializer.serialize_struct("TxIn", field_count)?; state.serialize_field("txid", &self.txid)?; state.serialize_field("vout", &self.vout)?; state.serialize_field("prevout", &self.prevout)?; state.serialize_field("scriptsig", &self.script_sig.to_hex_string())?; - state.serialize_field("scriptsig_asm", &self.script_sig_asm())?; + state.serialize_field("scriptsig_asm", &self.script_sig.to_asm_string())?; + if has_witness { + state.serialize_field("witness", &self.witness)?; + } state.serialize_field("is_coinbase", &self.is_coinbase)?; state.serialize_field("sequence", &self.sequence)?; - state.serialize_field("inner_redeemscript_asm", &self.inner_redeemscript_asm())?; + if has_inner_redeem { + state.serialize_field("inner_redeemscript_asm", &inner_redeem)?; + } + if has_inner_witness { + state.serialize_field("inner_witnessscript_asm", &inner_witness)?; + } state.end() } diff --git a/crates/brk_types/src/weight.rs b/crates/brk_types/src/weight.rs index 1500db06a..a786071fd 100644 --- a/crates/brk_types/src/weight.rs +++ b/crates/brk_types/src/weight.rs @@ -76,6 +76,13 @@ impl From for Weight { } } +impl From for Weight { + #[inline] + fn from(value: u64) -> Self { + Self(value) + } +} + impl From for Weight { #[inline] fn from(value: usize) -> Self { diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index d67f8fcbc..c5b756361 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -60,6 +60,8 @@ * @property {?boolean=} iswitness - Whether this is a witness address * @property {?number=} witnessVersion - Witness version (0 for P2WPKH/P2WSH, 1 for P2TR) * @property {?string=} witnessProgram - Witness program in hex + * @property {?number[]=} errorLocations - Error locations (empty array for most errors) + * @property {?string=} error - Error message for invalid addresses */ /** * Unified index for any address type (funded or empty) @@ -145,6 +147,7 @@ * @property {Height} avgHeight * @property {Timestamp} timestamp * @property {Sats} avgFees + * @property {Dollars} uSD - BTC/USD price at that height */ /** * Block hash @@ -171,17 +174,16 @@ * @typedef {Object} BlockInfo * @property {BlockHash} id - Block hash * @property {Height} height - Block height - * @property {number} version - Block version, used for soft fork signaling - * @property {BlockHash} previousblockhash - Previous block hash - * @property {string} merkleRoot - Merkle root of the transaction tree - * @property {number} time - Block timestamp as claimed by the miner (Unix time) - * @property {number} bits - Compact target (bits) - * @property {number} nonce - Nonce used to produce a valid block hash + * @property {number} version - Block version * @property {Timestamp} timestamp - Block timestamp (Unix time) - * @property {number} txCount - Number of transactions in the block + * @property {number} txCount - Number of transactions * @property {number} size - Block size in bytes * @property {Weight} weight - Block weight in weight units + * @property {string} merkleRoot - Merkle root of the transaction tree + * @property {BlockHash} previousblockhash - Previous block hash * @property {Timestamp} mediantime - Median time of the last 11 blocks + * @property {number} nonce - Nonce + * @property {number} bits - Compact target (bits) * @property {number} difficulty - Block difficulty */ /** @@ -190,18 +192,18 @@ * @typedef {Object} BlockInfoV1 * @property {BlockHash} id - Block hash * @property {Height} height - Block height - * @property {number} version - Block version, used for soft fork signaling - * @property {BlockHash} previousblockhash - Previous block hash - * @property {string} merkleRoot - Merkle root of the transaction tree - * @property {number} time - Block timestamp as claimed by the miner (Unix time) - * @property {number} bits - Compact target (bits) - * @property {number} nonce - Nonce used to produce a valid block hash + * @property {number} version - Block version * @property {Timestamp} timestamp - Block timestamp (Unix time) - * @property {number} txCount - Number of transactions in the block + * @property {number} txCount - Number of transactions * @property {number} size - Block size in bytes * @property {Weight} weight - Block weight in weight units + * @property {string} merkleRoot - Merkle root of the transaction tree + * @property {BlockHash} previousblockhash - Previous block hash * @property {Timestamp} mediantime - Median time of the last 11 blocks + * @property {number} nonce - Nonce + * @property {number} bits - Compact target (bits) * @property {number} difficulty - Block difficulty + * @property {boolean=} stale - Whether this block is stale (orphaned) * @property {BlockExtras} extras - Extended block data */ /** @@ -211,21 +213,23 @@ * @property {number} id - Unique pool identifier * @property {string} name - Pool name * @property {PoolSlug} slug - URL-friendly pool identifier + * @property {?string=} minerNames - Alternative miner names (if identified) */ /** * A single block rewards data point. * * @typedef {Object} BlockRewardsEntry - * @property {number} avgHeight - * @property {number} timestamp - * @property {number} avgRewards + * @property {Height} avgHeight + * @property {Timestamp} timestamp + * @property {Sats} avgRewards + * @property {Dollars} uSD - BTC/USD price at that height */ /** * A single block size data point. * * @typedef {Object} BlockSizeEntry - * @property {number} avgHeight - * @property {number} timestamp + * @property {Height} avgHeight + * @property {Timestamp} timestamp * @property {number} avgSize */ /** @@ -241,7 +245,7 @@ * @typedef {Object} BlockStatus * @property {boolean} inBestChain - Whether this block is in the best chain * @property {(Height|null)=} height - Block height (only if in best chain) - * @property {(BlockHash|null)=} nextBest - Hash of the next block in the best chain (only if in best chain and not tip) + * @property {(BlockHash|null)=} nextBest - Hash of the next block in the best chain (null if tip) */ /** * Block information returned for timestamp queries @@ -255,9 +259,9 @@ * A single block weight data point. * * @typedef {Object} BlockWeightEntry - * @property {number} avgHeight - * @property {number} timestamp - * @property {number} avgWeight + * @property {Height} avgHeight + * @property {Timestamp} timestamp + * @property {Weight} avgWeight */ /** * Unsigned cents (u64) - for values that should never be negative. @@ -342,17 +346,20 @@ * A transaction in a CPFP relationship * * @typedef {Object} CpfpEntry - * @property {Txid} txid - * @property {Weight} weight - * @property {Sats} fee + * @property {Txid} txid - Transaction ID + * @property {Weight} weight - Transaction weight + * @property {Sats} fee - Transaction fee (sats) */ /** * CPFP (Child Pays For Parent) information for a transaction * * @typedef {Object} CpfpInfo - * @property {CpfpEntry[]} ancestors - * @property {CpfpEntry[]} descendants - * @property {FeeRate} effectiveFeePerVsize + * @property {CpfpEntry[]} ancestors - Ancestor transactions in the CPFP chain + * @property {(CpfpEntry|null)=} bestDescendant - Best (highest fee rate) descendant, if any + * @property {CpfpEntry[]} descendants - Descendant transactions in the CPFP chain + * @property {FeeRate} effectiveFeePerVsize - Effective fee rate considering CPFP relationships (sat/vB) + * @property {Sats} fee - Transaction fee (sats) + * @property {VSize} adjustedVsize - Adjusted virtual size (accounting for sigops) */ /** * Data range with output format for API query parameters @@ -386,14 +393,16 @@ * @typedef {Object} DifficultyAdjustment * @property {number} progressPercent - Progress through current difficulty epoch (0-100%) * @property {number} difficultyChange - Estimated difficulty change at next retarget (%) - * @property {number} estimatedRetargetDate - Estimated Unix timestamp of next retarget + * @property {number} estimatedRetargetDate - Estimated timestamp of next retarget (milliseconds) * @property {number} remainingBlocks - Blocks remaining until retarget - * @property {number} remainingTime - Estimated seconds until retarget + * @property {number} remainingTime - Estimated time until retarget (milliseconds) * @property {number} previousRetarget - Previous difficulty adjustment (%) + * @property {Timestamp} previousTime - Timestamp of most recent retarget (seconds) * @property {Height} nextRetargetHeight - Height of next retarget - * @property {number} timeAvg - Average block time in current epoch (seconds) - * @property {number} adjustedTimeAvg - Time-adjusted average (accounting for timestamp manipulation) + * @property {number} timeAvg - Average block time in current epoch (milliseconds) + * @property {number} adjustedTimeAvg - Time-adjusted average (milliseconds) * @property {number} timeOffset - Time offset from expected schedule (seconds) + * @property {number} expectedBlocks - Expected blocks based on wall clock time since epoch start */ /** * A single difficulty adjustment entry. @@ -406,12 +415,13 @@ * @property {number} changePercent */ /** - * A single difficulty data point. + * A single difficulty data point in the hashrate summary. * * @typedef {Object} DifficultyEntry - * @property {Timestamp} timestamp - Unix timestamp of the difficulty adjustment. - * @property {number} difficulty - Difficulty value. - * @property {Height} height - Block height of the adjustment. + * @property {Timestamp} time - Unix timestamp of the difficulty adjustment + * @property {Height} height - Block height of the adjustment + * @property {number} difficulty - Difficulty value + * @property {number} adjustment - Adjustment ratio (new/previous, e.g. 1.068 = +6.8%) */ /** * Disk usage of the indexed data @@ -729,12 +739,13 @@ * Pool information for detail view * * @typedef {Object} PoolDetailInfo - * @property {number} id - Unique pool identifier + * @property {number} id - Pool identifier * @property {string} name - Pool name * @property {string} link - Pool website URL - * @property {string[]} addrs - Known payout addresses + * @property {string[]} addresses - Known payout addresses * @property {string[]} regexes - Coinbase tag patterns (regexes) * @property {PoolSlug} slug - URL-friendly pool identifier + * @property {number} uniqueId - Unique pool identifier */ /** * A single pool hashrate data point. @@ -753,7 +764,7 @@ * @property {PoolSlug} slug - URL-friendly pool identifier * @property {number} uniqueId - Unique numeric pool identifier */ -/** @typedef {("unknown"|"blockfills"|"ultimuspool"|"terrapool"|"luxor"|"onethash"|"btccom"|"bitfarms"|"huobipool"|"wayicn"|"canoepool"|"btctop"|"bitcoincom"|"pool175btc"|"gbminers"|"axbt"|"asicminer"|"bitminter"|"bitcoinrussia"|"btcserv"|"simplecoinus"|"btcguild"|"eligius"|"ozcoin"|"eclipsemc"|"maxbtc"|"triplemining"|"coinlab"|"pool50btc"|"ghashio"|"stminingcorp"|"bitparking"|"mmpool"|"polmine"|"kncminer"|"bitalo"|"f2pool"|"hhtt"|"megabigpower"|"mtred"|"nmcbit"|"yourbtcnet"|"givemecoins"|"braiinspool"|"antpool"|"multicoinco"|"bcpoolio"|"cointerra"|"kanopool"|"solock"|"ckpool"|"nicehash"|"bitclub"|"bitcoinaffiliatenetwork"|"btcc"|"bwpool"|"exxbw"|"bitsolo"|"bitfury"|"twentyoneinc"|"digitalbtc"|"eightbaochi"|"mybtccoinpool"|"tbdice"|"hashpool"|"nexious"|"bravomining"|"hotpool"|"okexpool"|"bcmonster"|"onehash"|"bixin"|"tatmaspool"|"viabtc"|"connectbtc"|"batpool"|"waterhole"|"dcexploration"|"dcex"|"btpool"|"fiftyeightcoin"|"bitcoinindia"|"shawnp0wers"|"phashio"|"rigpool"|"haozhuzhu"|"sevenpool"|"miningkings"|"hashbx"|"dpool"|"rawpool"|"haominer"|"helix"|"bitcoinukraine"|"poolin"|"secretsuperstar"|"tigerpoolnet"|"sigmapoolcom"|"okpooltop"|"hummerpool"|"tangpool"|"bytepool"|"spiderpool"|"novablock"|"miningcity"|"binancepool"|"minerium"|"lubiancom"|"okkong"|"aaopool"|"emcdpool"|"foundryusa"|"sbicrypto"|"arkpool"|"purebtccom"|"marapool"|"kucoinpool"|"entrustcharitypool"|"okminer"|"titan"|"pegapool"|"btcnuggets"|"cloudhashing"|"digitalxmintsy"|"telco214"|"btcpoolparty"|"multipool"|"transactioncoinmining"|"btcdig"|"trickysbtcpool"|"btcmp"|"eobot"|"unomp"|"patels"|"gogreenlight"|"bitcoinindiapool"|"ekanembtc"|"canoe"|"tiger"|"onem1x"|"zulupool"|"secpool"|"ocean"|"whitepool"|"wiz"|"wk057"|"futurebitapollosolo"|"carbonnegative"|"portlandhodl"|"phoenix"|"neopool"|"maxipool"|"bitfufupool"|"gdpool"|"miningdutch"|"publicpool"|"miningsquared"|"innopolistech"|"btclab"|"parasite"|"redrockpool"|"est3lar")} PoolSlug */ +/** @typedef {("unknown"|"blockfills"|"ultimuspool"|"terrapool"|"luxor"|"onethash"|"btccom"|"bitfarms"|"huobipool"|"wayicn"|"canoepool"|"btctop"|"bitcoincom"|"pool175btc"|"gbminers"|"axbt"|"asicminer"|"bitminter"|"bitcoinrussia"|"btcserv"|"simplecoinus"|"btcguild"|"eligius"|"ozcoin"|"eclipsemc"|"maxbtc"|"triplemining"|"coinlab"|"pool50btc"|"ghashio"|"stminingcorp"|"bitparking"|"mmpool"|"polmine"|"kncminer"|"bitalo"|"f2pool"|"hhtt"|"megabigpower"|"mtred"|"nmcbit"|"yourbtcnet"|"givemecoins"|"braiinspool"|"antpool"|"multicoinco"|"bcpoolio"|"cointerra"|"kanopool"|"solock"|"ckpool"|"nicehash"|"bitclub"|"bitcoinaffiliatenetwork"|"btcc"|"bwpool"|"exxbw"|"bitsolo"|"bitfury"|"twentyoneinc"|"digitalbtc"|"eightbaochi"|"mybtccoinpool"|"tbdice"|"hashpool"|"nexious"|"bravomining"|"hotpool"|"okexpool"|"bcmonster"|"onehash"|"bixin"|"tatmaspool"|"viabtc"|"connectbtc"|"batpool"|"waterhole"|"dcexploration"|"dcex"|"btpool"|"fiftyeightcoin"|"bitcoinindia"|"shawnp0wers"|"phashio"|"rigpool"|"haozhuzhu"|"sevenpool"|"miningkings"|"hashbx"|"dpool"|"rawpool"|"haominer"|"helix"|"bitcoinukraine"|"poolin"|"secretsuperstar"|"tigerpoolnet"|"sigmapoolcom"|"okpooltop"|"hummerpool"|"tangpool"|"bytepool"|"spiderpool"|"novablock"|"miningcity"|"binancepool"|"minerium"|"lubiancom"|"okkong"|"aaopool"|"emcdpool"|"foundryusa"|"sbicrypto"|"arkpool"|"purebtccom"|"marapool"|"kucoinpool"|"entrustcharitypool"|"okminer"|"titan"|"pegapool"|"btcnuggets"|"cloudhashing"|"digitalxmintsy"|"telco214"|"btcpoolparty"|"multipool"|"transactioncoinmining"|"btcdig"|"trickysbtcpool"|"btcmp"|"eobot"|"unomp"|"patels"|"gogreenlight"|"bitcoinindiapool"|"ekanembtc"|"canoe"|"tiger"|"onem1x"|"zulupool"|"secpool"|"ocean"|"whitepool"|"wiz"|"wk057"|"futurebitapollosolo"|"carbonnegative"|"portlandhodl"|"phoenix"|"neopool"|"maxipool"|"bitfufupool"|"gdpool"|"miningdutch"|"publicpool"|"miningsquared"|"innopolistech"|"btclab"|"parasite"|"redrockpool"|"est3lar"|"braiinssolo"|"solopool")} PoolSlug */ /** * @typedef {Object} PoolSlugAndHeightParam * @property {PoolSlug} slug @@ -775,6 +786,7 @@ * @property {number} emptyBlocks - Number of empty blocks mined * @property {PoolSlug} slug - URL-friendly pool identifier * @property {number} share - Pool's share of total blocks (0.0 - 1.0) + * @property {number} poolUniqueId - Unique pool identifier */ /** * Mining pools response for a time period @@ -783,6 +795,8 @@ * @property {PoolStats[]} pools - List of pools sorted by block count descending * @property {number} blockCount - Total blocks in the time period * @property {number} lastEstimatedHashrate - Estimated network hashrate (hashes per second) + * @property {number} lastEstimatedHashrate3d - Estimated network hashrate over last 3 days + * @property {number} lastEstimatedHashrate1w - Estimated network hashrate over last 1 week */ /** * Current price response matching mempool.space /api/v1/prices format @@ -990,12 +1004,12 @@ * @property {Txid} txid * @property {TxVersion} version * @property {RawLockTime} locktime + * @property {TxIn[]} vin - Transaction inputs + * @property {TxOut[]} vout - Transaction outputs * @property {number} size - Transaction size in bytes * @property {Weight} weight - Transaction weight * @property {number} sigops - Number of signature operations * @property {Sats} fee - Transaction fee in satoshis - * @property {TxIn[]} vin - Transaction inputs - * @property {TxOut[]} vout - Transaction outputs * @property {TxStatus} status */ /** @@ -1008,13 +1022,15 @@ * * @typedef {Object} TxIn * @property {Txid} txid - Transaction ID of the output being spent - * @property {Vout} vout + * @property {Vout} vout - Output index being spent * @property {(TxOut|null)=} prevout - Information about the previous output being spent - * @property {string} scriptsig - Signature script (for non-SegWit inputs) + * @property {string} scriptsig - Signature script (hex, for non-SegWit inputs) * @property {string} scriptsigAsm - Signature script in assembly format + * @property {string[]} witness - Witness data (hex-encoded stack items, present for SegWit inputs) * @property {boolean} isCoinbase - Whether this input is a coinbase (block reward) input * @property {number} sequence - Input sequence number - * @property {?string=} innerRedeemscriptAsm - Inner redeemscript in assembly format (for P2SH-wrapped SegWit) + * @property {string} innerRedeemscriptAsm - Inner redeemscript in assembly (for P2SH-wrapped SegWit: scriptsig + witness both present) + * @property {string} innerWitnessscriptAsm - Inner witnessscript in assembly (for P2WSH: last witness item decoded as script) */ /** @typedef {number} TxInIndex */ /** @typedef {number} TxIndex */ @@ -5683,6 +5699,8 @@ function createTransferPattern(client, acc) { * @property {BlocksDominancePattern} parasite * @property {BlocksDominancePattern} redrockpool * @property {BlocksDominancePattern} est3lar + * @property {BlocksDominancePattern} braiinssolo + * @property {BlocksDominancePattern} solopool */ /** @@ -6746,7 +6764,9 @@ class BrkClient extends BrkClientBase { "btclab": "BTCLab", "parasite": "Parasite", "redrockpool": "RedRock Pool", - "est3lar": "Est3lar" + "est3lar": "Est3lar", + "braiinssolo": "Braiins Solo", + "solopool": "SoloPool.com" }); TERM_NAMES = /** @type {const} */ ({ @@ -8712,6 +8732,8 @@ class BrkClient extends BrkClientBase { parasite: createBlocksDominancePattern(this, 'parasite'), redrockpool: createBlocksDominancePattern(this, 'redrockpool'), est3lar: createBlocksDominancePattern(this, 'est3lar'), + braiinssolo: createBlocksDominancePattern(this, 'braiinssolo'), + solopool: createBlocksDominancePattern(this, 'solopool'), }, }, prices: { diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index ec0dfd2da..2a7cd2c4f 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -49,11 +49,13 @@ BasisPointsSigned16 = int BasisPointsSigned32 = int # Bitcoin amount as floating point (1 BTC = 100,000,000 satoshis) Bitcoin = float -PoolSlug = Literal["unknown", "blockfills", "ultimuspool", "terrapool", "luxor", "onethash", "btccom", "bitfarms", "huobipool", "wayicn", "canoepool", "btctop", "bitcoincom", "pool175btc", "gbminers", "axbt", "asicminer", "bitminter", "bitcoinrussia", "btcserv", "simplecoinus", "btcguild", "eligius", "ozcoin", "eclipsemc", "maxbtc", "triplemining", "coinlab", "pool50btc", "ghashio", "stminingcorp", "bitparking", "mmpool", "polmine", "kncminer", "bitalo", "f2pool", "hhtt", "megabigpower", "mtred", "nmcbit", "yourbtcnet", "givemecoins", "braiinspool", "antpool", "multicoinco", "bcpoolio", "cointerra", "kanopool", "solock", "ckpool", "nicehash", "bitclub", "bitcoinaffiliatenetwork", "btcc", "bwpool", "exxbw", "bitsolo", "bitfury", "twentyoneinc", "digitalbtc", "eightbaochi", "mybtccoinpool", "tbdice", "hashpool", "nexious", "bravomining", "hotpool", "okexpool", "bcmonster", "onehash", "bixin", "tatmaspool", "viabtc", "connectbtc", "batpool", "waterhole", "dcexploration", "dcex", "btpool", "fiftyeightcoin", "bitcoinindia", "shawnp0wers", "phashio", "rigpool", "haozhuzhu", "sevenpool", "miningkings", "hashbx", "dpool", "rawpool", "haominer", "helix", "bitcoinukraine", "poolin", "secretsuperstar", "tigerpoolnet", "sigmapoolcom", "okpooltop", "hummerpool", "tangpool", "bytepool", "spiderpool", "novablock", "miningcity", "binancepool", "minerium", "lubiancom", "okkong", "aaopool", "emcdpool", "foundryusa", "sbicrypto", "arkpool", "purebtccom", "marapool", "kucoinpool", "entrustcharitypool", "okminer", "titan", "pegapool", "btcnuggets", "cloudhashing", "digitalxmintsy", "telco214", "btcpoolparty", "multipool", "transactioncoinmining", "btcdig", "trickysbtcpool", "btcmp", "eobot", "unomp", "patels", "gogreenlight", "bitcoinindiapool", "ekanembtc", "canoe", "tiger", "onem1x", "zulupool", "secpool", "ocean", "whitepool", "wiz", "wk057", "futurebitapollosolo", "carbonnegative", "portlandhodl", "phoenix", "neopool", "maxipool", "bitfufupool", "gdpool", "miningdutch", "publicpool", "miningsquared", "innopolistech", "btclab", "parasite", "redrockpool", "est3lar"] +PoolSlug = Literal["unknown", "blockfills", "ultimuspool", "terrapool", "luxor", "onethash", "btccom", "bitfarms", "huobipool", "wayicn", "canoepool", "btctop", "bitcoincom", "pool175btc", "gbminers", "axbt", "asicminer", "bitminter", "bitcoinrussia", "btcserv", "simplecoinus", "btcguild", "eligius", "ozcoin", "eclipsemc", "maxbtc", "triplemining", "coinlab", "pool50btc", "ghashio", "stminingcorp", "bitparking", "mmpool", "polmine", "kncminer", "bitalo", "f2pool", "hhtt", "megabigpower", "mtred", "nmcbit", "yourbtcnet", "givemecoins", "braiinspool", "antpool", "multicoinco", "bcpoolio", "cointerra", "kanopool", "solock", "ckpool", "nicehash", "bitclub", "bitcoinaffiliatenetwork", "btcc", "bwpool", "exxbw", "bitsolo", "bitfury", "twentyoneinc", "digitalbtc", "eightbaochi", "mybtccoinpool", "tbdice", "hashpool", "nexious", "bravomining", "hotpool", "okexpool", "bcmonster", "onehash", "bixin", "tatmaspool", "viabtc", "connectbtc", "batpool", "waterhole", "dcexploration", "dcex", "btpool", "fiftyeightcoin", "bitcoinindia", "shawnp0wers", "phashio", "rigpool", "haozhuzhu", "sevenpool", "miningkings", "hashbx", "dpool", "rawpool", "haominer", "helix", "bitcoinukraine", "poolin", "secretsuperstar", "tigerpoolnet", "sigmapoolcom", "okpooltop", "hummerpool", "tangpool", "bytepool", "spiderpool", "novablock", "miningcity", "binancepool", "minerium", "lubiancom", "okkong", "aaopool", "emcdpool", "foundryusa", "sbicrypto", "arkpool", "purebtccom", "marapool", "kucoinpool", "entrustcharitypool", "okminer", "titan", "pegapool", "btcnuggets", "cloudhashing", "digitalxmintsy", "telco214", "btcpoolparty", "multipool", "transactioncoinmining", "btcdig", "trickysbtcpool", "btcmp", "eobot", "unomp", "patels", "gogreenlight", "bitcoinindiapool", "ekanembtc", "canoe", "tiger", "onem1x", "zulupool", "secpool", "ocean", "whitepool", "wiz", "wk057", "futurebitapollosolo", "carbonnegative", "portlandhodl", "phoenix", "neopool", "maxipool", "bitfufupool", "gdpool", "miningdutch", "publicpool", "miningsquared", "innopolistech", "btclab", "parasite", "redrockpool", "est3lar", "braiinssolo", "solopool"] # Fee rate in sats/vB FeeRate = float # Transaction or block weight in weight units (WU) Weight = int +# US Dollar amount as floating point +Dollars = float # Block height Height = int # UNIX timestamp in seconds @@ -74,8 +76,6 @@ CentsSigned = int # Used for precise accumulation of investor cap values: Σ(price² × sats). # investor_price = investor_cap_raw / realized_cap_raw CentsSquaredSats = int -# US Dollar amount as floating point -Dollars = float # Closing price value for a time period Close = Dollars # Cohort identifier for cost basis distribution. @@ -95,6 +95,8 @@ CostBasisBucket = Literal["raw", "lin200", "lin500", "lin1000", "log10", "log50" # Value type for cost basis distribution. # Options: supply (BTC), realized (USD, price × supply), unrealized (USD, spot × supply). CostBasisValue = Literal["supply", "realized", "unrealized"] +# Virtual size in vbytes (weight / 4, rounded up) +VSize = int # Date in YYYYMMDD format stored as u32 Date = int # Output format for API responses @@ -121,8 +123,6 @@ Hour4 = int SeriesName = str # Lowest price value for a time period Low = Dollars -# Virtual size in vbytes (weight / 4, rounded up) -VSize = int Minute10 = int Minute30 = int Month1 = int @@ -288,6 +288,8 @@ class AddrValidation(TypedDict): iswitness: Whether this is a witness address witness_version: Witness version (0 for P2WPKH/P2WSH, 1 for P2TR) witness_program: Witness program in hex + error_locations: Error locations (empty array for most errors) + error: Error message for invalid addresses """ isvalid: bool address: Optional[str] @@ -296,6 +298,8 @@ class AddrValidation(TypedDict): iswitness: Optional[bool] witness_version: Optional[int] witness_program: Optional[str] + error_locations: Optional[List[int]] + error: Optional[str] class BlockCountParam(TypedDict): """ @@ -312,10 +316,12 @@ class BlockPool(TypedDict): id: Unique pool identifier name: Pool name slug: URL-friendly pool identifier + minerNames: Alternative miner names (if identified) """ id: int name: str slug: PoolSlug + minerNames: Optional[str] class BlockExtras(TypedDict): """ @@ -379,10 +385,14 @@ class BlockExtras(TypedDict): class BlockFeesEntry(TypedDict): """ A single block fees data point. + + Attributes: + USD: BTC/USD price at that height """ avgHeight: Height timestamp: Timestamp avgFees: Sats + USD: Dollars class BlockHashParam(TypedDict): hash: BlockHash @@ -412,32 +422,30 @@ class BlockInfo(TypedDict): Attributes: id: Block hash height: Block height - version: Block version, used for soft fork signaling - previousblockhash: Previous block hash - merkle_root: Merkle root of the transaction tree - time: Block timestamp as claimed by the miner (Unix time) - bits: Compact target (bits) - nonce: Nonce used to produce a valid block hash + version: Block version timestamp: Block timestamp (Unix time) - tx_count: Number of transactions in the block + tx_count: Number of transactions size: Block size in bytes weight: Block weight in weight units + merkle_root: Merkle root of the transaction tree + previousblockhash: Previous block hash mediantime: Median time of the last 11 blocks + nonce: Nonce + bits: Compact target (bits) difficulty: Block difficulty """ id: BlockHash height: Height version: int - previousblockhash: BlockHash - merkle_root: str - time: int - bits: int - nonce: int timestamp: Timestamp tx_count: int size: int weight: Weight + merkle_root: str + previousblockhash: BlockHash mediantime: Timestamp + nonce: int + bits: int difficulty: float class BlockInfoV1(TypedDict): @@ -447,59 +455,63 @@ class BlockInfoV1(TypedDict): Attributes: id: Block hash height: Block height - version: Block version, used for soft fork signaling - previousblockhash: Previous block hash - merkle_root: Merkle root of the transaction tree - time: Block timestamp as claimed by the miner (Unix time) - bits: Compact target (bits) - nonce: Nonce used to produce a valid block hash + version: Block version timestamp: Block timestamp (Unix time) - tx_count: Number of transactions in the block + tx_count: Number of transactions size: Block size in bytes weight: Block weight in weight units + merkle_root: Merkle root of the transaction tree + previousblockhash: Previous block hash mediantime: Median time of the last 11 blocks + nonce: Nonce + bits: Compact target (bits) difficulty: Block difficulty + stale: Whether this block is stale (orphaned) extras: Extended block data """ id: BlockHash height: Height version: int - previousblockhash: BlockHash - merkle_root: str - time: int - bits: int - nonce: int timestamp: Timestamp tx_count: int size: int weight: Weight + merkle_root: str + previousblockhash: BlockHash mediantime: Timestamp + nonce: int + bits: int difficulty: float + stale: bool extras: BlockExtras class BlockRewardsEntry(TypedDict): """ A single block rewards data point. + + Attributes: + USD: BTC/USD price at that height """ - avgHeight: int - timestamp: int - avgRewards: int + avgHeight: Height + timestamp: Timestamp + avgRewards: Sats + USD: Dollars class BlockSizeEntry(TypedDict): """ A single block size data point. """ - avgHeight: int - timestamp: int + avgHeight: Height + timestamp: Timestamp avgSize: int class BlockWeightEntry(TypedDict): """ A single block weight data point. """ - avgHeight: int - timestamp: int - avgWeight: int + avgHeight: Height + timestamp: Timestamp + avgWeight: Weight class BlockSizesWeights(TypedDict): """ @@ -515,7 +527,7 @@ class BlockStatus(TypedDict): Attributes: in_best_chain: Whether this block is in the best chain height: Block height (only if in best chain) - next_best: Hash of the next block in the best chain (only if in best chain and not tip) + next_best: Hash of the next block in the best chain (null if tip) """ in_best_chain: bool height: Union[Height, None] @@ -561,6 +573,11 @@ class CostBasisQuery(TypedDict): class CpfpEntry(TypedDict): """ A transaction in a CPFP relationship + + Attributes: + txid: Transaction ID + weight: Transaction weight + fee: Transaction fee (sats) """ txid: Txid weight: Weight @@ -569,10 +586,21 @@ class CpfpEntry(TypedDict): class CpfpInfo(TypedDict): """ CPFP (Child Pays For Parent) information for a transaction + + Attributes: + ancestors: Ancestor transactions in the CPFP chain + bestDescendant: Best (highest fee rate) descendant, if any + descendants: Descendant transactions in the CPFP chain + effectiveFeePerVsize: Effective fee rate considering CPFP relationships (sat/vB) + fee: Transaction fee (sats) + adjustedVsize: Adjusted virtual size (accounting for sigops) """ ancestors: List[CpfpEntry] + bestDescendant: Union[CpfpEntry, None] descendants: List[CpfpEntry] effectiveFeePerVsize: FeeRate + fee: Sats + adjustedVsize: VSize class DataRangeFormat(TypedDict): """ @@ -628,14 +656,16 @@ class DifficultyAdjustment(TypedDict): Attributes: progressPercent: Progress through current difficulty epoch (0-100%) difficultyChange: Estimated difficulty change at next retarget (%) - estimatedRetargetDate: Estimated Unix timestamp of next retarget + estimatedRetargetDate: Estimated timestamp of next retarget (milliseconds) remainingBlocks: Blocks remaining until retarget - remainingTime: Estimated seconds until retarget + remainingTime: Estimated time until retarget (milliseconds) previousRetarget: Previous difficulty adjustment (%) + previousTime: Timestamp of most recent retarget (seconds) nextRetargetHeight: Height of next retarget - timeAvg: Average block time in current epoch (seconds) - adjustedTimeAvg: Time-adjusted average (accounting for timestamp manipulation) + timeAvg: Average block time in current epoch (milliseconds) + adjustedTimeAvg: Time-adjusted average (milliseconds) timeOffset: Time offset from expected schedule (seconds) + expectedBlocks: Expected blocks based on wall clock time since epoch start """ progressPercent: float difficultyChange: float @@ -643,10 +673,12 @@ class DifficultyAdjustment(TypedDict): remainingBlocks: int remainingTime: int previousRetarget: float + previousTime: Timestamp nextRetargetHeight: Height timeAvg: int adjustedTimeAvg: int timeOffset: int + expectedBlocks: float class DifficultyAdjustmentEntry(TypedDict): """ @@ -660,16 +692,18 @@ class DifficultyAdjustmentEntry(TypedDict): class DifficultyEntry(TypedDict): """ - A single difficulty data point. + A single difficulty data point in the hashrate summary. Attributes: - timestamp: Unix timestamp of the difficulty adjustment. - difficulty: Difficulty value. - height: Block height of the adjustment. + time: Unix timestamp of the difficulty adjustment + height: Block height of the adjustment + difficulty: Difficulty value + adjustment: Adjustment ratio (new/previous, e.g. 1.068 = +6.8%) """ - timestamp: Timestamp - difficulty: float + time: Timestamp height: Height + difficulty: float + adjustment: float class DiskUsage(TypedDict): """ @@ -974,19 +1008,21 @@ class PoolDetailInfo(TypedDict): Pool information for detail view Attributes: - id: Unique pool identifier + id: Pool identifier name: Pool name link: Pool website URL - addrs: Known payout addresses + addresses: Known payout addresses regexes: Coinbase tag patterns (regexes) slug: URL-friendly pool identifier + unique_id: Unique pool identifier """ id: int name: str link: str - addrs: List[str] + addresses: List[str] regexes: List[str] slug: PoolSlug + unique_id: int class PoolDetail(TypedDict): """ @@ -1053,6 +1089,7 @@ class PoolStats(TypedDict): emptyBlocks: Number of empty blocks mined slug: URL-friendly pool identifier share: Pool's share of total blocks (0.0 - 1.0) + poolUniqueId: Unique pool identifier """ poolId: int name: str @@ -1062,6 +1099,7 @@ class PoolStats(TypedDict): emptyBlocks: int slug: PoolSlug share: float + poolUniqueId: int class PoolsSummary(TypedDict): """ @@ -1071,10 +1109,14 @@ class PoolsSummary(TypedDict): pools: List of pools sorted by block count descending blockCount: Total blocks in the time period lastEstimatedHashrate: Estimated network hashrate (hashes per second) + lastEstimatedHashrate3d: Estimated network hashrate over last 3 days + lastEstimatedHashrate1w: Estimated network hashrate over last 1 week """ pools: List[PoolStats] blockCount: int lastEstimatedHashrate: int + lastEstimatedHashrate3d: int + lastEstimatedHashrate1w: int class Prices(TypedDict): """ @@ -1235,21 +1277,26 @@ class TxIn(TypedDict): Attributes: txid: Transaction ID of the output being spent + vout: Output index being spent prevout: Information about the previous output being spent - scriptsig: Signature script (for non-SegWit inputs) + scriptsig: Signature script (hex, for non-SegWit inputs) scriptsig_asm: Signature script in assembly format + witness: Witness data (hex-encoded stack items, present for SegWit inputs) is_coinbase: Whether this input is a coinbase (block reward) input sequence: Input sequence number - inner_redeemscript_asm: Inner redeemscript in assembly format (for P2SH-wrapped SegWit) + inner_redeemscript_asm: Inner redeemscript in assembly (for P2SH-wrapped SegWit: scriptsig + witness both present) + inner_witnessscript_asm: Inner witnessscript in assembly (for P2WSH: last witness item decoded as script) """ txid: Txid vout: Vout prevout: Union[TxOut, None] scriptsig: str scriptsig_asm: str + witness: List[str] is_coinbase: bool sequence: int - inner_redeemscript_asm: Optional[str] + inner_redeemscript_asm: str + inner_witnessscript_asm: str class TxStatus(TypedDict): """ @@ -1271,23 +1318,23 @@ class Transaction(TypedDict): Transaction information compatible with mempool.space API format Attributes: + vin: Transaction inputs + vout: Transaction outputs size: Transaction size in bytes weight: Transaction weight sigops: Number of signature operations fee: Transaction fee in satoshis - vin: Transaction inputs - vout: Transaction outputs """ index: Union[TxIndex, None] txid: Txid version: TxVersion locktime: RawLockTime + vin: List[TxIn] + vout: List[TxOut] size: int weight: Weight sigops: int fee: Sats - vin: List[TxIn] - vout: List[TxOut] status: TxStatus class TxOutspend(TypedDict): @@ -4935,6 +4982,8 @@ class SeriesTree_Pools_Minor: self.parasite: BlocksDominancePattern = BlocksDominancePattern(client, 'parasite') self.redrockpool: BlocksDominancePattern = BlocksDominancePattern(client, 'redrockpool') self.est3lar: BlocksDominancePattern = BlocksDominancePattern(client, 'est3lar') + self.braiinssolo: BlocksDominancePattern = BlocksDominancePattern(client, 'braiinssolo') + self.solopool: BlocksDominancePattern = BlocksDominancePattern(client, 'solopool') class SeriesTree_Pools: """Series tree node.""" @@ -5952,6 +6001,7 @@ class BrkClient(BrkClientBase): "bixin": "Bixin", "blockfills": "BlockFills", "braiinspool": "Braiins Pool", + "braiinssolo": "Braiins Solo", "bravomining": "Bravo Mining", "btcc": "BTCC", "btccom": "BTC.com", @@ -6063,6 +6113,7 @@ class BrkClient(BrkClientBase): "sigmapoolcom": "Sigmapool.com", "simplecoinus": "simplecoin.us", "solock": "Solo CK", + "solopool": "SoloPool.com", "spiderpool": "SpiderPool", "stminingcorp": "ST Mining Corp", "tangpool": "Tangpool",