diff --git a/crates/brk_bindgen/src/generators/javascript/api.rs b/crates/brk_bindgen/src/generators/javascript/api.rs index cc927189c..153f8229d 100644 --- a/crates/brk_bindgen/src/generators/javascript/api.rs +++ b/crates/brk_bindgen/src/generators/javascript/api.rs @@ -69,16 +69,32 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { .unwrap(); } + writeln!( + output, + " * @param {{{{ signal?: AbortSignal, onUpdate?: (value: {}) => void }}}} [options]", + return_type + ) + .unwrap(); writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap(); writeln!(output, " */").unwrap(); let params = build_method_params(endpoint); - writeln!(output, " async {}({}) {{", method_name, params).unwrap(); + let params_with_opts = if params.is_empty() { + "{ signal, onUpdate } = {}".to_string() + } else { + format!("{}, {{ signal, onUpdate }} = {{}}", params) + }; + writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap(); let path = build_path_template(&endpoint.path, &endpoint.path_params); if endpoint.query_params.is_empty() { - writeln!(output, " return this.getJson(`{}`);", path).unwrap(); + writeln!( + output, + " return this.getJson(`{}`, {{ signal, onUpdate }});", + path + ) + .unwrap(); } else { writeln!(output, " const params = new URLSearchParams();").unwrap(); for param in &endpoint.query_params { @@ -109,11 +125,11 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { if endpoint.supports_csv { writeln!(output, " if (format === 'csv') {{").unwrap(); - writeln!(output, " return this.getText(path);").unwrap(); + writeln!(output, " return this.getText(path, {{ signal }});").unwrap(); writeln!(output, " }}").unwrap(); - writeln!(output, " return this.getJson(path);").unwrap(); + writeln!(output, " return this.getJson(path, {{ signal, onUpdate }});").unwrap(); } else { - writeln!(output, " return this.getJson(path);").unwrap(); + writeln!(output, " return this.getJson(path, {{ signal, onUpdate }});").unwrap(); } } diff --git a/crates/brk_bindgen/src/generators/javascript/client.rs b/crates/brk_bindgen/src/generators/javascript/client.rs index ab3e5a293..20091cf8c 100644 --- a/crates/brk_bindgen/src/generators/javascript/client.rs +++ b/crates/brk_bindgen/src/generators/javascript/client.rs @@ -404,11 +404,14 @@ class BrkClientBase {{ /** * @param {{string}} path + * @param {{{{ signal?: AbortSignal }}}} [options] * @returns {{Promise}} */ - async get(path) {{ + async get(path, {{ signal }} = {{}}) {{ const url = `${{this.baseUrl}}${{path}}`; - const res = await fetch(url, {{ signal: AbortSignal.timeout(this.timeout) }}); + const signals = [AbortSignal.timeout(this.timeout)]; + if (signal) signals.push(signal); + const res = await fetch(url, {{ signal: AbortSignal.any(signals) }}); if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status); return res; }} @@ -417,10 +420,10 @@ class BrkClientBase {{ * Make a GET request - races cache vs network, first to resolve calls onUpdate * @template T * @param {{string}} path - * @param {{(value: T) => void}} [onUpdate] - Called when data is available (may be called twice: cache then network) + * @param {{{{ onUpdate?: (value: T) => void, signal?: AbortSignal }}}} [options] * @returns {{Promise}} */ - async getJson(path, onUpdate) {{ + async getJson(path, {{ onUpdate, signal }} = {{}}) {{ const url = `${{this.baseUrl}}${{path}}`; const cache = this._cache ?? await this._cachePromise; @@ -440,7 +443,7 @@ class BrkClientBase {{ return json; }}); - const networkPromise = this.get(path).then(async (res) => {{ + const networkPromise = this.get(path, {{ signal }}).then(async (res) => {{ const cloned = res.clone(); const json = _addCamelGetters(await res.json()); // Skip update if ETag matches and cache already delivered @@ -472,10 +475,11 @@ class BrkClientBase {{ /** * Make a GET request and return raw text (for CSV responses) * @param {{string}} path + * @param {{{{ signal?: AbortSignal }}}} [options] * @returns {{Promise}} */ - async getText(path) {{ - const res = await this.get(path); + async getText(path, {{ signal }} = {{}}) {{ + const res = await this.get(path, {{ signal }}); return res.text(); }} @@ -488,7 +492,7 @@ class BrkClientBase {{ */ async _fetchSeriesData(path, onUpdate) {{ const wrappedOnUpdate = onUpdate ? (/** @type {{SeriesData}} */ raw) => onUpdate(_wrapSeriesData(raw)) : undefined; - const raw = await this.getJson(path, wrappedOnUpdate); + const raw = await this.getJson(path, {{ onUpdate: wrappedOnUpdate }}); return _wrapSeriesData(raw); }} }} diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index ab04b2275..723672207 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -8348,7 +8348,7 @@ impl BrkClient { /// Block header /// - /// Returns the hex-encoded block header. + /// Returns the hex-encoded 80-byte block header. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-header)* /// @@ -8839,7 +8839,7 @@ impl BrkClient { /// CPFP info /// - /// Returns ancestors and descendants for a CPFP transaction. + /// Returns ancestors and descendants for a CPFP (Child Pays For Parent) transaction, including the effective fee rate of the package. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)* /// @@ -8909,7 +8909,7 @@ impl BrkClient { /// Block fee rates /// - /// Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + /// Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)* /// @@ -8920,7 +8920,7 @@ impl BrkClient { /// Block fees /// - /// Get average block fees for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + /// Get average total fees per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)* /// @@ -8931,7 +8931,7 @@ impl BrkClient { /// Block rewards /// - /// Get average block rewards (coinbase = subsidy + fees) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + /// Get average coinbase reward (subsidy + fees) per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)* /// @@ -8942,7 +8942,7 @@ impl BrkClient { /// Block sizes and weights /// - /// Get average block sizes and weights for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + /// Get average block sizes and weights for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)* /// @@ -8975,7 +8975,7 @@ impl BrkClient { /// Difficulty adjustments /// - /// Get historical difficulty adjustments for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y. + /// Get historical difficulty adjustments for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)* /// @@ -9008,7 +9008,7 @@ impl BrkClient { /// All pools hashrate /// - /// Get hashrate data for all mining pools for a time period. Valid periods: 1m, 3m, 6m, 1y, 2y, 3y + /// Get hashrate data for all mining pools for a time period. Valid periods: `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)* /// @@ -9019,7 +9019,7 @@ impl BrkClient { /// Network hashrate /// - /// Get network hashrate and difficulty data for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + /// Get network hashrate and difficulty data for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)* /// @@ -9085,7 +9085,7 @@ impl BrkClient { /// Mining pool statistics /// - /// Get mining pool statistics for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + /// Get mining pool statistics for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)* /// @@ -9129,7 +9129,7 @@ impl BrkClient { /// Validate address /// - /// Validate a Bitcoin address and get information about its type and scriptPubKey. + /// Validate a Bitcoin address and get information about its type and scriptPubKey. Returns `isvalid: false` with an error message for invalid addresses. /// /// *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)* /// diff --git a/crates/brk_query/src/impl/addr.rs b/crates/brk_query/src/impl/addr.rs index dc4369bcf..835fedfbf 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, BlockHash, Height, OutputType, Timestamp, Transaction, TxIndex, TxStatus, - Txid, TypeIndex, Unit, Utxo, Vout, + AnyAddrDataIndexEnum, BlockHash, Dollars, Height, OutputType, Timestamp, Transaction, TxIndex, + TxStatus, Txid, TypeIndex, Unit, Utxo, Vout, }; use vecdb::{ReadableVec, VecIndex}; @@ -69,8 +69,14 @@ impl Query { .into(), }; + let realized_price = match &any_addr_index.to_enum() { + AnyAddrDataIndexEnum::Funded(_) => addr_data.realized_price().to_dollars(), + AnyAddrDataIndexEnum::Empty(_) => Dollars::default(), + }; + Ok(AddrStats { addr, + addr_type, chain_stats: AddrChainStats { type_index, funded_txo_count: addr_data.funded_txo_count, @@ -78,6 +84,7 @@ impl Query { spent_txo_count: addr_data.spent_txo_count, spent_txo_sum: addr_data.sent, tx_count: addr_data.tx_count, + realized_price, }, mempool_stats: self.mempool().map(|mempool| { mempool diff --git a/crates/brk_query/src/impl/price.rs b/crates/brk_query/src/impl/price.rs index 9a7d30972..f67dda836 100644 --- a/crates/brk_query/src/impl/price.rs +++ b/crates/brk_query/src/impl/price.rs @@ -35,7 +35,7 @@ impl Query { 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, + time: h4.to_timestamp(), usd: Dollars::from(cents.flatten().unwrap_or_default()), }]) } @@ -52,7 +52,7 @@ impl Query { .enumerate() .filter_map(|(i, cents)| { Some(HistoricalPriceEntry { - time: usize::from(Hour4::from(i).to_timestamp()) as u64, + time: Hour4::from(i).to_timestamp(), usd: Dollars::from(cents?), }) }) diff --git a/crates/brk_server/src/api/mempool_space/addrs.rs b/crates/brk_server/src/api/mempool_space/addrs.rs index 341e86778..c45709dac 100644 --- a/crates/brk_server/src/api/mempool_space/addrs.rs +++ b/crates/brk_server/src/api/mempool_space/addrs.rs @@ -147,9 +147,10 @@ impl AddrRoutes for ApiRouter { .id("validate_address") .addrs_tag() .summary("Validate address") - .description("Validate a Bitcoin address and get information about its type and scriptPubKey.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)*") + .description("Validate a Bitcoin address and get information about its type and scriptPubKey. Returns `isvalid: false` with an error message for invalid addresses.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)*") .json_response::() .not_modified() + .server_error() ), ) } diff --git a/crates/brk_server/src/api/mempool_space/blocks.rs b/crates/brk_server/src/api/mempool_space/blocks.rs index f8e1379ad..21aa861bb 100644 --- a/crates/brk_server/src/api/mempool_space/blocks.rs +++ b/crates/brk_server/src/api/mempool_space/blocks.rs @@ -62,6 +62,7 @@ impl BlockRoutes for ApiRouter { .description("Returns block details with extras by hash.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-v1)*") .json_response::() .not_modified() + .bad_request() .not_found() .server_error() }, @@ -78,9 +79,10 @@ impl BlockRoutes for ApiRouter { op.id("get_block_header") .blocks_tag() .summary("Block header") - .description("Returns the hex-encoded block header.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-header)*") + .description("Returns the hex-encoded 80-byte block header.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-header)*") .text_response() .not_modified() + .bad_request() .not_found() .server_error() }, diff --git a/crates/brk_server/src/api/mempool_space/mining.rs b/crates/brk_server/src/api/mempool_space/mining.rs index 17d83d7a8..8ab68beac 100644 --- a/crates/brk_server/src/api/mempool_space/mining.rs +++ b/crates/brk_server/src/api/mempool_space/mining.rs @@ -55,9 +55,10 @@ impl MiningRoutes for ApiRouter { op.id("get_pool_stats") .mining_tag() .summary("Mining pool statistics") - .description("Get mining pool statistics for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)*") + .description("Get mining pool statistics for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)*") .json_response::() .not_modified() + .bad_request() .server_error() }, ), @@ -107,9 +108,10 @@ impl MiningRoutes for ApiRouter { op.id("get_pools_hashrate_by_period") .mining_tag() .summary("All pools hashrate") - .description("Get hashrate data for all mining pools for a time period. Valid periods: 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)*") + .description("Get hashrate data for all mining pools for a time period. Valid periods: `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)*") .json_response::>() .not_modified() + .bad_request() .server_error() }, ), @@ -195,9 +197,10 @@ impl MiningRoutes for ApiRouter { op.id("get_hashrate_by_period") .mining_tag() .summary("Network hashrate") - .description("Get network hashrate and difficulty data for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)*") + .description("Get network hashrate and difficulty data for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)*") .json_response::() .not_modified() + .bad_request() .server_error() }, ), @@ -229,9 +232,10 @@ impl MiningRoutes for ApiRouter { op.id("get_difficulty_adjustments_by_period") .mining_tag() .summary("Difficulty adjustments") - .description("Get historical difficulty adjustments for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)*") + .description("Get historical difficulty adjustments for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)*") .json_response::>() .not_modified() + .bad_request() .server_error() }, ), @@ -249,6 +253,7 @@ impl MiningRoutes for ApiRouter { .description("Get mining reward statistics for the last N blocks including total rewards, fees, and transaction count.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-reward-stats)*") .json_response::() .not_modified() + .bad_request() .server_error() }, ), @@ -263,9 +268,10 @@ impl MiningRoutes for ApiRouter { op.id("get_block_fees") .mining_tag() .summary("Block fees") - .description("Get average block fees for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)*") + .description("Get average total fees per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)*") .json_response::>() .not_modified() + .bad_request() .server_error() }, ), @@ -280,9 +286,10 @@ impl MiningRoutes for ApiRouter { op.id("get_block_rewards") .mining_tag() .summary("Block rewards") - .description("Get average block rewards (coinbase = subsidy + fees) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)*") + .description("Get average coinbase reward (subsidy + fees) per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)*") .json_response::>() .not_modified() + .bad_request() .server_error() }, ), @@ -297,9 +304,10 @@ impl MiningRoutes for ApiRouter { op.id("get_block_fee_rates") .mining_tag() .summary("Block fee rates") - .description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*") + .description("Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)*") .json_response::>() .not_modified() + .bad_request() .server_error() }, ), @@ -314,9 +322,10 @@ impl MiningRoutes for ApiRouter { op.id("get_block_sizes_weights") .mining_tag() .summary("Block sizes and weights") - .description("Get average block sizes and weights for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)*") + .description("Get average block sizes and weights for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)*") .json_response::() .not_modified() + .bad_request() .server_error() }, ), diff --git a/crates/brk_server/src/api/mempool_space/transactions.rs b/crates/brk_server/src/api/mempool_space/transactions.rs index afd3e20b9..613720628 100644 --- a/crates/brk_server/src/api/mempool_space/transactions.rs +++ b/crates/brk_server/src/api/mempool_space/transactions.rs @@ -32,9 +32,10 @@ impl TxRoutes for ApiRouter { .id("get_cpfp") .transactions_tag() .summary("CPFP info") - .description("Returns ancestors and descendants for a CPFP transaction.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)*") + .description("Returns ancestors and descendants for a CPFP (Child Pays For Parent) transaction, including the effective fee rate of the package.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)*") .json_response::() .not_modified() + .bad_request() .not_found() .server_error(), ), diff --git a/crates/brk_server/src/params/addr_param.rs b/crates/brk_server/src/params/addr_param.rs index 5ec38e391..67e24cdd3 100644 --- a/crates/brk_server/src/params/addr_param.rs +++ b/crates/brk_server/src/params/addr_param.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use brk_types::Addr; +/// Bitcoin address path parameter #[derive(Deserialize, JsonSchema)] pub struct AddrParam { #[serde(rename = "address")] diff --git a/crates/brk_server/src/params/block_count_param.rs b/crates/brk_server/src/params/block_count_param.rs index de2989690..eb7d6dd46 100644 --- a/crates/brk_server/src/params/block_count_param.rs +++ b/crates/brk_server/src/params/block_count_param.rs @@ -1,6 +1,7 @@ use schemars::JsonSchema; use serde::Deserialize; +/// Block count path parameter #[derive(Deserialize, JsonSchema)] pub struct BlockCountParam { /// Number of recent blocks to include diff --git a/crates/brk_server/src/params/blockhash_param.rs b/crates/brk_server/src/params/blockhash_param.rs index 92fcbc7f8..9bebf8a3a 100644 --- a/crates/brk_server/src/params/blockhash_param.rs +++ b/crates/brk_server/src/params/blockhash_param.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use brk_types::BlockHash; +/// Block hash path parameter #[derive(Deserialize, JsonSchema)] pub struct BlockHashParam { pub hash: BlockHash, diff --git a/crates/brk_server/src/params/blockhash_start_index.rs b/crates/brk_server/src/params/blockhash_start_index.rs index 742e584e7..40583a012 100644 --- a/crates/brk_server/src/params/blockhash_start_index.rs +++ b/crates/brk_server/src/params/blockhash_start_index.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use brk_types::{BlockHash, TxIndex}; +/// Block hash + starting transaction index path parameters #[derive(Deserialize, JsonSchema)] pub struct BlockHashStartIndex { /// Bitcoin block hash diff --git a/crates/brk_server/src/params/blockhash_tx_index.rs b/crates/brk_server/src/params/blockhash_tx_index.rs index 574cc54ae..78dfef8fb 100644 --- a/crates/brk_server/src/params/blockhash_tx_index.rs +++ b/crates/brk_server/src/params/blockhash_tx_index.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use brk_types::{BlockHash, TxIndex}; +/// Block hash + transaction index path parameters #[derive(Deserialize, JsonSchema)] pub struct BlockHashTxIndex { /// Bitcoin block hash diff --git a/crates/brk_server/src/params/height_param.rs b/crates/brk_server/src/params/height_param.rs index 2c153cd8b..c6025df8e 100644 --- a/crates/brk_server/src/params/height_param.rs +++ b/crates/brk_server/src/params/height_param.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use brk_types::Height; +/// Block height path parameter #[derive(Deserialize, JsonSchema)] pub struct HeightParam { pub height: Height, diff --git a/crates/brk_server/src/params/pool_slug_param.rs b/crates/brk_server/src/params/pool_slug_param.rs index 8aff97a02..ab201d67b 100644 --- a/crates/brk_server/src/params/pool_slug_param.rs +++ b/crates/brk_server/src/params/pool_slug_param.rs @@ -3,11 +3,13 @@ use serde::Deserialize; use brk_types::{Height, PoolSlug}; +/// Mining pool slug path parameter #[derive(Deserialize, JsonSchema)] pub struct PoolSlugParam { pub slug: PoolSlug, } +/// Mining pool slug + block height path parameters #[derive(Deserialize, JsonSchema)] pub struct PoolSlugAndHeightParam { pub slug: PoolSlug, diff --git a/crates/brk_server/src/params/time_period_param.rs b/crates/brk_server/src/params/time_period_param.rs index 7df989e8a..e41f0901a 100644 --- a/crates/brk_server/src/params/time_period_param.rs +++ b/crates/brk_server/src/params/time_period_param.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use brk_types::TimePeriod; +/// Time period path parameter (24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y) #[derive(Deserialize, JsonSchema)] pub struct TimePeriodParam { #[schemars(example = &"24h")] diff --git a/crates/brk_server/src/params/timestamp_param.rs b/crates/brk_server/src/params/timestamp_param.rs index 3ce70cf0f..6df85c9cb 100644 --- a/crates/brk_server/src/params/timestamp_param.rs +++ b/crates/brk_server/src/params/timestamp_param.rs @@ -3,11 +3,13 @@ use serde::Deserialize; use brk_types::Timestamp; +/// UNIX timestamp path parameter #[derive(Deserialize, JsonSchema)] pub struct TimestampParam { pub timestamp: Timestamp, } +/// Optional UNIX timestamp query parameter #[derive(Deserialize, JsonSchema)] pub struct OptionalTimestampParam { pub timestamp: Option, diff --git a/crates/brk_server/src/params/txid_param.rs b/crates/brk_server/src/params/txid_param.rs index 92fa5c0fc..25adabab9 100644 --- a/crates/brk_server/src/params/txid_param.rs +++ b/crates/brk_server/src/params/txid_param.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use brk_types::Txid; +/// Transaction ID path parameter #[derive(Deserialize, JsonSchema)] pub struct TxidParam { pub txid: Txid, diff --git a/crates/brk_types/src/addr_chain_stats.rs b/crates/brk_types/src/addr_chain_stats.rs index 05a45fdb9..614d0f1b4 100644 --- a/crates/brk_types/src/addr_chain_stats.rs +++ b/crates/brk_types/src/addr_chain_stats.rs @@ -1,11 +1,11 @@ -use crate::{Sats, TypeIndex}; +use crate::{Dollars, Sats, TypeIndex}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// Address statistics on the blockchain (confirmed transactions only) /// /// Based on mempool.space's format with type_index extension. -#[derive(Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct AddrChainStats { /// Total number of transaction outputs that funded this address #[schemars(example = 5)] @@ -30,4 +30,7 @@ pub struct AddrChainStats { /// Index of this address within its type on the blockchain #[schemars(example = TypeIndex::new(0))] pub type_index: TypeIndex, + + /// Realized price (average cost basis) in USD + pub realized_price: Dollars, } diff --git a/crates/brk_types/src/addr_stats.rs b/crates/brk_types/src/addr_stats.rs index 0f9a3ebe7..17355a238 100644 --- a/crates/brk_types/src/addr_stats.rs +++ b/crates/brk_types/src/addr_stats.rs @@ -1,4 +1,4 @@ -use crate::{Addr, AddrChainStats, AddrMempoolStats}; +use crate::{Addr, AddrChainStats, AddrMempoolStats, OutputType}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -12,6 +12,9 @@ pub struct AddrStats { #[serde(rename = "address")] pub addr: Addr, + /// Address type (p2pkh, p2sh, v0_p2wpkh, v0_p2wsh, v1_p2tr, etc.) + pub addr_type: OutputType, + /// Statistics for confirmed transactions on the blockchain pub chain_stats: AddrChainStats, diff --git a/crates/brk_types/src/addr_validation.rs b/crates/brk_types/src/addr_validation.rs index bdf1b7bba..bdda500b9 100644 --- a/crates/brk_types/src/addr_validation.rs +++ b/crates/brk_types/src/addr_validation.rs @@ -8,6 +8,7 @@ use crate::{AddrBytes, OutputType}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct AddrValidation { /// Whether the address is valid + #[schemars(example = true)] pub isvalid: bool, /// The validated address diff --git a/crates/brk_types/src/block_extras.rs b/crates/brk_types/src/block_extras.rs index a79cc9c01..d5b69d26c 100644 --- a/crates/brk_types/src/block_extras.rs +++ b/crates/brk_types/src/block_extras.rs @@ -54,14 +54,17 @@ pub struct BlockExtras { /// Average transaction size in bytes #[serde(rename = "avgTxSize")] + #[schemars(example = 534.5)] pub avg_tx_size: f64, /// Total number of inputs (excluding coinbase) #[serde(rename = "totalInputs")] + #[schemars(example = 5000)] pub total_inputs: u64, /// Total number of outputs #[serde(rename = "totalOutputs")] + #[schemars(example = 7500)] pub total_outputs: u64, /// Total output amount in satoshis @@ -78,10 +81,12 @@ pub struct BlockExtras { /// Number of segwit transactions #[serde(rename = "segwitTotalTxs")] + #[schemars(example = 2000)] pub segwit_total_txs: u32, /// Total size of segwit transactions in bytes #[serde(rename = "segwitTotalSize")] + #[schemars(example = 1200000)] pub segwit_total_size: u64, /// Total weight of segwit transactions @@ -95,10 +100,12 @@ pub struct BlockExtras { /// Note: intentionally differs from utxo_set_size diff which excludes unspendable outputs. /// Matches mempool.space/bitcoin-cli behavior. #[serde(rename = "utxoSetChange")] + #[schemars(example = 2500)] pub utxo_set_change: i64, /// Total spendable UTXO set size at this height (excludes OP_RETURN and other unspendable outputs) #[serde(rename = "utxoSetSize")] + #[schemars(example = 170_000_000)] pub utxo_set_size: u64, /// Total input amount in satoshis @@ -107,6 +114,7 @@ pub struct BlockExtras { /// Virtual size in vbytes #[serde(rename = "virtualSize")] + #[schemars(example = 998000.25)] pub virtual_size: f64, /// Timestamp when the block was first seen (always null, not yet supported) diff --git a/crates/brk_types/src/block_info.rs b/crates/brk_types/src/block_info.rs index 2356a2a98..f4aa483c5 100644 --- a/crates/brk_types/src/block_info.rs +++ b/crates/brk_types/src/block_info.rs @@ -11,20 +11,29 @@ pub struct BlockInfo { /// Block height pub height: Height, /// Block version + #[schemars(example = 536870912)] pub version: u32, /// Block timestamp (Unix time) pub timestamp: Timestamp, /// Compact target (bits) + #[schemars(example = 386089497)] pub bits: u32, /// Nonce + #[schemars(example = 2083236893)] pub nonce: u32, /// Block difficulty + #[schemars(example = 110_451_832_649_830.94)] pub difficulty: f64, /// Merkle root of the transaction tree + #[schemars( + example = &"4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b" + )] pub merkle_root: String, /// Number of transactions + #[schemars(example = 2500)] pub tx_count: u32, /// Block size in bytes + #[schemars(example = 1580000)] pub size: u64, /// Block weight in weight units pub weight: Weight, diff --git a/crates/brk_types/src/block_pool.rs b/crates/brk_types/src/block_pool.rs index f35d25af4..a34973ae1 100644 --- a/crates/brk_types/src/block_pool.rs +++ b/crates/brk_types/src/block_pool.rs @@ -8,9 +8,11 @@ use crate::PoolSlug; #[serde(rename_all = "camelCase")] pub struct BlockPool { /// Unique pool identifier + #[schemars(example = 44)] pub id: u8, /// Pool name + #[schemars(example = &"Foundry USA")] pub name: String, /// URL-friendly pool identifier diff --git a/crates/brk_types/src/block_status.rs b/crates/brk_types/src/block_status.rs index 1d59c8d50..2c8264c99 100644 --- a/crates/brk_types/src/block_status.rs +++ b/crates/brk_types/src/block_status.rs @@ -7,6 +7,7 @@ use crate::{BlockHash, Height}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct BlockStatus { /// Whether this block is in the best chain + #[schemars(example = true)] pub in_best_chain: bool, /// Block height (only if in best chain) diff --git a/crates/brk_types/src/block_timestamp.rs b/crates/brk_types/src/block_timestamp.rs index 9c9b312c9..7ebb788ca 100644 --- a/crates/brk_types/src/block_timestamp.rs +++ b/crates/brk_types/src/block_timestamp.rs @@ -13,5 +13,6 @@ pub struct BlockTimestamp { pub hash: BlockHash, /// Block timestamp in ISO 8601 format + #[schemars(example = &"2024-04-20T00:00:00.000Z")] pub timestamp: String, } diff --git a/crates/brk_types/src/difficulty_adjustment_entry.rs b/crates/brk_types/src/difficulty_adjustment_entry.rs index eef1d55ab..8f982510e 100644 --- a/crates/brk_types/src/difficulty_adjustment_entry.rs +++ b/crates/brk_types/src/difficulty_adjustment_entry.rs @@ -13,8 +13,10 @@ pub struct DifficultyAdjustmentEntry { /// Block height of the adjustment pub height: Height, /// Difficulty value + #[schemars(example = 110_451_832_649_830.94)] pub difficulty: f64, /// Adjustment ratio (new/previous, e.g. 1.068 = +6.8%) + #[schemars(example = 1.068)] pub change_percent: f64, } diff --git a/crates/brk_types/src/difficulty_entry.rs b/crates/brk_types/src/difficulty_entry.rs index 06518cfb7..556422563 100644 --- a/crates/brk_types/src/difficulty_entry.rs +++ b/crates/brk_types/src/difficulty_entry.rs @@ -11,7 +11,9 @@ pub struct DifficultyEntry { /// Block height of the adjustment pub height: Height, /// Difficulty value + #[schemars(example = 110_451_832_649_830.94)] pub difficulty: f64, /// Adjustment ratio (new/previous, e.g. 1.068 = +6.8%) + #[schemars(example = 1.068)] pub adjustment: f64, } diff --git a/crates/brk_types/src/dollars.rs b/crates/brk_types/src/dollars.rs index 412043bfd..3915fa850 100644 --- a/crates/brk_types/src/dollars.rs +++ b/crates/brk_types/src/dollars.rs @@ -15,8 +15,9 @@ use crate::{Cents, Low, Open}; use super::{Bitcoin, CentsSigned, Close, High, Sats, StoredF32, StoredF64}; -/// US Dollar amount as floating point +/// US Dollar amount #[derive(Debug, Default, Clone, Copy, Deref, Serialize, Deserialize, Pco, JsonSchema)] +#[schemars(example = 0.0, example = 100.50, example = 30_000.0, example = 69_000.0, example = 84_342.12)] pub struct Dollars(f64); impl Hash for Dollars { diff --git a/crates/brk_types/src/feerate.rs b/crates/brk_types/src/feerate.rs index f28d06b8e..acd1345ac 100644 --- a/crates/brk_types/src/feerate.rs +++ b/crates/brk_types/src/feerate.rs @@ -9,8 +9,9 @@ use vecdb::{CheckedSub, Formattable, Pco}; use super::{Sats, VSize, Weight}; -/// Fee rate in sats/vB +/// Fee rate in sat/vB #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)] +#[schemars(example = 1.0, example = 2.5, example = 10.14, example = 25.0, example = 302.11)] pub struct FeeRate(f64); impl FeeRate { diff --git a/crates/brk_types/src/feerate_percentiles.rs b/crates/brk_types/src/feerate_percentiles.rs index 54ae57339..863c180bb 100644 --- a/crates/brk_types/src/feerate_percentiles.rs +++ b/crates/brk_types/src/feerate_percentiles.rs @@ -6,18 +6,25 @@ use super::FeeRate; /// Fee rate percentiles (min, 10%, 25%, 50%, 75%, 90%, max). #[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, JsonSchema)] pub struct FeeRatePercentiles { + /// Minimum fee rate (sat/vB) #[serde(rename = "avgFee_0")] pub min: FeeRate, + /// 10th percentile fee rate (sat/vB) #[serde(rename = "avgFee_10")] pub pct10: FeeRate, + /// 25th percentile fee rate (sat/vB) #[serde(rename = "avgFee_25")] pub pct25: FeeRate, + /// Median fee rate (sat/vB) #[serde(rename = "avgFee_50")] pub median: FeeRate, + /// 75th percentile fee rate (sat/vB) #[serde(rename = "avgFee_75")] pub pct75: FeeRate, + /// 90th percentile fee rate (sat/vB) #[serde(rename = "avgFee_90")] pub pct90: FeeRate, + /// Maximum fee rate (sat/vB) #[serde(rename = "avgFee_100")] pub max: FeeRate, } diff --git a/crates/brk_types/src/hashrate_entry.rs b/crates/brk_types/src/hashrate_entry.rs index bc72cadd3..14370ecd4 100644 --- a/crates/brk_types/src/hashrate_entry.rs +++ b/crates/brk_types/src/hashrate_entry.rs @@ -6,9 +6,10 @@ use super::Timestamp; /// A single hashrate data point. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct HashrateEntry { - /// Unix timestamp. + /// Unix timestamp pub timestamp: Timestamp, - /// Average hashrate (H/s). + /// Average hashrate (H/s) #[serde(rename = "avgHashrate")] + #[schemars(example = 700_000_000_000_000_000_000_u128)] pub avg_hashrate: u128, } diff --git a/crates/brk_types/src/hashrate_summary.rs b/crates/brk_types/src/hashrate_summary.rs index 1d6d3a239..9c2a05661 100644 --- a/crates/brk_types/src/hashrate_summary.rs +++ b/crates/brk_types/src/hashrate_summary.rs @@ -6,14 +6,16 @@ use super::{DifficultyEntry, HashrateEntry}; /// Summary of network hashrate and difficulty data. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct HashrateSummary { - /// Historical hashrate data points. + /// Historical hashrate data points pub hashrates: Vec, - /// Historical difficulty adjustments. + /// Historical difficulty adjustments pub difficulty: Vec, - /// Current network hashrate (H/s). + /// Current network hashrate (H/s) #[serde(rename = "currentHashrate")] + #[schemars(example = 700_000_000_000_000_000_000_u128)] pub current_hashrate: u128, - /// Current network difficulty. + /// Current network difficulty #[serde(rename = "currentDifficulty")] + #[schemars(example = 110_451_832_649_830.94)] pub current_difficulty: f64, } diff --git a/crates/brk_types/src/historical_price.rs b/crates/brk_types/src/historical_price.rs index c6338ac5b..fd42cbf83 100644 --- a/crates/brk_types/src/historical_price.rs +++ b/crates/brk_types/src/historical_price.rs @@ -27,7 +27,7 @@ pub struct HistoricalPrice { #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct HistoricalPriceEntry { /// Unix timestamp - pub time: u64, + pub time: Timestamp, /// BTC/USD price #[serde(rename = "USD")] pub usd: Dollars, diff --git a/crates/brk_types/src/mempool_info.rs b/crates/brk_types/src/mempool_info.rs index dac65c4de..fd7e45fd0 100644 --- a/crates/brk_types/src/mempool_info.rs +++ b/crates/brk_types/src/mempool_info.rs @@ -9,6 +9,7 @@ use crate::{FeeRate, Sats, Transaction, VSize}; #[derive(Debug, Default, Clone, Serialize, Deserialize, JsonSchema)] pub struct MempoolInfo { /// Number of transactions in the mempool + #[schemars(example = 5000)] pub count: usize, /// Total virtual size of all transactions in the mempool (vbytes) pub vsize: VSize, diff --git a/crates/brk_types/src/merkle_proof.rs b/crates/brk_types/src/merkle_proof.rs index db694dd52..b17a5b770 100644 --- a/crates/brk_types/src/merkle_proof.rs +++ b/crates/brk_types/src/merkle_proof.rs @@ -10,6 +10,7 @@ pub struct MerkleProof { pub block_height: Height, /// Merkle proof path (hex-encoded hashes) pub merkle: Vec, - /// Transaction position in the block + /// Transaction position in the block (0-indexed) + #[schemars(example = 42)] pub pos: usize, } diff --git a/crates/brk_types/src/pool_detail.rs b/crates/brk_types/src/pool_detail.rs index df873c938..32b2e9440 100644 --- a/crates/brk_types/src/pool_detail.rs +++ b/crates/brk_types/src/pool_detail.rs @@ -19,11 +19,12 @@ pub struct PoolDetail { #[serde(rename = "blockShare")] pub block_share: PoolBlockShares, - /// Estimated hashrate based on blocks mined + /// Estimated hashrate based on blocks mined (H/s) #[serde(rename = "estimatedHashrate")] + #[schemars(example = 200_000_000_000_000_000_000_u128)] pub estimated_hashrate: u128, - /// Self-reported hashrate (if available) + /// Self-reported hashrate (if available, H/s) #[serde(rename = "reportedHashrate")] pub reported_hashrate: Option, @@ -36,12 +37,15 @@ pub struct PoolDetail { #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct PoolDetailInfo { /// Pool identifier + #[schemars(example = 111)] pub id: u8, /// Pool name + #[schemars(example = &"Foundry USA")] pub name: Cow<'static, str>, /// Pool website URL + #[schemars(example = &"https://foundrydigital.com/")] pub link: Cow<'static, str>, /// Known payout addresses @@ -54,6 +58,7 @@ pub struct PoolDetailInfo { pub slug: PoolSlug, /// Unique pool identifier + #[schemars(example = 44)] pub unique_id: u8, } @@ -75,14 +80,17 @@ impl From<&'static Pool> for PoolDetailInfo { #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct PoolBlockCounts { /// Total blocks mined (all time) + #[schemars(example = 75000)] pub all: u64, /// Blocks mined in last 24 hours #[serde(rename = "24h")] + #[schemars(example = 42)] pub day: u64, /// Blocks mined in last week #[serde(rename = "1w")] + #[schemars(example = 280)] pub week: u64, } @@ -90,13 +98,16 @@ pub struct PoolBlockCounts { #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct PoolBlockShares { /// Share of all blocks (0.0 - 1.0) + #[schemars(example = 0.28)] pub all: f64, - /// Share of blocks in last 24 hours + /// Share of blocks in last 24 hours (0.0 - 1.0) #[serde(rename = "24h")] + #[schemars(example = 0.30)] pub day: f64, - /// Share of blocks in last week + /// Share of blocks in last week (0.0 - 1.0) #[serde(rename = "1w")] + #[schemars(example = 0.29)] pub week: f64, } diff --git a/crates/brk_types/src/pool_hashrate_entry.rs b/crates/brk_types/src/pool_hashrate_entry.rs index f86fdeced..a3f202d5d 100644 --- a/crates/brk_types/src/pool_hashrate_entry.rs +++ b/crates/brk_types/src/pool_hashrate_entry.rs @@ -6,14 +6,17 @@ use super::Timestamp; /// A single pool hashrate data point. #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct PoolHashrateEntry { - /// Unix timestamp. + /// Unix timestamp pub timestamp: Timestamp, - /// Average hashrate (H/s). + /// Average hashrate (H/s) #[serde(rename = "avgHashrate")] + #[schemars(example = 200_000_000_000_000_000_000_u128)] pub avg_hashrate: u128, - /// Pool's share of total network hashrate. + /// Pool's share of total network hashrate (0.0 - 1.0) + #[schemars(example = 0.30)] pub share: f64, - /// Pool name. + /// Pool name #[serde(rename = "poolName")] + #[schemars(example = &"Foundry USA")] pub pool_name: String, } diff --git a/crates/brk_types/src/pool_info.rs b/crates/brk_types/src/pool_info.rs index d324feec6..253e51278 100644 --- a/crates/brk_types/src/pool_info.rs +++ b/crates/brk_types/src/pool_info.rs @@ -9,12 +9,14 @@ use crate::{Pool, PoolSlug}; #[derive(Debug, Serialize, Deserialize, JsonSchema)] pub struct PoolInfo { /// Pool name + #[schemars(example = &"Foundry USA")] pub name: Cow<'static, str>, /// URL-friendly pool identifier pub slug: PoolSlug, /// Unique numeric pool identifier + #[schemars(example = 44)] pub unique_id: u8, } diff --git a/crates/brk_types/src/pool_slug.rs b/crates/brk_types/src/pool_slug.rs index 392346a72..83ab00ab2 100644 --- a/crates/brk_types/src/pool_slug.rs +++ b/crates/brk_types/src/pool_slug.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use strum::Display; use vecdb::{Bytes, Formattable}; -// Slug of a mining pool +/// URL-friendly mining pool identifier #[allow(clippy::upper_case_acronyms)] #[derive( Default, diff --git a/crates/brk_types/src/pool_stats.rs b/crates/brk_types/src/pool_stats.rs index c6404310f..c68238b3b 100644 --- a/crates/brk_types/src/pool_stats.rs +++ b/crates/brk_types/src/pool_stats.rs @@ -10,33 +10,41 @@ use crate::{Pool, PoolSlug}; pub struct PoolStats { /// Unique pool identifier #[serde(rename = "poolId")] + #[schemars(example = 111)] pub pool_id: u8, /// Pool name + #[schemars(example = &"Foundry USA")] pub name: Cow<'static, str>, /// Pool website URL + #[schemars(example = &"https://foundrydigital.com/")] pub link: Cow<'static, str>, /// Number of blocks mined in the time period #[serde(rename = "blockCount")] + #[schemars(example = 42)] pub block_count: u64, /// Pool ranking by block count (1 = most blocks) + #[schemars(example = 1)] pub rank: u32, /// Number of empty blocks mined #[serde(rename = "emptyBlocks")] + #[schemars(example = 0)] pub empty_blocks: u64, /// URL-friendly pool identifier pub slug: PoolSlug, /// Pool's share of total blocks (0.0 - 1.0) + #[schemars(example = 0.30)] pub share: f64, /// Unique pool identifier #[serde(rename = "poolUniqueId")] + #[schemars(example = 44)] pub pool_unique_id: u8, } diff --git a/crates/brk_types/src/pools_summary.rs b/crates/brk_types/src/pools_summary.rs index aa2cc580c..4b029a478 100644 --- a/crates/brk_types/src/pools_summary.rs +++ b/crates/brk_types/src/pools_summary.rs @@ -10,11 +10,15 @@ pub struct PoolsSummary { /// List of pools sorted by block count descending pub pools: Vec, /// Total blocks in the time period + #[schemars(example = 144)] pub block_count: u64, - /// Estimated network hashrate (hashes per second) + /// Estimated network hashrate (H/s) + #[schemars(example = 700_000_000_000_000_000_000_u128)] pub last_estimated_hashrate: u128, - /// Estimated network hashrate over last 3 days + /// Estimated network hashrate over last 3 days (H/s) + #[schemars(example = 700_000_000_000_000_000_000_u128)] pub last_estimated_hashrate3d: u128, - /// Estimated network hashrate over last 1 week + /// Estimated network hashrate over last 1 week (H/s) + #[schemars(example = 700_000_000_000_000_000_000_u128)] pub last_estimated_hashrate1w: u128, } diff --git a/crates/brk_types/src/raw_locktime.rs b/crates/brk_types/src/raw_locktime.rs index 9dbf8b851..521ffdff9 100644 --- a/crates/brk_types/src/raw_locktime.rs +++ b/crates/brk_types/src/raw_locktime.rs @@ -3,8 +3,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use vecdb::{Formattable, Pco}; -/// Transaction locktime +/// Transaction locktime. Values below 500,000,000 are interpreted as block heights; values at or above are Unix timestamps. #[derive(Debug, Clone, Copy, Serialize, Deserialize, Pco, JsonSchema)] +#[schemars(example = 0, example = 840000, example = 840001, example = 1713571200, example = 4294967295_u32)] pub struct RawLockTime(u32); impl From for RawLockTime { diff --git a/crates/brk_types/src/sats.rs b/crates/brk_types/src/sats.rs index 73cf8537d..398c2d23d 100644 --- a/crates/brk_types/src/sats.rs +++ b/crates/brk_types/src/sats.rs @@ -13,7 +13,7 @@ use crate::StoredF64; use super::{Bitcoin, Cents, Dollars, Height}; -/// Satoshis +/// Amount in satoshis (1 BTC = 100,000,000 sats) #[derive( Debug, PartialEq, @@ -30,6 +30,13 @@ use super::{Bitcoin, Cents, Dollars, Height}; Pco, JsonSchema, )] +#[schemars( + example = 0, + example = 546, + example = 10000, + example = 100_000_000, + example = 2_100_000_000_000_000_u64 +)] pub struct Sats(u64); #[allow(clippy::inconsistent_digit_grouping)] diff --git a/crates/brk_types/src/timestamp.rs b/crates/brk_types/src/timestamp.rs index 44c912c20..9d16efb68 100644 --- a/crates/brk_types/src/timestamp.rs +++ b/crates/brk_types/src/timestamp.rs @@ -24,7 +24,13 @@ use super::Date; Pco, JsonSchema, )] -#[schemars(example = 1672531200)] +#[schemars( + example = 1231006505, + example = 1672531200, + example = 1713571200, + example = 1743631892, + example = 1759000868 +)] pub struct Timestamp(u32); pub const ONE_HOUR_IN_SEC: u32 = 60 * 60; diff --git a/crates/brk_types/src/tx_index.rs b/crates/brk_types/src/tx_index.rs index a00dda948..6f5795d1a 100644 --- a/crates/brk_types/src/tx_index.rs +++ b/crates/brk_types/src/tx_index.rs @@ -8,6 +8,7 @@ use vecdb::{CheckedSub, Formattable, Pco, PrintableIndex}; use super::StoredU32; +/// Transaction index within a block (0 = coinbase) #[derive( Debug, PartialEq, @@ -25,6 +26,7 @@ use super::StoredU32; JsonSchema, Hash, )] +#[schemars(example = 0)] pub struct TxIndex(u32); impl TxIndex { diff --git a/crates/brk_types/src/tx_version_raw.rs b/crates/brk_types/src/tx_version_raw.rs index 87e1fc6dd..785b6753d 100644 --- a/crates/brk_types/src/tx_version_raw.rs +++ b/crates/brk_types/src/tx_version_raw.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; /// Unlike TxVersion (u8, indexed), this preserves non-standard values /// used in coinbase txs for miner signaling/branding. #[derive(Debug, Deref, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +#[schemars(example = 1, example = 2, example = 3, example = 536_870_912, example = 805_306_368)] pub struct TxVersionRaw(i32); impl From for TxVersionRaw { diff --git a/crates/brk_types/src/txout_spend.rs b/crates/brk_types/src/txout_spend.rs index 3f608fbb8..47b4d9767 100644 --- a/crates/brk_types/src/txout_spend.rs +++ b/crates/brk_types/src/txout_spend.rs @@ -7,6 +7,7 @@ use crate::{Height, TxStatus, Txid, Vin}; #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] pub struct TxOutspend { /// Whether the output has been spent + #[schemars(example = true)] pub spent: bool, /// Transaction ID of the spending transaction (only present if spent) diff --git a/crates/brk_types/src/vin.rs b/crates/brk_types/src/vin.rs index b5fce8bc1..3f9ebe92d 100644 --- a/crates/brk_types/src/vin.rs +++ b/crates/brk_types/src/vin.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; #[derive( Debug, Deref, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema, )] -#[schemars(example = 0)] +#[schemars(example = 0, example = 1, example = 2, example = 5, example = 10)] pub struct Vin(u16); impl Vin { diff --git a/crates/brk_types/src/vout.rs b/crates/brk_types/src/vout.rs index c90ae2f87..4c8a159d2 100644 --- a/crates/brk_types/src/vout.rs +++ b/crates/brk_types/src/vout.rs @@ -20,7 +20,7 @@ use vecdb::{Bytes, Formattable}; Bytes, Hash, )] -#[schemars(example = 0)] +#[schemars(example = 0, example = 1, example = 2, example = 5, example = 10)] pub struct Vout(u16); impl Vout { diff --git a/crates/brk_types/src/vsize.rs b/crates/brk_types/src/vsize.rs index 39697f46a..22497348a 100644 --- a/crates/brk_types/src/vsize.rs +++ b/crates/brk_types/src/vsize.rs @@ -7,7 +7,7 @@ use vecdb::{CheckedSub, Formattable, Pco}; use crate::Weight; -/// Virtual size in vbytes (weight / 4, rounded up) +/// Virtual size in vbytes (weight / 4, rounded up). Max block vsize is ~1,000,000 vB. #[derive( Debug, Default, @@ -23,6 +23,7 @@ use crate::Weight; Pco, JsonSchema, )] +#[schemars(example = 110, example = 140, example = 225, example = 500_000, example = 998_368)] pub struct VSize(u64); impl VSize { diff --git a/crates/brk_types/src/weight.rs b/crates/brk_types/src/weight.rs index a786071fd..6d8ffd0bb 100644 --- a/crates/brk_types/src/weight.rs +++ b/crates/brk_types/src/weight.rs @@ -7,7 +7,7 @@ use vecdb::{CheckedSub, Formattable, Pco}; use crate::VSize; -/// Transaction or block weight in weight units (WU) +/// Weight in weight units (WU). Max block weight is 4,000,000 WU. #[derive( Debug, Default, @@ -23,6 +23,7 @@ use crate::VSize; Pco, JsonSchema, )] +#[schemars(example = 396, example = 561, example = 900, example = 2_000_000, example = 3_993_472)] pub struct Weight(u64); impl Weight { diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index 2a06d5d4a..336a6703d 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -20,6 +20,7 @@ * @property {Sats} spentTxoSum - Total amount in satoshis spent from this address * @property {number} txCount - Total number of confirmed transactions involving this address * @property {TypeIndex} typeIndex - Index of this address within its type on the blockchain + * @property {Dollars} realizedPrice - Realized price (average cost basis) in USD */ /** * Address statistics in the mempool (unconfirmed transactions only) @@ -34,6 +35,8 @@ * @property {number} txCount - Number of unconfirmed transactions involving this address */ /** + * Bitcoin address path parameter + * * @typedef {Object} AddrParam * @property {Addr} address */ @@ -42,6 +45,7 @@ * * @typedef {Object} AddrStats * @property {Addr} address - Bitcoin address string + * @property {OutputType} addrType - Address type (p2pkh, p2sh, v0_p2wpkh, v0_p2wsh, v1_p2tr, etc.) * @property {AddrChainStats} chainStats - Statistics for confirmed transactions on the blockchain * @property {(AddrMempoolStats|null)=} mempoolStats - Statistics for unconfirmed transactions in the mempool */ @@ -106,6 +110,8 @@ * @typedef {number} Bitcoin */ /** + * Block count path parameter + * * @typedef {Object} BlockCountParam * @property {number} blockCount - Number of recent blocks to include */ @@ -151,13 +157,13 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {Object} BlockFeeRatesEntry * @property {Height} avgHeight - Average block height in this window * @property {Timestamp} timestamp - Unix timestamp at the window midpoint - * @property {FeeRate} avgFee0 - * @property {FeeRate} avgFee10 - * @property {FeeRate} avgFee25 - * @property {FeeRate} avgFee50 - * @property {FeeRate} avgFee75 - * @property {FeeRate} avgFee90 - * @property {FeeRate} avgFee100 + * @property {FeeRate} avgFee0 - Minimum fee rate (sat/vB) + * @property {FeeRate} avgFee10 - 10th percentile fee rate (sat/vB) + * @property {FeeRate} avgFee25 - 25th percentile fee rate (sat/vB) + * @property {FeeRate} avgFee50 - Median fee rate (sat/vB) + * @property {FeeRate} avgFee75 - 75th percentile fee rate (sat/vB) + * @property {FeeRate} avgFee90 - 90th percentile fee rate (sat/vB) + * @property {FeeRate} avgFee100 - Maximum fee rate (sat/vB) */ /** * A single block fees data point. @@ -174,15 +180,21 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {string} BlockHash */ /** + * Block hash path parameter + * * @typedef {Object} BlockHashParam * @property {BlockHash} hash */ /** + * Block hash + starting transaction index path parameters + * * @typedef {Object} BlockHashStartIndex * @property {BlockHash} hash - Bitcoin block hash * @property {TxIndex} startIndex - Starting transaction index within the block (0-based) */ /** + * Block hash + transaction index path parameters + * * @typedef {Object} BlockHashTxIndex * @property {BlockHash} hash - Bitcoin block hash * @property {TxIndex} index - Transaction index within the block (0-based) @@ -452,7 +464,7 @@ Matches mempool.space/bitcoin-cli behavior. * @property {number} ratio - brk as percentage of Bitcoin data */ /** - * US Dollar amount as floating point + * US Dollar amount * * @typedef {number} Dollars */ @@ -484,7 +496,7 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {Object} ExchangeRates */ /** - * Fee rate in sats/vB + * Fee rate in sat/vB * * @typedef {number} FeeRate */ @@ -511,17 +523,17 @@ Matches mempool.space/bitcoin-cli behavior. * A single hashrate data point. * * @typedef {Object} HashrateEntry - * @property {Timestamp} timestamp - Unix timestamp. - * @property {number} avgHashrate - Average hashrate (H/s). + * @property {Timestamp} timestamp - Unix timestamp + * @property {number} avgHashrate - Average hashrate (H/s) */ /** * Summary of network hashrate and difficulty data. * * @typedef {Object} HashrateSummary - * @property {HashrateEntry[]} hashrates - Historical hashrate data points. - * @property {DifficultyEntry[]} difficulty - Historical difficulty adjustments. - * @property {number} currentHashrate - Current network hashrate (H/s). - * @property {number} currentDifficulty - Current network difficulty. + * @property {HashrateEntry[]} hashrates - Historical hashrate data points + * @property {DifficultyEntry[]} difficulty - Historical difficulty adjustments + * @property {number} currentHashrate - Current network hashrate (H/s) + * @property {number} currentDifficulty - Current network difficulty */ /** * Server health status @@ -546,6 +558,8 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {number} Height */ /** + * Block height path parameter + * * @typedef {Object} HeightParam * @property {Height} height */ @@ -565,7 +579,7 @@ Matches mempool.space/bitcoin-cli behavior. * A single price data point * * @typedef {Object} HistoricalPriceEntry - * @property {number} time - Unix timestamp + * @property {Timestamp} time - Unix timestamp * @property {Dollars} uSD - BTC/USD price */ /** @typedef {number} Hour1 */ @@ -642,7 +656,7 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {Object} MerkleProof * @property {Height} blockHeight - Block height containing the transaction * @property {string[]} merkle - Merkle proof path (hex-encoded hashes) - * @property {number} pos - Transaction position in the block + * @property {number} pos - Transaction position in the block (0-indexed) */ /** @typedef {number} Minute10 */ /** @typedef {number} Minute30 */ @@ -683,6 +697,8 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {Dollars} Open */ /** + * Optional UNIX timestamp query parameter + * * @typedef {Object} OptionalTimestampParam * @property {(Timestamp|null)=} timestamp */ @@ -740,8 +756,8 @@ Matches mempool.space/bitcoin-cli behavior. * * @typedef {Object} PoolBlockShares * @property {number} all - Share of all blocks (0.0 - 1.0) - * @property {number} _24h - Share of blocks in last 24 hours - * @property {number} _1w - Share of blocks in last week + * @property {number} _24h - Share of blocks in last 24 hours (0.0 - 1.0) + * @property {number} _1w - Share of blocks in last week (0.0 - 1.0) */ /** * Detailed pool information with statistics across time periods @@ -750,8 +766,8 @@ Matches mempool.space/bitcoin-cli behavior. * @property {PoolDetailInfo} pool - Pool information * @property {PoolBlockCounts} blockCount - Block counts for different time periods * @property {PoolBlockShares} blockShare - Pool's share of total blocks for different time periods - * @property {number} estimatedHashrate - Estimated hashrate based on blocks mined - * @property {?number=} reportedHashrate - Self-reported hashrate (if available) + * @property {number} estimatedHashrate - Estimated hashrate based on blocks mined (H/s) + * @property {?number=} reportedHashrate - Self-reported hashrate (if available, H/s) * @property {(Sats|null)=} totalReward - Total reward earned by this pool (sats, all time; None for minor pools) */ /** @@ -770,10 +786,10 @@ Matches mempool.space/bitcoin-cli behavior. * A single pool hashrate data point. * * @typedef {Object} PoolHashrateEntry - * @property {Timestamp} timestamp - Unix timestamp. - * @property {number} avgHashrate - Average hashrate (H/s). - * @property {number} share - Pool's share of total network hashrate. - * @property {string} poolName - Pool name. + * @property {Timestamp} timestamp - Unix timestamp + * @property {number} avgHashrate - Average hashrate (H/s) + * @property {number} share - Pool's share of total network hashrate (0.0 - 1.0) + * @property {string} poolName - Pool name */ /** * Basic pool information for listing all pools @@ -783,13 +799,21 @@ Matches mempool.space/bitcoin-cli behavior. * @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"|"braiinssolo"|"solopool")} PoolSlug */ /** + * URL-friendly mining 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"|"braiinssolo"|"solopool")} PoolSlug + */ +/** + * Mining pool slug + block height path parameters + * * @typedef {Object} PoolSlugAndHeightParam * @property {PoolSlug} slug * @property {Height} height */ /** + * Mining pool slug path parameter + * * @typedef {Object} PoolSlugParam * @property {PoolSlug} slug */ @@ -813,9 +837,9 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {Object} PoolsSummary * @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 + * @property {number} lastEstimatedHashrate - Estimated network hashrate (H/s) + * @property {number} lastEstimatedHashrate3d - Estimated network hashrate over last 3 days (H/s) + * @property {number} lastEstimatedHashrate1w - Estimated network hashrate over last 1 week (H/s) */ /** * Current price response matching mempool.space /api/v1/prices format @@ -830,7 +854,7 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {(number|Date|Timestamp)} RangeIndex */ /** - * Transaction locktime + * Transaction locktime. Values below 500,000,000 are interpreted as block heights; values at or above are Unix timestamps. * * @typedef {number} RawLockTime */ @@ -855,7 +879,7 @@ Matches mempool.space/bitcoin-cli behavior. * @property {number} totalTx - Total number of transactions */ /** - * Satoshis + * Amount in satoshis (1 BTC = 100,000,000 sats) * * @typedef {number} Sats */ @@ -1003,6 +1027,8 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {("24h"|"3d"|"1w"|"1m"|"3m"|"6m"|"1y"|"2y"|"3y"|"all")} TimePeriod */ /** + * Time period path parameter (24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y) + * * @typedef {Object} TimePeriodParam * @property {TimePeriod} timePeriod */ @@ -1012,6 +1038,8 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {number} Timestamp */ /** + * UNIX timestamp path parameter + * * @typedef {Object} TimestampParam * @property {Timestamp} timestamp */ @@ -1052,7 +1080,11 @@ Matches mempool.space/bitcoin-cli behavior. * @property {string} innerWitnessscriptAsm - Inner witnessscript in assembly (for P2WSH: last witness item decoded as script) */ /** @typedef {number} TxInIndex */ -/** @typedef {number} TxIndex */ +/** + * Transaction index within a block (0 = coinbase) + * + * @typedef {number} TxIndex + */ /** * Transaction output * @@ -1097,6 +1129,8 @@ Matches mempool.space/bitcoin-cli behavior. * @typedef {string} Txid */ /** + * Transaction ID path parameter + * * @typedef {Object} TxidParam * @property {Txid} txid */ @@ -1128,7 +1162,7 @@ Matches mempool.space/bitcoin-cli behavior. * @property {Sats} value - Output value in satoshis */ /** - * Virtual size in vbytes (weight / 4, rounded up) + * Virtual size in vbytes (weight / 4, rounded up). Max block vsize is ~1,000,000 vB. * * @typedef {number} VSize */ @@ -1157,7 +1191,7 @@ Matches mempool.space/bitcoin-cli behavior. */ /** @typedef {number} Week1 */ /** - * Transaction or block weight in weight units (WU) + * Weight in weight units (WU). Max block weight is 4,000,000 WU. * * @typedef {number} Weight */ @@ -1556,11 +1590,14 @@ class BrkClientBase { /** * @param {string} path + * @param {{ signal?: AbortSignal }} [options] * @returns {Promise} */ - async get(path) { + async get(path, { signal } = {}) { const url = `${this.baseUrl}${path}`; - const res = await fetch(url, { signal: AbortSignal.timeout(this.timeout) }); + const signals = [AbortSignal.timeout(this.timeout)]; + if (signal) signals.push(signal); + const res = await fetch(url, { signal: AbortSignal.any(signals) }); if (!res.ok) throw new BrkError(`HTTP ${res.status}: ${url}`, res.status); return res; } @@ -1569,10 +1606,10 @@ class BrkClientBase { * Make a GET request - races cache vs network, first to resolve calls onUpdate * @template T * @param {string} path - * @param {(value: T) => void} [onUpdate] - Called when data is available (may be called twice: cache then network) + * @param {{ onUpdate?: (value: T) => void, signal?: AbortSignal }} [options] * @returns {Promise} */ - async getJson(path, onUpdate) { + async getJson(path, { onUpdate, signal } = {}) { const url = `${this.baseUrl}${path}`; const cache = this._cache ?? await this._cachePromise; @@ -1592,7 +1629,7 @@ class BrkClientBase { return json; }); - const networkPromise = this.get(path).then(async (res) => { + const networkPromise = this.get(path, { signal }).then(async (res) => { const cloned = res.clone(); const json = _addCamelGetters(await res.json()); // Skip update if ETag matches and cache already delivered @@ -1624,10 +1661,11 @@ class BrkClientBase { /** * Make a GET request and return raw text (for CSV responses) * @param {string} path + * @param {{ signal?: AbortSignal }} [options] * @returns {Promise} */ - async getText(path) { - const res = await this.get(path); + async getText(path, { signal } = {}) { + const res = await this.get(path, { signal }); return res.text(); } @@ -1640,7 +1678,7 @@ class BrkClientBase { */ async _fetchSeriesData(path, onUpdate) { const wrappedOnUpdate = onUpdate ? (/** @type {SeriesData} */ raw) => onUpdate(_wrapSeriesData(raw)) : undefined; - const raw = await this.getJson(path, wrappedOnUpdate); + const raw = await this.getJson(path, { onUpdate: wrappedOnUpdate }); return _wrapSeriesData(raw); } } @@ -9458,10 +9496,11 @@ class BrkClient extends BrkClientBase { * Compact OpenAPI specification optimized for LLM consumption. Removes redundant fields while preserving essential API information. Full spec available at `/openapi.json`. * * Endpoint: `GET /api.json` + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getApi() { - return this.getJson(`/api.json`); + async getApi({ signal, onUpdate } = {}) { + return this.getJson(`/api.json`, { signal, onUpdate }); } /** @@ -9474,10 +9513,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/address/{address}` * * @param {Addr} address + * @param {{ signal?: AbortSignal, onUpdate?: (value: AddrStats) => void }} [options] * @returns {Promise} */ - async getAddress(address) { - return this.getJson(`/api/address/${address}`); + async getAddress(address, { signal, onUpdate } = {}) { + return this.getJson(`/api/address/${address}`, { signal, onUpdate }); } /** @@ -9491,14 +9531,15 @@ class BrkClient extends BrkClientBase { * * @param {Addr} address * @param {Txid=} [after_txid] - Txid to paginate from (return transactions before this one) + * @param {{ signal?: AbortSignal, onUpdate?: (value: Transaction[]) => void }} [options] * @returns {Promise} */ - async getAddressTxs(address, after_txid) { + async getAddressTxs(address, after_txid, { signal, onUpdate } = {}) { const params = new URLSearchParams(); if (after_txid !== undefined) params.set('after_txid', String(after_txid)); const query = params.toString(); const path = `/api/address/${address}/txs${query ? '?' + query : ''}`; - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** @@ -9512,14 +9553,15 @@ class BrkClient extends BrkClientBase { * * @param {Addr} address * @param {Txid=} [after_txid] - Txid to paginate from (return transactions before this one) + * @param {{ signal?: AbortSignal, onUpdate?: (value: Transaction[]) => void }} [options] * @returns {Promise} */ - async getAddressConfirmedTxs(address, after_txid) { + async getAddressConfirmedTxs(address, after_txid, { signal, onUpdate } = {}) { const params = new URLSearchParams(); if (after_txid !== undefined) params.set('after_txid', String(after_txid)); const query = params.toString(); const path = `/api/address/${address}/txs/chain${query ? '?' + query : ''}`; - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** @@ -9532,10 +9574,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/address/{address}/txs/mempool` * * @param {Addr} address + * @param {{ signal?: AbortSignal, onUpdate?: (value: Txid[]) => void }} [options] * @returns {Promise} */ - async getAddressMempoolTxs(address) { - return this.getJson(`/api/address/${address}/txs/mempool`); + async getAddressMempoolTxs(address, { signal, onUpdate } = {}) { + return this.getJson(`/api/address/${address}/txs/mempool`, { signal, onUpdate }); } /** @@ -9548,10 +9591,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/address/{address}/utxo` * * @param {Addr} address + * @param {{ signal?: AbortSignal, onUpdate?: (value: Utxo[]) => void }} [options] * @returns {Promise} */ - async getAddressUtxos(address) { - return this.getJson(`/api/address/${address}/utxo`); + async getAddressUtxos(address, { signal, onUpdate } = {}) { + return this.getJson(`/api/address/${address}/utxo`, { signal, onUpdate }); } /** @@ -9564,10 +9608,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/block-height/{height}` * * @param {Height} height + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getBlockByHeight(height) { - return this.getJson(`/api/block-height/${height}`); + async getBlockByHeight(height, { signal, onUpdate } = {}) { + return this.getJson(`/api/block-height/${height}`, { signal, onUpdate }); } /** @@ -9580,26 +9625,28 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/block/{hash}` * * @param {BlockHash} hash + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockInfo) => void }} [options] * @returns {Promise} */ - async getBlock(hash) { - return this.getJson(`/api/block/${hash}`); + async getBlock(hash, { signal, onUpdate } = {}) { + return this.getJson(`/api/block/${hash}`, { signal, onUpdate }); } /** * Block header * - * Returns the hex-encoded block header. + * Returns the hex-encoded 80-byte block header. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-header)* * * Endpoint: `GET /api/block/{hash}/header` * * @param {BlockHash} hash + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getBlockHeader(hash) { - return this.getJson(`/api/block/${hash}/header`); + async getBlockHeader(hash, { signal, onUpdate } = {}) { + return this.getJson(`/api/block/${hash}/header`, { signal, onUpdate }); } /** @@ -9612,10 +9659,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/block/{hash}/raw` * * @param {BlockHash} hash + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getBlockRaw(hash) { - return this.getJson(`/api/block/${hash}/raw`); + async getBlockRaw(hash, { signal, onUpdate } = {}) { + return this.getJson(`/api/block/${hash}/raw`, { signal, onUpdate }); } /** @@ -9628,10 +9676,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/block/{hash}/status` * * @param {BlockHash} hash + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockStatus) => void }} [options] * @returns {Promise} */ - async getBlockStatus(hash) { - return this.getJson(`/api/block/${hash}/status`); + async getBlockStatus(hash, { signal, onUpdate } = {}) { + return this.getJson(`/api/block/${hash}/status`, { signal, onUpdate }); } /** @@ -9645,10 +9694,11 @@ class BrkClient extends BrkClientBase { * * @param {BlockHash} hash - Bitcoin block hash * @param {TxIndex} index - Transaction index within the block (0-based) + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getBlockTxid(hash, index) { - return this.getJson(`/api/block/${hash}/txid/${index}`); + async getBlockTxid(hash, index, { signal, onUpdate } = {}) { + return this.getJson(`/api/block/${hash}/txid/${index}`, { signal, onUpdate }); } /** @@ -9661,10 +9711,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/block/{hash}/txids` * * @param {BlockHash} hash + * @param {{ signal?: AbortSignal, onUpdate?: (value: Txid[]) => void }} [options] * @returns {Promise} */ - async getBlockTxids(hash) { - return this.getJson(`/api/block/${hash}/txids`); + async getBlockTxids(hash, { signal, onUpdate } = {}) { + return this.getJson(`/api/block/${hash}/txids`, { signal, onUpdate }); } /** @@ -9677,10 +9728,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/block/{hash}/txs` * * @param {BlockHash} hash + * @param {{ signal?: AbortSignal, onUpdate?: (value: Transaction[]) => void }} [options] * @returns {Promise} */ - async getBlockTxs(hash) { - return this.getJson(`/api/block/${hash}/txs`); + async getBlockTxs(hash, { signal, onUpdate } = {}) { + return this.getJson(`/api/block/${hash}/txs`, { signal, onUpdate }); } /** @@ -9694,10 +9746,11 @@ class BrkClient extends BrkClientBase { * * @param {BlockHash} hash - Bitcoin block hash * @param {TxIndex} start_index - Starting transaction index within the block (0-based) + * @param {{ signal?: AbortSignal, onUpdate?: (value: Transaction[]) => void }} [options] * @returns {Promise} */ - async getBlockTxsFromIndex(hash, start_index) { - return this.getJson(`/api/block/${hash}/txs/${start_index}`); + async getBlockTxsFromIndex(hash, start_index, { signal, onUpdate } = {}) { + return this.getJson(`/api/block/${hash}/txs/${start_index}`, { signal, onUpdate }); } /** @@ -9708,10 +9761,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks)* * * Endpoint: `GET /api/blocks` + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockInfo[]) => void }} [options] * @returns {Promise} */ - async getBlocks() { - return this.getJson(`/api/blocks`); + async getBlocks({ signal, onUpdate } = {}) { + return this.getJson(`/api/blocks`, { signal, onUpdate }); } /** @@ -9722,10 +9776,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-tip-hash)* * * Endpoint: `GET /api/blocks/tip/hash` + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getBlockTipHash() { - return this.getJson(`/api/blocks/tip/hash`); + async getBlockTipHash({ signal, onUpdate } = {}) { + return this.getJson(`/api/blocks/tip/hash`, { signal, onUpdate }); } /** @@ -9736,10 +9791,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-tip-height)* * * Endpoint: `GET /api/blocks/tip/height` + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getBlockTipHeight() { - return this.getJson(`/api/blocks/tip/height`); + async getBlockTipHeight({ signal, onUpdate } = {}) { + return this.getJson(`/api/blocks/tip/height`, { signal, onUpdate }); } /** @@ -9752,10 +9808,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/blocks/{height}` * * @param {Height} height + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockInfo[]) => void }} [options] * @returns {Promise} */ - async getBlocksFromHeight(height) { - return this.getJson(`/api/blocks/${height}`); + async getBlocksFromHeight(height, { signal, onUpdate } = {}) { + return this.getJson(`/api/blocks/${height}`, { signal, onUpdate }); } /** @@ -9766,10 +9823,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool)* * * Endpoint: `GET /api/mempool` + * @param {{ signal?: AbortSignal, onUpdate?: (value: MempoolInfo) => void }} [options] * @returns {Promise} */ - async getMempool() { - return this.getJson(`/api/mempool`); + async getMempool({ signal, onUpdate } = {}) { + return this.getJson(`/api/mempool`, { signal, onUpdate }); } /** @@ -9778,10 +9836,11 @@ class BrkClient extends BrkClientBase { * Returns the current BTC/USD price in dollars, derived from on-chain round-dollar output patterns in the last 12 blocks plus mempool. * * Endpoint: `GET /api/mempool/price` + * @param {{ signal?: AbortSignal, onUpdate?: (value: Dollars) => void }} [options] * @returns {Promise} */ - async getLivePrice() { - return this.getJson(`/api/mempool/price`); + async getLivePrice({ signal, onUpdate } = {}) { + return this.getJson(`/api/mempool/price`, { signal, onUpdate }); } /** @@ -9792,10 +9851,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-recent)* * * Endpoint: `GET /api/mempool/recent` + * @param {{ signal?: AbortSignal, onUpdate?: (value: MempoolRecentTx[]) => void }} [options] * @returns {Promise} */ - async getMempoolRecent() { - return this.getJson(`/api/mempool/recent`); + async getMempoolRecent({ signal, onUpdate } = {}) { + return this.getJson(`/api/mempool/recent`, { signal, onUpdate }); } /** @@ -9806,10 +9866,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-transaction-ids)* * * Endpoint: `GET /api/mempool/txids` + * @param {{ signal?: AbortSignal, onUpdate?: (value: Txid[]) => void }} [options] * @returns {Promise} */ - async getMempoolTxids() { - return this.getJson(`/api/mempool/txids`); + async getMempoolTxids({ signal, onUpdate } = {}) { + return this.getJson(`/api/mempool/txids`, { signal, onUpdate }); } /** @@ -9818,10 +9879,11 @@ class BrkClient extends BrkClientBase { * Returns the complete hierarchical catalog of available series organized as a tree structure. Series are grouped by categories and subcategories. * * Endpoint: `GET /api/series` + * @param {{ signal?: AbortSignal, onUpdate?: (value: TreeNode) => void }} [options] * @returns {Promise} */ - async getSeriesTree() { - return this.getJson(`/api/series`); + async getSeriesTree({ signal, onUpdate } = {}) { + return this.getJson(`/api/series`, { signal, onUpdate }); } /** @@ -9837,9 +9899,10 @@ class BrkClient extends BrkClientBase { * @param {RangeIndex=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e` * @param {Limit=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l` * @param {Format=} [format] - Format of the output + * @param {{ signal?: AbortSignal, onUpdate?: (value: AnySeriesData[] | string) => void }} [options] * @returns {Promise} */ - async getSeriesBulk(series, index, start, end, limit, format) { + async getSeriesBulk(series, index, start, end, limit, format, { signal, onUpdate } = {}) { const params = new URLSearchParams(); params.set('series', String(series)); params.set('index', String(index)); @@ -9850,9 +9913,9 @@ class BrkClient extends BrkClientBase { const query = params.toString(); const path = `/api/series/bulk${query ? '?' + query : ''}`; if (format === 'csv') { - return this.getText(path); + return this.getText(path, { signal }); } - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** @@ -9861,10 +9924,11 @@ class BrkClient extends BrkClientBase { * List available cohorts for cost basis distribution. * * Endpoint: `GET /api/series/cost-basis` + * @param {{ signal?: AbortSignal, onUpdate?: (value: string[]) => void }} [options] * @returns {Promise} */ - async getCostBasisCohorts() { - return this.getJson(`/api/series/cost-basis`); + async getCostBasisCohorts({ signal, onUpdate } = {}) { + return this.getJson(`/api/series/cost-basis`, { signal, onUpdate }); } /** @@ -9875,10 +9939,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/series/cost-basis/{cohort}/dates` * * @param {Cohort} cohort + * @param {{ signal?: AbortSignal, onUpdate?: (value: Date[]) => void }} [options] * @returns {Promise} */ - async getCostBasisDates(cohort) { - return this.getJson(`/api/series/cost-basis/${cohort}/dates`); + async getCostBasisDates(cohort, { signal, onUpdate } = {}) { + return this.getJson(`/api/series/cost-basis/${cohort}/dates`, { signal, onUpdate }); } /** @@ -9896,15 +9961,16 @@ class BrkClient extends BrkClientBase { * @param {string} date * @param {CostBasisBucket=} [bucket] - Bucket type for aggregation. Default: raw (no aggregation). * @param {CostBasisValue=} [value] - Value type to return. Default: supply. + * @param {{ signal?: AbortSignal, onUpdate?: (value: Object) => void }} [options] * @returns {Promise} */ - async getCostBasis(cohort, date, bucket, value) { + async getCostBasis(cohort, date, bucket, value, { signal, onUpdate } = {}) { const params = new URLSearchParams(); if (bucket !== undefined) params.set('bucket', String(bucket)); if (value !== undefined) params.set('value', String(value)); const query = params.toString(); const path = `/api/series/cost-basis/${cohort}/${date}${query ? '?' + query : ''}`; - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** @@ -9913,10 +9979,11 @@ class BrkClient extends BrkClientBase { * Returns the number of series available per index type. * * Endpoint: `GET /api/series/count` + * @param {{ signal?: AbortSignal, onUpdate?: (value: SeriesCount[]) => void }} [options] * @returns {Promise} */ - async getSeriesCount() { - return this.getJson(`/api/series/count`); + async getSeriesCount({ signal, onUpdate } = {}) { + return this.getJson(`/api/series/count`, { signal, onUpdate }); } /** @@ -9925,10 +9992,11 @@ class BrkClient extends BrkClientBase { * Returns all available indexes with their accepted query aliases. Use any alias when querying series. * * Endpoint: `GET /api/series/indexes` + * @param {{ signal?: AbortSignal, onUpdate?: (value: IndexInfo[]) => void }} [options] * @returns {Promise} */ - async getIndexes() { - return this.getJson(`/api/series/indexes`); + async getIndexes({ signal, onUpdate } = {}) { + return this.getJson(`/api/series/indexes`, { signal, onUpdate }); } /** @@ -9940,15 +10008,16 @@ class BrkClient extends BrkClientBase { * * @param {number=} [page] - Pagination index * @param {number=} [per_page] - Results per page (default: 1000, max: 1000) + * @param {{ signal?: AbortSignal, onUpdate?: (value: PaginatedSeries) => void }} [options] * @returns {Promise} */ - async listSeries(page, per_page) { + async listSeries(page, per_page, { signal, onUpdate } = {}) { const params = new URLSearchParams(); if (page !== undefined) params.set('page', String(page)); if (per_page !== undefined) params.set('per_page', String(per_page)); const query = params.toString(); const path = `/api/series/list${query ? '?' + query : ''}`; - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** @@ -9960,15 +10029,16 @@ class BrkClient extends BrkClientBase { * * @param {SeriesName} [q] - Search query string * @param {Limit=} [limit] - Maximum number of results + * @param {{ signal?: AbortSignal, onUpdate?: (value: string[]) => void }} [options] * @returns {Promise} */ - async searchSeries(q, limit) { + async searchSeries(q, limit, { signal, onUpdate } = {}) { const params = new URLSearchParams(); params.set('q', String(q)); if (limit !== undefined) params.set('limit', String(limit)); const query = params.toString(); const path = `/api/series/search${query ? '?' + query : ''}`; - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** @@ -9979,10 +10049,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/series/{series}` * * @param {SeriesName} series + * @param {{ signal?: AbortSignal, onUpdate?: (value: SeriesInfo) => void }} [options] * @returns {Promise} */ - async getSeriesInfo(series) { - return this.getJson(`/api/series/${series}`); + async getSeriesInfo(series, { signal, onUpdate } = {}) { + return this.getJson(`/api/series/${series}`, { signal, onUpdate }); } /** @@ -9998,9 +10069,10 @@ class BrkClient extends BrkClientBase { * @param {RangeIndex=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e` * @param {Limit=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l` * @param {Format=} [format] - Format of the output + * @param {{ signal?: AbortSignal, onUpdate?: (value: AnySeriesData | string) => void }} [options] * @returns {Promise} */ - async getSeries(series, index, start, end, limit, format) { + async getSeries(series, index, start, end, limit, format, { signal, onUpdate } = {}) { const params = new URLSearchParams(); if (start !== undefined) params.set('start', String(start)); if (end !== undefined) params.set('end', String(end)); @@ -10009,9 +10081,9 @@ class BrkClient extends BrkClientBase { const query = params.toString(); const path = `/api/series/${series}/${index}${query ? '?' + query : ''}`; if (format === 'csv') { - return this.getText(path); + return this.getText(path, { signal }); } - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** @@ -10027,9 +10099,10 @@ class BrkClient extends BrkClientBase { * @param {RangeIndex=} [end] - Exclusive end: integer index, date (YYYY-MM-DD), or timestamp (ISO 8601). Negative integers count from end. Aliases: `to`, `t`, `e` * @param {Limit=} [limit] - Maximum number of values to return (ignored if `end` is set). Aliases: `count`, `c`, `l` * @param {Format=} [format] - Format of the output + * @param {{ signal?: AbortSignal, onUpdate?: (value: boolean[] | string) => void }} [options] * @returns {Promise} */ - async getSeriesData(series, index, start, end, limit, format) { + async getSeriesData(series, index, start, end, limit, format, { signal, onUpdate } = {}) { const params = new URLSearchParams(); if (start !== undefined) params.set('start', String(start)); if (end !== undefined) params.set('end', String(end)); @@ -10038,9 +10111,9 @@ class BrkClient extends BrkClientBase { const query = params.toString(); const path = `/api/series/${series}/${index}/data${query ? '?' + query : ''}`; if (format === 'csv') { - return this.getText(path); + return this.getText(path, { signal }); } - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** @@ -10052,10 +10125,11 @@ class BrkClient extends BrkClientBase { * * @param {SeriesName} series - Series name * @param {Index} index - Aggregation index + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getSeriesLatest(series, index) { - return this.getJson(`/api/series/${series}/${index}/latest`); + async getSeriesLatest(series, index, { signal, onUpdate } = {}) { + return this.getJson(`/api/series/${series}/${index}/latest`, { signal, onUpdate }); } /** @@ -10067,10 +10141,11 @@ class BrkClient extends BrkClientBase { * * @param {SeriesName} series - Series name * @param {Index} index - Aggregation index + * @param {{ signal?: AbortSignal, onUpdate?: (value: number) => void }} [options] * @returns {Promise} */ - async getSeriesLen(series, index) { - return this.getJson(`/api/series/${series}/${index}/len`); + async getSeriesLen(series, index, { signal, onUpdate } = {}) { + return this.getJson(`/api/series/${series}/${index}/len`, { signal, onUpdate }); } /** @@ -10082,10 +10157,11 @@ class BrkClient extends BrkClientBase { * * @param {SeriesName} series - Series name * @param {Index} index - Aggregation index + * @param {{ signal?: AbortSignal, onUpdate?: (value: Version) => void }} [options] * @returns {Promise} */ - async getSeriesVersion(series, index) { - return this.getJson(`/api/series/${series}/${index}/version`); + async getSeriesVersion(series, index, { signal, onUpdate } = {}) { + return this.getJson(`/api/series/${series}/${index}/version`, { signal, onUpdate }); } /** @@ -10094,10 +10170,11 @@ class BrkClient extends BrkClientBase { * Returns the disk space used by BRK and Bitcoin data. * * Endpoint: `GET /api/server/disk` + * @param {{ signal?: AbortSignal, onUpdate?: (value: DiskUsage) => void }} [options] * @returns {Promise} */ - async getDiskUsage() { - return this.getJson(`/api/server/disk`); + async getDiskUsage({ signal, onUpdate } = {}) { + return this.getJson(`/api/server/disk`, { signal, onUpdate }); } /** @@ -10106,10 +10183,11 @@ class BrkClient extends BrkClientBase { * Returns the sync status of the indexer, including indexed height, tip height, blocks behind, and last indexed timestamp. * * Endpoint: `GET /api/server/sync` + * @param {{ signal?: AbortSignal, onUpdate?: (value: SyncStatus) => void }} [options] * @returns {Promise} */ - async getSyncStatus() { - return this.getJson(`/api/server/sync`); + async getSyncStatus({ signal, onUpdate } = {}) { + return this.getJson(`/api/server/sync`, { signal, onUpdate }); } /** @@ -10122,10 +10200,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/tx/{txid}` * * @param {Txid} txid + * @param {{ signal?: AbortSignal, onUpdate?: (value: Transaction) => void }} [options] * @returns {Promise} */ - async getTx(txid) { - return this.getJson(`/api/tx/${txid}`); + async getTx(txid, { signal, onUpdate } = {}) { + return this.getJson(`/api/tx/${txid}`, { signal, onUpdate }); } /** @@ -10138,10 +10217,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/tx/{txid}/hex` * * @param {Txid} txid + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getTxHex(txid) { - return this.getJson(`/api/tx/${txid}/hex`); + async getTxHex(txid, { signal, onUpdate } = {}) { + return this.getJson(`/api/tx/${txid}/hex`, { signal, onUpdate }); } /** @@ -10154,10 +10234,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/tx/{txid}/merkle-proof` * * @param {Txid} txid + * @param {{ signal?: AbortSignal, onUpdate?: (value: MerkleProof) => void }} [options] * @returns {Promise} */ - async getTxMerkleProof(txid) { - return this.getJson(`/api/tx/${txid}/merkle-proof`); + async getTxMerkleProof(txid, { signal, onUpdate } = {}) { + return this.getJson(`/api/tx/${txid}/merkle-proof`, { signal, onUpdate }); } /** @@ -10170,10 +10251,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/tx/{txid}/merkleblock-proof` * * @param {Txid} txid + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getTxMerkleblockProof(txid) { - return this.getJson(`/api/tx/${txid}/merkleblock-proof`); + async getTxMerkleblockProof(txid, { signal, onUpdate } = {}) { + return this.getJson(`/api/tx/${txid}/merkleblock-proof`, { signal, onUpdate }); } /** @@ -10187,10 +10269,11 @@ class BrkClient extends BrkClientBase { * * @param {Txid} txid - Transaction ID * @param {Vout} vout - Output index + * @param {{ signal?: AbortSignal, onUpdate?: (value: TxOutspend) => void }} [options] * @returns {Promise} */ - async getTxOutspend(txid, vout) { - return this.getJson(`/api/tx/${txid}/outspend/${vout}`); + async getTxOutspend(txid, vout, { signal, onUpdate } = {}) { + return this.getJson(`/api/tx/${txid}/outspend/${vout}`, { signal, onUpdate }); } /** @@ -10203,10 +10286,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/tx/{txid}/outspends` * * @param {Txid} txid + * @param {{ signal?: AbortSignal, onUpdate?: (value: TxOutspend[]) => void }} [options] * @returns {Promise} */ - async getTxOutspends(txid) { - return this.getJson(`/api/tx/${txid}/outspends`); + async getTxOutspends(txid, { signal, onUpdate } = {}) { + return this.getJson(`/api/tx/${txid}/outspends`, { signal, onUpdate }); } /** @@ -10219,10 +10303,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/tx/{txid}/raw` * * @param {Txid} txid + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getTxRaw(txid) { - return this.getJson(`/api/tx/${txid}/raw`); + async getTxRaw(txid, { signal, onUpdate } = {}) { + return this.getJson(`/api/tx/${txid}/raw`, { signal, onUpdate }); } /** @@ -10235,10 +10320,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/tx/{txid}/status` * * @param {Txid} txid + * @param {{ signal?: AbortSignal, onUpdate?: (value: TxStatus) => void }} [options] * @returns {Promise} */ - async getTxStatus(txid) { - return this.getJson(`/api/tx/${txid}/status`); + async getTxStatus(txid, { signal, onUpdate } = {}) { + return this.getJson(`/api/tx/${txid}/status`, { signal, onUpdate }); } /** @@ -10251,10 +10337,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/v1/block/{hash}` * * @param {BlockHash} hash + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockInfoV1) => void }} [options] * @returns {Promise} */ - async getBlockV1(hash) { - return this.getJson(`/api/v1/block/${hash}`); + async getBlockV1(hash, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/block/${hash}`, { signal, onUpdate }); } /** @@ -10265,10 +10352,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-blocks-v1)* * * Endpoint: `GET /api/v1/blocks` + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockInfoV1[]) => void }} [options] * @returns {Promise} */ - async getBlocksV1() { - return this.getJson(`/api/v1/blocks`); + async getBlocksV1({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/blocks`, { signal, onUpdate }); } /** @@ -10281,26 +10369,28 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/v1/blocks/{height}` * * @param {Height} height + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockInfoV1[]) => void }} [options] * @returns {Promise} */ - async getBlocksV1FromHeight(height) { - return this.getJson(`/api/v1/blocks/${height}`); + async getBlocksV1FromHeight(height, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/blocks/${height}`, { signal, onUpdate }); } /** * CPFP info * - * Returns ancestors and descendants for a CPFP transaction. + * Returns ancestors and descendants for a CPFP (Child Pays For Parent) transaction, including the effective fee rate of the package. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)* * * Endpoint: `GET /api/v1/cpfp/{txid}` * * @param {Txid} txid + * @param {{ signal?: AbortSignal, onUpdate?: (value: CpfpInfo) => void }} [options] * @returns {Promise} */ - async getCpfp(txid) { - return this.getJson(`/api/v1/cpfp/${txid}`); + async getCpfp(txid, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/cpfp/${txid}`, { signal, onUpdate }); } /** @@ -10311,10 +10401,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustment)* * * Endpoint: `GET /api/v1/difficulty-adjustment` + * @param {{ signal?: AbortSignal, onUpdate?: (value: DifficultyAdjustment) => void }} [options] * @returns {Promise} */ - async getDifficultyAdjustment() { - return this.getJson(`/api/v1/difficulty-adjustment`); + async getDifficultyAdjustment({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/difficulty-adjustment`, { signal, onUpdate }); } /** @@ -10325,10 +10416,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mempool-blocks-fees)* * * Endpoint: `GET /api/v1/fees/mempool-blocks` + * @param {{ signal?: AbortSignal, onUpdate?: (value: MempoolBlock[]) => void }} [options] * @returns {Promise} */ - async getMempoolBlocks() { - return this.getJson(`/api/v1/fees/mempool-blocks`); + async getMempoolBlocks({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/fees/mempool-blocks`, { signal, onUpdate }); } /** @@ -10339,10 +10431,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees-precise)* * * Endpoint: `GET /api/v1/fees/precise` + * @param {{ signal?: AbortSignal, onUpdate?: (value: RecommendedFees) => void }} [options] * @returns {Promise} */ - async getPreciseFees() { - return this.getJson(`/api/v1/fees/precise`); + async getPreciseFees({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/fees/precise`, { signal, onUpdate }); } /** @@ -10353,10 +10446,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-recommended-fees)* * * Endpoint: `GET /api/v1/fees/recommended` + * @param {{ signal?: AbortSignal, onUpdate?: (value: RecommendedFees) => void }} [options] * @returns {Promise} */ - async getRecommendedFees() { - return this.getJson(`/api/v1/fees/recommended`); + async getRecommendedFees({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/fees/recommended`, { signal, onUpdate }); } /** @@ -10369,78 +10463,83 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/v1/historical-price` * * @param {Timestamp=} [timestamp] + * @param {{ signal?: AbortSignal, onUpdate?: (value: HistoricalPrice) => void }} [options] * @returns {Promise} */ - async getHistoricalPrice(timestamp) { + async getHistoricalPrice(timestamp, { signal, onUpdate } = {}) { const params = new URLSearchParams(); if (timestamp !== undefined) params.set('timestamp', String(timestamp)); const query = params.toString(); const path = `/api/v1/historical-price${query ? '?' + query : ''}`; - return this.getJson(path); + return this.getJson(path, { signal, onUpdate }); } /** * Block fee rates * - * Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + * Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)* * * Endpoint: `GET /api/v1/mining/blocks/fee-rates/{time_period}` * * @param {TimePeriod} time_period + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockFeeRatesEntry[]) => void }} [options] * @returns {Promise} */ - async getBlockFeeRates(time_period) { - return this.getJson(`/api/v1/mining/blocks/fee-rates/${time_period}`); + async getBlockFeeRates(time_period, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/blocks/fee-rates/${time_period}`, { signal, onUpdate }); } /** * Block fees * - * Get average block fees for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + * Get average total fees per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)* * * Endpoint: `GET /api/v1/mining/blocks/fees/{time_period}` * * @param {TimePeriod} time_period + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockFeesEntry[]) => void }} [options] * @returns {Promise} */ - async getBlockFees(time_period) { - return this.getJson(`/api/v1/mining/blocks/fees/${time_period}`); + async getBlockFees(time_period, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/blocks/fees/${time_period}`, { signal, onUpdate }); } /** * Block rewards * - * Get average block rewards (coinbase = subsidy + fees) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + * Get average coinbase reward (subsidy + fees) per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)* * * Endpoint: `GET /api/v1/mining/blocks/rewards/{time_period}` * * @param {TimePeriod} time_period + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockRewardsEntry[]) => void }} [options] * @returns {Promise} */ - async getBlockRewards(time_period) { - return this.getJson(`/api/v1/mining/blocks/rewards/${time_period}`); + async getBlockRewards(time_period, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/blocks/rewards/${time_period}`, { signal, onUpdate }); } /** * Block sizes and weights * - * Get average block sizes and weights for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + * Get average block sizes and weights for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)* * * Endpoint: `GET /api/v1/mining/blocks/sizes-weights/{time_period}` * * @param {TimePeriod} time_period + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockSizesWeights) => void }} [options] * @returns {Promise} */ - async getBlockSizesWeights(time_period) { - return this.getJson(`/api/v1/mining/blocks/sizes-weights/${time_period}`); + async getBlockSizesWeights(time_period, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/blocks/sizes-weights/${time_period}`, { signal, onUpdate }); } /** @@ -10453,10 +10552,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/v1/mining/blocks/timestamp/{timestamp}` * * @param {Timestamp} timestamp + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockTimestamp) => void }} [options] * @returns {Promise} */ - async getBlockByTimestamp(timestamp) { - return this.getJson(`/api/v1/mining/blocks/timestamp/${timestamp}`); + async getBlockByTimestamp(timestamp, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/blocks/timestamp/${timestamp}`, { signal, onUpdate }); } /** @@ -10467,26 +10567,28 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)* * * Endpoint: `GET /api/v1/mining/difficulty-adjustments` + * @param {{ signal?: AbortSignal, onUpdate?: (value: DifficultyAdjustmentEntry[]) => void }} [options] * @returns {Promise} */ - async getDifficultyAdjustments() { - return this.getJson(`/api/v1/mining/difficulty-adjustments`); + async getDifficultyAdjustments({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/difficulty-adjustments`, { signal, onUpdate }); } /** * Difficulty adjustments * - * Get historical difficulty adjustments for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y. + * Get historical difficulty adjustments for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)* * * Endpoint: `GET /api/v1/mining/difficulty-adjustments/{time_period}` * * @param {TimePeriod} time_period + * @param {{ signal?: AbortSignal, onUpdate?: (value: DifficultyAdjustmentEntry[]) => void }} [options] * @returns {Promise} */ - async getDifficultyAdjustmentsByPeriod(time_period) { - return this.getJson(`/api/v1/mining/difficulty-adjustments/${time_period}`); + async getDifficultyAdjustmentsByPeriod(time_period, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/difficulty-adjustments/${time_period}`, { signal, onUpdate }); } /** @@ -10497,10 +10599,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)* * * Endpoint: `GET /api/v1/mining/hashrate` + * @param {{ signal?: AbortSignal, onUpdate?: (value: HashrateSummary) => void }} [options] * @returns {Promise} */ - async getHashrate() { - return this.getJson(`/api/v1/mining/hashrate`); + async getHashrate({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/hashrate`, { signal, onUpdate }); } /** @@ -10511,42 +10614,45 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)* * * Endpoint: `GET /api/v1/mining/hashrate/pools` + * @param {{ signal?: AbortSignal, onUpdate?: (value: PoolHashrateEntry[]) => void }} [options] * @returns {Promise} */ - async getPoolsHashrate() { - return this.getJson(`/api/v1/mining/hashrate/pools`); + async getPoolsHashrate({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/hashrate/pools`, { signal, onUpdate }); } /** * All pools hashrate * - * Get hashrate data for all mining pools for a time period. Valid periods: 1m, 3m, 6m, 1y, 2y, 3y + * Get hashrate data for all mining pools for a time period. Valid periods: `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)* * * Endpoint: `GET /api/v1/mining/hashrate/pools/{time_period}` * * @param {TimePeriod} time_period + * @param {{ signal?: AbortSignal, onUpdate?: (value: PoolHashrateEntry[]) => void }} [options] * @returns {Promise} */ - async getPoolsHashrateByPeriod(time_period) { - return this.getJson(`/api/v1/mining/hashrate/pools/${time_period}`); + async getPoolsHashrateByPeriod(time_period, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/hashrate/pools/${time_period}`, { signal, onUpdate }); } /** * Network hashrate * - * Get network hashrate and difficulty data for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + * Get network hashrate and difficulty data for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)* * * Endpoint: `GET /api/v1/mining/hashrate/{time_period}` * * @param {TimePeriod} time_period + * @param {{ signal?: AbortSignal, onUpdate?: (value: HashrateSummary) => void }} [options] * @returns {Promise} */ - async getHashrateByPeriod(time_period) { - return this.getJson(`/api/v1/mining/hashrate/${time_period}`); + async getHashrateByPeriod(time_period, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/hashrate/${time_period}`, { signal, onUpdate }); } /** @@ -10559,10 +10665,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/v1/mining/pool/{slug}` * * @param {PoolSlug} slug + * @param {{ signal?: AbortSignal, onUpdate?: (value: PoolDetail) => void }} [options] * @returns {Promise} */ - async getPool(slug) { - return this.getJson(`/api/v1/mining/pool/${slug}`); + async getPool(slug, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/pool/${slug}`, { signal, onUpdate }); } /** @@ -10575,10 +10682,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/v1/mining/pool/{slug}/blocks` * * @param {PoolSlug} slug + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockInfoV1[]) => void }} [options] * @returns {Promise} */ - async getPoolBlocks(slug) { - return this.getJson(`/api/v1/mining/pool/${slug}/blocks`); + async getPoolBlocks(slug, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/pool/${slug}/blocks`, { signal, onUpdate }); } /** @@ -10592,10 +10700,11 @@ class BrkClient extends BrkClientBase { * * @param {PoolSlug} slug * @param {Height} height + * @param {{ signal?: AbortSignal, onUpdate?: (value: BlockInfoV1[]) => void }} [options] * @returns {Promise} */ - async getPoolBlocksFrom(slug, height) { - return this.getJson(`/api/v1/mining/pool/${slug}/blocks/${height}`); + async getPoolBlocksFrom(slug, height, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/pool/${slug}/blocks/${height}`, { signal, onUpdate }); } /** @@ -10608,10 +10717,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/v1/mining/pool/{slug}/hashrate` * * @param {PoolSlug} slug + * @param {{ signal?: AbortSignal, onUpdate?: (value: PoolHashrateEntry[]) => void }} [options] * @returns {Promise} */ - async getPoolHashrate(slug) { - return this.getJson(`/api/v1/mining/pool/${slug}/hashrate`); + async getPoolHashrate(slug, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/pool/${slug}/hashrate`, { signal, onUpdate }); } /** @@ -10622,26 +10732,28 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)* * * Endpoint: `GET /api/v1/mining/pools` + * @param {{ signal?: AbortSignal, onUpdate?: (value: PoolInfo[]) => void }} [options] * @returns {Promise} */ - async getPools() { - return this.getJson(`/api/v1/mining/pools`); + async getPools({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/pools`, { signal, onUpdate }); } /** * Mining pool statistics * - * Get mining pool statistics for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + * Get mining pool statistics for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)* * * Endpoint: `GET /api/v1/mining/pools/{time_period}` * * @param {TimePeriod} time_period + * @param {{ signal?: AbortSignal, onUpdate?: (value: PoolsSummary) => void }} [options] * @returns {Promise} */ - async getPoolStats(time_period) { - return this.getJson(`/api/v1/mining/pools/${time_period}`); + async getPoolStats(time_period, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/pools/${time_period}`, { signal, onUpdate }); } /** @@ -10654,10 +10766,11 @@ class BrkClient extends BrkClientBase { * Endpoint: `GET /api/v1/mining/reward-stats/{block_count}` * * @param {number} block_count - Number of recent blocks to include + * @param {{ signal?: AbortSignal, onUpdate?: (value: RewardStats) => void }} [options] * @returns {Promise} */ - async getRewardStats(block_count) { - return this.getJson(`/api/v1/mining/reward-stats/${block_count}`); + async getRewardStats(block_count, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/mining/reward-stats/${block_count}`, { signal, onUpdate }); } /** @@ -10668,10 +10781,11 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-price)* * * Endpoint: `GET /api/v1/prices` + * @param {{ signal?: AbortSignal, onUpdate?: (value: Prices) => void }} [options] * @returns {Promise} */ - async getPrices() { - return this.getJson(`/api/v1/prices`); + async getPrices({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/prices`, { signal, onUpdate }); } /** @@ -10682,26 +10796,28 @@ class BrkClient extends BrkClientBase { * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-transaction-times)* * * Endpoint: `GET /api/v1/transaction-times` + * @param {{ signal?: AbortSignal, onUpdate?: (value: number[]) => void }} [options] * @returns {Promise} */ - async getTransactionTimes() { - return this.getJson(`/api/v1/transaction-times`); + async getTransactionTimes({ signal, onUpdate } = {}) { + return this.getJson(`/api/v1/transaction-times`, { signal, onUpdate }); } /** * Validate address * - * Validate a Bitcoin address and get information about its type and scriptPubKey. + * Validate a Bitcoin address and get information about its type and scriptPubKey. Returns `isvalid: false` with an error message for invalid addresses. * * *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)* * * Endpoint: `GET /api/v1/validate-address/{address}` * * @param {string} address - Bitcoin address to validate (can be any string) + * @param {{ signal?: AbortSignal, onUpdate?: (value: AddrValidation) => void }} [options] * @returns {Promise} */ - async validateAddress(address) { - return this.getJson(`/api/v1/validate-address/${address}`); + async validateAddress(address, { signal, onUpdate } = {}) { + return this.getJson(`/api/v1/validate-address/${address}`, { signal, onUpdate }); } /** @@ -10710,10 +10826,11 @@ class BrkClient extends BrkClientBase { * Returns the health status of the API server, including uptime information. * * Endpoint: `GET /health` + * @param {{ signal?: AbortSignal, onUpdate?: (value: Health) => void }} [options] * @returns {Promise} */ - async getHealth() { - return this.getJson(`/health`); + async getHealth({ signal, onUpdate } = {}) { + return this.getJson(`/health`, { signal, onUpdate }); } /** @@ -10722,10 +10839,11 @@ class BrkClient extends BrkClientBase { * Full OpenAPI 3.1 specification for this API. * * Endpoint: `GET /openapi.json` + * @param {{ signal?: AbortSignal, onUpdate?: (value: *) => void }} [options] * @returns {Promise<*>} */ - async getOpenapi() { - return this.getJson(`/openapi.json`); + async getOpenapi({ signal, onUpdate } = {}) { + return this.getJson(`/openapi.json`, { signal, onUpdate }); } /** @@ -10734,10 +10852,11 @@ class BrkClient extends BrkClientBase { * Returns the current version of the API server * * Endpoint: `GET /version` + * @param {{ signal?: AbortSignal, onUpdate?: (value: string) => void }} [options] * @returns {Promise} */ - async getVersion() { - return this.getJson(`/version`); + async getVersion({ signal, onUpdate } = {}) { + return this.getJson(`/version`, { signal, onUpdate }); } } diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index fded1b176..e507c522e 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -19,10 +19,14 @@ T = TypeVar('T') # Bitcoin address string Addr = str -# Satoshis +# US Dollar amount +Dollars = float +# Amount in satoshis (1 BTC = 100,000,000 sats) Sats = int # Index within its type (e.g., 0 for first P2WPKH address) TypeIndex = int +# Type (P2PKH, P2WPKH, P2SH, P2TR, etc.) +OutputType = Literal["p2pk", "p2pk", "p2pkh", "multisig", "p2sh", "op_return", "v0_p2wpkh", "v0_p2wsh", "v1_p2tr", "p2a", "empty", "unknown"] # Transaction ID (hash) Txid = str # Unified index for any address type (funded or empty) @@ -49,12 +53,11 @@ BasisPointsSigned16 = int BasisPointsSigned32 = int # Bitcoin amount as floating point (1 BTC = 100,000,000 satoshis) Bitcoin = float +# URL-friendly mining pool identifier 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"] -# US Dollar amount as floating point -Dollars = float -# Fee rate in sats/vB +# Fee rate in sat/vB FeeRate = float -# Transaction or block weight in weight units (WU) +# Weight in weight units (WU). Max block weight is 4,000,000 WU. Weight = int # Block height Height = int @@ -62,6 +65,7 @@ Height = int Timestamp = int # Block hash BlockHash = str +# Transaction index within a block (0 = coinbase) TxIndex = int # Unsigned cents (u64) - for values that should never be negative. # Used for invested capital, realized cap, etc. @@ -95,7 +99,7 @@ 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) +# Virtual size in vbytes (weight / 4, rounded up). Max block vsize is ~1,000,000 vB. VSize = int # Date in YYYYMMDD format stored as u32 Date = int @@ -132,8 +136,6 @@ Month6 = int Open = Dollars OpReturnIndex = TypeIndex OutPoint = int -# Type (P2PKH, P2WPKH, P2SH, P2TR, etc.) -OutputType = Literal["p2pk", "p2pk", "p2pkh", "multisig", "p2sh", "op_return", "v0_p2wpkh", "v0_p2wsh", "v1_p2tr", "p2a", "empty", "unknown"] P2AAddrIndex = TypeIndex U8x2 = List[int] P2ABytes = U8x2 @@ -156,7 +158,7 @@ P2WPKHAddrIndex = TypeIndex P2WPKHBytes = U8x20 P2WSHAddrIndex = TypeIndex P2WSHBytes = U8x32 -# Transaction locktime +# Transaction locktime. Values below 500,000,000 are interpreted as block heights; values at or above are Unix timestamps. RawLockTime = int # Fractional satoshis (f64) - for representing USD prices in sats # @@ -230,6 +232,7 @@ class AddrChainStats(TypedDict): spent_txo_sum: Total amount in satoshis spent from this address tx_count: Total number of confirmed transactions involving this address type_index: Index of this address within its type on the blockchain + realized_price: Realized price (average cost basis) in USD """ funded_txo_count: int funded_txo_sum: Sats @@ -237,6 +240,7 @@ class AddrChainStats(TypedDict): spent_txo_sum: Sats tx_count: int type_index: TypeIndex + realized_price: Dollars class AddrMempoolStats(TypedDict): """ @@ -258,6 +262,9 @@ class AddrMempoolStats(TypedDict): tx_count: int class AddrParam(TypedDict): + """ + Bitcoin address path parameter + """ address: Addr class AddrStats(TypedDict): @@ -266,10 +273,12 @@ class AddrStats(TypedDict): Attributes: address: Bitcoin address string + addr_type: Address type (p2pkh, p2sh, v0_p2wpkh, v0_p2wsh, v1_p2tr, etc.) chain_stats: Statistics for confirmed transactions on the blockchain mempool_stats: Statistics for unconfirmed transactions in the mempool """ address: Addr + addr_type: OutputType chain_stats: AddrChainStats mempool_stats: Union[AddrMempoolStats, None] @@ -307,6 +316,8 @@ class AddrValidation(TypedDict): class BlockCountParam(TypedDict): """ + Block count path parameter + Attributes: block_count: Number of recent blocks to include """ @@ -401,6 +412,13 @@ class BlockFeeRatesEntry(TypedDict): Attributes: avgHeight: Average block height in this window timestamp: Unix timestamp at the window midpoint + avgFee_0: Minimum fee rate (sat/vB) + avgFee_10: 10th percentile fee rate (sat/vB) + avgFee_25: 25th percentile fee rate (sat/vB) + avgFee_50: Median fee rate (sat/vB) + avgFee_75: 75th percentile fee rate (sat/vB) + avgFee_90: 90th percentile fee rate (sat/vB) + avgFee_100: Maximum fee rate (sat/vB) """ avgHeight: Height timestamp: Timestamp @@ -428,10 +446,15 @@ class BlockFeesEntry(TypedDict): USD: Dollars class BlockHashParam(TypedDict): + """ + Block hash path parameter + """ hash: BlockHash class BlockHashStartIndex(TypedDict): """ + Block hash + starting transaction index path parameters + Attributes: hash: Bitcoin block hash start_index: Starting transaction index within the block (0-based) @@ -441,6 +464,8 @@ class BlockHashStartIndex(TypedDict): class BlockHashTxIndex(TypedDict): """ + Block hash + transaction index path parameters + Attributes: hash: Bitcoin block hash index: Transaction index within the block (0-based) @@ -831,8 +856,8 @@ class HashrateEntry(TypedDict): A single hashrate data point. Attributes: - timestamp: Unix timestamp. - avgHashrate: Average hashrate (H/s). + timestamp: Unix timestamp + avgHashrate: Average hashrate (H/s) """ timestamp: Timestamp avgHashrate: int @@ -842,10 +867,10 @@ class HashrateSummary(TypedDict): Summary of network hashrate and difficulty data. Attributes: - hashrates: Historical hashrate data points. - difficulty: Historical difficulty adjustments. - currentHashrate: Current network hashrate (H/s). - currentDifficulty: Current network difficulty. + hashrates: Historical hashrate data points + difficulty: Historical difficulty adjustments + currentHashrate: Current network hashrate (H/s) + currentDifficulty: Current network difficulty """ hashrates: List[HashrateEntry] difficulty: List[DifficultyEntry] @@ -884,6 +909,9 @@ class Health(TypedDict): last_indexed_at_unix: Timestamp class HeightParam(TypedDict): + """ + Block height path parameter + """ height: Height class HistoricalPriceEntry(TypedDict): @@ -894,7 +922,7 @@ class HistoricalPriceEntry(TypedDict): time: Unix timestamp USD: BTC/USD price """ - time: int + time: Timestamp USD: Dollars class HistoricalPrice(TypedDict): @@ -988,7 +1016,7 @@ class MerkleProof(TypedDict): Attributes: block_height: Block height containing the transaction merkle: Merkle proof path (hex-encoded hashes) - pos: Transaction position in the block + pos: Transaction position in the block (0-indexed) """ block_height: Height merkle: List[str] @@ -1022,6 +1050,9 @@ class OHLCSats(TypedDict): close: Close class OptionalTimestampParam(TypedDict): + """ + Optional UNIX timestamp query parameter + """ timestamp: Union[Timestamp, None] class PaginatedSeries(TypedDict): @@ -1073,8 +1104,8 @@ class PoolBlockShares(TypedDict): Attributes: all: Share of all blocks (0.0 - 1.0) - _24h: Share of blocks in last 24 hours - _1w: Share of blocks in last week + _24h: Share of blocks in last 24 hours (0.0 - 1.0) + _1w: Share of blocks in last week (0.0 - 1.0) """ all: float _24h: float @@ -1109,8 +1140,8 @@ class PoolDetail(TypedDict): pool: Pool information blockCount: Block counts for different time periods blockShare: Pool's share of total blocks for different time periods - estimatedHashrate: Estimated hashrate based on blocks mined - reportedHashrate: Self-reported hashrate (if available) + estimatedHashrate: Estimated hashrate based on blocks mined (H/s) + reportedHashrate: Self-reported hashrate (if available, H/s) totalReward: Total reward earned by this pool (sats, all time; None for minor pools) """ pool: PoolDetailInfo @@ -1125,10 +1156,10 @@ class PoolHashrateEntry(TypedDict): A single pool hashrate data point. Attributes: - timestamp: Unix timestamp. - avgHashrate: Average hashrate (H/s). - share: Pool's share of total network hashrate. - poolName: Pool name. + timestamp: Unix timestamp + avgHashrate: Average hashrate (H/s) + share: Pool's share of total network hashrate (0.0 - 1.0) + poolName: Pool name """ timestamp: Timestamp avgHashrate: int @@ -1149,10 +1180,16 @@ class PoolInfo(TypedDict): unique_id: int class PoolSlugAndHeightParam(TypedDict): + """ + Mining pool slug + block height path parameters + """ slug: PoolSlug height: Height class PoolSlugParam(TypedDict): + """ + Mining pool slug path parameter + """ slug: PoolSlug class PoolStats(TypedDict): @@ -1187,9 +1224,9 @@ class PoolsSummary(TypedDict): Attributes: 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 + lastEstimatedHashrate: Estimated network hashrate (H/s) + lastEstimatedHashrate3d: Estimated network hashrate over last 3 days (H/s) + lastEstimatedHashrate1w: Estimated network hashrate over last 1 week (H/s) """ pools: List[PoolStats] blockCount: int @@ -1341,9 +1378,15 @@ class SyncStatus(TypedDict): last_indexed_at_unix: Timestamp class TimePeriodParam(TypedDict): + """ + Time period path parameter (24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y) + """ time_period: TimePeriod class TimestampParam(TypedDict): + """ + UNIX timestamp path parameter + """ timestamp: Timestamp class TxOut(TypedDict): @@ -1444,6 +1487,9 @@ class TxOutspend(TypedDict): status: Union[TxStatus, None] class TxidParam(TypedDict): + """ + Transaction ID path parameter + """ txid: Txid class TxidVout(TypedDict): @@ -7296,7 +7342,7 @@ class BrkClient(BrkClientBase): def get_block_header(self, hash: BlockHash) -> str: """Block header. - Returns the hex-encoded block header. + Returns the hex-encoded 80-byte block header. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-header)* @@ -7738,7 +7784,7 @@ class BrkClient(BrkClientBase): def get_cpfp(self, txid: Txid) -> CpfpInfo: """CPFP info. - Returns ancestors and descendants for a CPFP transaction. + Returns ancestors and descendants for a CPFP (Child Pays For Parent) transaction, including the effective fee rate of the package. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-children-pay-for-parent)* @@ -7802,7 +7848,7 @@ class BrkClient(BrkClientBase): def get_block_fee_rates(self, time_period: TimePeriod) -> List[BlockFeeRatesEntry]: """Block fee rates. - Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + Get block fee rate percentiles (min, 10th, 25th, median, 75th, 90th, max) for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-feerates)* @@ -7812,7 +7858,7 @@ class BrkClient(BrkClientBase): def get_block_fees(self, time_period: TimePeriod) -> List[BlockFeesEntry]: """Block fees. - Get average block fees for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + Get average total fees per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-fees)* @@ -7822,7 +7868,7 @@ class BrkClient(BrkClientBase): def get_block_rewards(self, time_period: TimePeriod) -> List[BlockRewardsEntry]: """Block rewards. - Get average block rewards (coinbase = subsidy + fees) for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + Get average coinbase reward (subsidy + fees) per block for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-rewards)* @@ -7832,7 +7878,7 @@ class BrkClient(BrkClientBase): def get_block_sizes_weights(self, time_period: TimePeriod) -> BlockSizesWeights: """Block sizes and weights. - Get average block sizes and weights for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + Get average block sizes and weights for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-sizes-weights)* @@ -7862,7 +7908,7 @@ class BrkClient(BrkClientBase): def get_difficulty_adjustments_by_period(self, time_period: TimePeriod) -> List[DifficultyAdjustmentEntry]: """Difficulty adjustments. - Get historical difficulty adjustments for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y. + Get historical difficulty adjustments for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-difficulty-adjustments)* @@ -7892,7 +7938,7 @@ class BrkClient(BrkClientBase): def get_pools_hashrate_by_period(self, time_period: TimePeriod) -> List[PoolHashrateEntry]: """All pools hashrate. - Get hashrate data for all mining pools for a time period. Valid periods: 1m, 3m, 6m, 1y, 2y, 3y + Get hashrate data for all mining pools for a time period. Valid periods: `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pool-hashrates)* @@ -7902,7 +7948,7 @@ class BrkClient(BrkClientBase): def get_hashrate_by_period(self, time_period: TimePeriod) -> HashrateSummary: """Network hashrate. - Get network hashrate and difficulty data for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + Get network hashrate and difficulty data for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-hashrate)* @@ -7962,7 +8008,7 @@ class BrkClient(BrkClientBase): def get_pool_stats(self, time_period: TimePeriod) -> PoolsSummary: """Mining pool statistics. - Get mining pool statistics for a time period. Valid periods: 24h, 3d, 1w, 1m, 3m, 6m, 1y, 2y, 3y + Get mining pool statistics for a time period. Valid periods: `24h`, `3d`, `1w`, `1m`, `3m`, `6m`, `1y`, `2y`, `3y`. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-mining-pools)* @@ -8002,7 +8048,7 @@ class BrkClient(BrkClientBase): def validate_address(self, address: str) -> AddrValidation: """Validate address. - Validate a Bitcoin address and get information about its type and scriptPubKey. + Validate a Bitcoin address and get information about its type and scriptPubKey. Returns `isvalid: false` with an error message for invalid addresses. *[Mempool.space docs](https://mempool.space/docs/api/rest#get-address-validate)* diff --git a/website/.gitignore b/website/.gitignore index 5273bf3cf..88a3873f0 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -3,3 +3,4 @@ *_old.js *dump* TODO.md +_explorer.js diff --git a/website/scripts/_types.js b/website/scripts/_types.js index 55d2200cc..ea75b727f 100644 --- a/website/scripts/_types.js +++ b/website/scripts/_types.js @@ -7,6 +7,7 @@ * @import { Options } from './options/full.js' * * @import { PersistedValue } from './utils/persisted.js' + * @import { MapCache } from './utils/cache.js' * * @import { SingleValueData, CandlestickData, Series, AnySeries, ISeries, HistogramData, LineData, BaselineData, LineSeriesPartialOptions, BaselineSeriesPartialOptions, HistogramSeriesPartialOptions, CandlestickSeriesPartialOptions, Chart, Legend } from "./utils/chart/index.js" * @@ -57,6 +58,9 @@ * @typedef {Brk.BlockHash} BlockHash * @typedef {Brk.BlockInfoV1} BlockInfoV1 * @typedef {Brk.Transaction} Transaction + * @typedef {Brk.AddrStats} AddrStats + * @typedef {Brk.TxIn} TxIn + * @typedef {Brk.TxOut} TxOut * ActivePriceRatioPattern: ratio pattern with price (extended) * @typedef {Brk.BpsPriceRatioPattern} ActivePriceRatioPattern * PriceRatioPercentilesPattern: price pattern with ratio + percentiles (no SMAs/stdDev) diff --git a/website/scripts/explorer/address.js b/website/scripts/explorer/address.js new file mode 100644 index 000000000..b21970e0a --- /dev/null +++ b/website/scripts/explorer/address.js @@ -0,0 +1,115 @@ +import { brk } from "../utils/client.js"; +import { createMapCache } from "../utils/cache.js"; +import { latestPrice } from "../utils/price.js"; +import { formatBtc, renderRows, renderTx, TX_PAGE_SIZE } from "./render.js"; + +/** @type {MapCache} */ +const addrTxCache = createMapCache(200); + +/** + * @param {string} address + * @param {HTMLDivElement} el + * @param {{ signal: AbortSignal, cache: MapCache }} options + */ +export async function showAddrDetail(address, el, { signal, cache }) { + el.hidden = false; + el.scrollTop = 0; + el.innerHTML = ""; + + try { + const cached = cache.get(address); + const stats = cached ?? (await brk.getAddress(address, { signal })); + if (!cached) cache.set(address, stats); + if (signal.aborted) return; + const chain = stats.chainStats; + + const title = document.createElement("h1"); + title.textContent = "Address"; + el.append(title); + + const balance = chain.fundedTxoSum - chain.spentTxoSum; + const mempool = stats.mempoolStats; + const pending = mempool ? mempool.fundedTxoSum - mempool.spentTxoSum : 0; + const pendingUtxos = mempool + ? mempool.fundedTxoCount - mempool.spentTxoCount + : 0; + const confirmedUtxos = chain.fundedTxoCount - chain.spentTxoCount; + const price = latestPrice(); + const fmtUsd = (/** @type {number} */ sats) => + price + ? ` $${((sats / 1e8) * price).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + : ""; + + renderRows( + [ + ["Address", address], + ["Confirmed Balance", `${formatBtc(balance)} BTC${fmtUsd(balance)}`], + [ + "Pending", + `${pending >= 0 ? "+" : ""}${formatBtc(pending)} BTC${fmtUsd(pending)}`, + ], + ["Confirmed UTXOs", confirmedUtxos.toLocaleString()], + ["Pending UTXOs", pendingUtxos.toLocaleString()], + ["Total Received", `${formatBtc(chain.fundedTxoSum)} BTC`], + ["Tx Count", chain.txCount.toLocaleString()], + [ + "Type", + /** @type {any} */ ((stats).addrType ?? "unknown") + .replace(/^v\d+_/, "") + .toUpperCase(), + ], + [ + "Avg Cost Basis", + chain.realizedPrice + ? `$${Number(chain.realizedPrice).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` + : "N/A", + ], + ], + el, + ); + + const section = document.createElement("div"); + section.classList.add("transactions"); + const heading = document.createElement("h2"); + heading.textContent = "Transactions"; + section.append(heading); + el.append(section); + + let loading = false; + let pageIndex = 0; + /** @type {string | undefined} */ + let afterTxid; + + const observer = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && !loading && pageIndex * TX_PAGE_SIZE < chain.txCount) + loadMore(); + }); + + async function loadMore() { + loading = true; + const key = `${address}:${pageIndex}`; + try { + const cached = addrTxCache.get(key); + const txs = cached ?? await brk.getAddressTxs(address, afterTxid, { signal }); + if (!cached) addrTxCache.set(key, txs); + for (const tx of txs) section.append(renderTx(tx)); + pageIndex++; + if (txs.length) { + afterTxid = txs[txs.length - 1].txid; + observer.disconnect(); + const last = section.lastElementChild; + if (last) observer.observe(last); + } + } catch (e) { + console.error("explorer addr txs:", e); + pageIndex = chain.txCount; // stop loading + } + loading = false; + } + + await loadMore(); + } catch (e) { + console.error("explorer addr:", e); + el.textContent = "Address not found"; + } +} diff --git a/website/scripts/explorer/block.js b/website/scripts/explorer/block.js new file mode 100644 index 000000000..e13c83fc8 --- /dev/null +++ b/website/scripts/explorer/block.js @@ -0,0 +1,230 @@ +import { brk } from "../utils/client.js"; +import { createMapCache } from "../utils/cache.js"; +import { createPersistedValue } from "../utils/persisted.js"; +import { formatFeeRate, renderTx, TX_PAGE_SIZE } from "./render.js"; + +/** @typedef {[string, (b: BlockInfoV1) => string | null, ((b: BlockInfoV1) => string | null)?]} RowDef */ + +/** @param {(x: NonNullable) => string | null} fn @returns {(b: BlockInfoV1) => string | null} */ +const ext = (fn) => (b) => (b.extras ? fn(b.extras) : null); + +/** @type {RowDef[]} */ +const ROW_DEFS = [ + ["Hash", (b) => b.id, (b) => `/block/${b.id}`], + ["Previous Hash", (b) => b.previousblockhash, (b) => `/block/${b.previousblockhash}`], + ["Merkle Root", (b) => b.merkleRoot], + ["Timestamp", (b) => new Date(b.timestamp * 1000).toUTCString()], + ["Median Time", (b) => new Date(b.mediantime * 1000).toUTCString()], + ["Version", (b) => `0x${b.version.toString(16)}`], + ["Bits", (b) => b.bits.toString(16)], + ["Nonce", (b) => b.nonce.toLocaleString()], + ["Difficulty", (b) => Number(b.difficulty).toLocaleString()], + ["Size", (b) => `${(b.size / 1_000_000).toFixed(2)} MB`], + ["Weight", (b) => `${(b.weight / 1_000_000).toFixed(2)} MWU`], + ["Transactions", (b) => b.txCount.toLocaleString()], + ["Price", ext((x) => `$${x.price.toLocaleString()}`)], + ["Pool", ext((x) => x.pool.name)], + ["Pool ID", ext((x) => x.pool.id.toString())], + ["Pool Slug", ext((x) => x.pool.slug)], + ["Miner Names", ext((x) => x.pool.minerNames?.join(", ") || null)], + ["Reward", ext((x) => `${(x.reward / 1e8).toFixed(8)} BTC`)], + ["Total Fees", ext((x) => `${(x.totalFees / 1e8).toFixed(8)} BTC`)], + ["Median Fee Rate", ext((x) => `${formatFeeRate(x.medianFee)} sat/vB`)], + ["Avg Fee Rate", ext((x) => `${formatFeeRate(x.avgFeeRate)} sat/vB`)], + ["Avg Fee", ext((x) => `${x.avgFee.toLocaleString()} sat`)], + ["Median Fee", ext((x) => `${x.medianFeeAmt.toLocaleString()} sat`)], + ["Fee Range", ext((x) => x.feeRange.map((f) => formatFeeRate(f)).join(", ") + " sat/vB")], + ["Fee Percentiles", ext((x) => x.feePercentiles.map((f) => f.toLocaleString()).join(", ") + " sat")], + ["Avg Tx Size", ext((x) => `${x.avgTxSize.toLocaleString()} B`)], + ["Virtual Size", ext((x) => `${x.virtualSize.toLocaleString()} vB`)], + ["Inputs", ext((x) => x.totalInputs.toLocaleString())], + ["Outputs", ext((x) => x.totalOutputs.toLocaleString())], + ["Total Input Amount", ext((x) => `${(x.totalInputAmt / 1e8).toFixed(8)} BTC`)], + ["Total Output Amount", ext((x) => `${(x.totalOutputAmt / 1e8).toFixed(8)} BTC`)], + ["UTXO Set Change", ext((x) => x.utxoSetChange.toLocaleString())], + ["UTXO Set Size", ext((x) => x.utxoSetSize.toLocaleString())], + ["SegWit Txs", ext((x) => x.segwitTotalTxs.toLocaleString())], + ["SegWit Size", ext((x) => `${x.segwitTotalSize.toLocaleString()} B`)], + ["SegWit Weight", ext((x) => `${x.segwitTotalWeight.toLocaleString()} WU`)], + ["Coinbase Address", ext((x) => x.coinbaseAddress || null)], + ["Coinbase Addresses", ext((x) => x.coinbaseAddresses.join(", ") || null)], + ["Coinbase Raw", ext((x) => x.coinbaseRaw)], + ["Coinbase Signature", ext((x) => x.coinbaseSignature)], + ["Coinbase Signature ASCII", ext((x) => x.coinbaseSignatureAscii)], + ["Header", ext((x) => x.header)], +]; + +/** @typedef {{ first: HTMLButtonElement, prev: HTMLButtonElement, label: HTMLSpanElement, next: HTMLButtonElement, last: HTMLButtonElement }} TxNav */ + +/** @type {HTMLDivElement} */ let el; +/** @type {HTMLSpanElement} */ let heightPrefix; +/** @type {HTMLSpanElement} */ let heightNum; +/** @type {{ row: HTMLDivElement, valueEl: HTMLSpanElement }[]} */ let detailRows; +/** @type {HTMLDivElement} */ let txList; +/** @type {HTMLDivElement} */ let txSection; +/** @type {IntersectionObserver} */ let txObserver; +/** @type {TxNav[]} */ let txNavs = []; +/** @type {BlockInfoV1 | null} */ let txBlock = null; +let txTotalPages = 0; +let txLoading = false; +let txLoaded = false; +const txPageCache = createMapCache(200); + +const txPageParam = createPersistedValue({ + defaultValue: 0, + urlKey: "page", + serialize: (v) => String(v + 1), + deserialize: (s) => Math.max(0, Number(s) - 1), +}); + +/** @param {HTMLElement} parent @param {(e: MouseEvent) => void} linkHandler */ +export function initBlockDetails(parent, linkHandler) { + el = document.createElement("div"); + el.id = "block-details"; + parent.append(el); + + const title = document.createElement("h1"); + title.textContent = "Block "; + const code = document.createElement("code"); + const container = document.createElement("span"); + heightPrefix = document.createElement("span"); + heightPrefix.style.opacity = "0.5"; + heightPrefix.style.userSelect = "none"; + heightNum = document.createElement("span"); + container.append(heightPrefix, heightNum); + code.append(container); + title.append(code); + el.append(title); + + el.addEventListener("click", linkHandler); + + detailRows = ROW_DEFS.map(([label, , linkFn]) => { + const row = document.createElement("div"); + row.classList.add("row"); + const labelEl = document.createElement("span"); + labelEl.classList.add("label"); + labelEl.textContent = label; + const valueEl = document.createElement(linkFn ? "a" : "span"); + valueEl.classList.add("value"); + row.append(labelEl, valueEl); + el.append(row); + return { row, valueEl }; + }); + + txSection = document.createElement("div"); + txSection.classList.add("transactions"); + el.append(txSection); + + const txHeader = document.createElement("div"); + txHeader.classList.add("tx-header"); + const heading = document.createElement("h2"); + heading.textContent = "Transactions"; + txHeader.append(heading, createTxNav()); + txSection.append(txHeader); + + txList = document.createElement("div"); + txList.classList.add("tx-list"); + txSection.append(txList, createTxNav()); + + txObserver = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting && !txLoaded) { + loadTxPage(txPageParam.value, false); + } + }); + txObserver.observe(txSection); +} + +function createTxNav() { + const nav = document.createElement("div"); + nav.classList.add("pagination"); + const first = document.createElement("button"); + first.textContent = "\u00AB"; + const prev = document.createElement("button"); + prev.textContent = "\u2190"; + const label = document.createElement("span"); + const next = document.createElement("button"); + next.textContent = "\u2192"; + const last = document.createElement("button"); + last.textContent = "\u00BB"; + nav.append(first, prev, label, next, last); + first.addEventListener("click", () => loadTxPage(0)); + prev.addEventListener("click", () => loadTxPage(txPageParam.value - 1)); + next.addEventListener("click", () => loadTxPage(txPageParam.value + 1)); + last.addEventListener("click", () => loadTxPage(txTotalPages - 1)); + txNavs.push({ first, prev, label, next, last }); + return nav; +} + +/** @param {number} page */ +function updateTxNavs(page) { + const atFirst = page <= 0; + const atLast = page >= txTotalPages - 1; + for (const n of txNavs) { + n.label.textContent = `${page + 1} / ${txTotalPages}`; + n.first.disabled = atFirst; + n.prev.disabled = atFirst; + n.next.disabled = atLast; + n.last.disabled = atLast; + } +} + +/** @param {BlockInfoV1} block */ +export function update(block) { + show(); + el.scrollTop = 0; + + const str = block.height.toString(); + heightPrefix.textContent = "#" + "0".repeat(7 - str.length); + heightNum.textContent = str; + + ROW_DEFS.forEach(([, getter, linkFn], i) => { + const value = getter(block); + const { row, valueEl } = detailRows[i]; + if (value !== null) { + valueEl.textContent = value; + if (linkFn) + /** @type {HTMLAnchorElement} */ (valueEl).href = linkFn(block) ?? ""; + row.hidden = false; + } else { + row.hidden = true; + } + }); + + txBlock = block; + txTotalPages = Math.ceil(block.txCount / TX_PAGE_SIZE); + if (txLoaded) txPageParam.setImmediate(0); + txLoaded = false; + updateTxNavs(txPageParam.value); + txList.innerHTML = ""; + txObserver.disconnect(); + txObserver.observe(txSection); +} + +export function show() { + el.hidden = false; +} + +export function hide() { + el.hidden = true; +} + +/** @param {number} page @param {boolean} [pushUrl] */ +async function loadTxPage(page, pushUrl = true) { + if (txLoading || !txBlock || page < 0 || page >= txTotalPages) return; + txLoading = true; + txLoaded = true; + if (pushUrl) txPageParam.setImmediate(page); + updateTxNavs(page); + const key = `${txBlock.id}:${page}`; + try { + const cached = txPageCache.get(key); + const txs = cached ?? await brk.getBlockTxsFromIndex(txBlock.id, page * TX_PAGE_SIZE); + if (!cached) txPageCache.set(key, txs); + txList.innerHTML = ""; + const ascii = txBlock.extras?.coinbaseSignatureAscii; + for (const tx of txs) txList.append(renderTx(tx, ascii)); + } catch (e) { + console.error("explorer txs:", e); + } + txLoading = false; +} diff --git a/website/scripts/explorer/chain.js b/website/scripts/explorer/chain.js new file mode 100644 index 000000000..5d23066eb --- /dev/null +++ b/website/scripts/explorer/chain.js @@ -0,0 +1,240 @@ +import { brk } from "../utils/client.js"; +import { createHeightElement, formatFeeRate } from "./render.js"; + +const LOOKAHEAD = 15; + +/** @type {HTMLDivElement} */ let chainEl; +/** @type {HTMLDivElement} */ let blocksEl; +/** @type {HTMLDivElement | null} */ let selectedCube = null; +/** @type {IntersectionObserver} */ let olderObserver; +/** @type {(block: BlockInfoV1) => void} */ let onSelect = () => {}; +/** @type {(cube: HTMLDivElement) => void} */ let onCubeClick = () => {}; + +/** @type {Map} */ +const blocksByHash = new Map(); + +let newestHeight = -1; +let oldestHeight = Infinity; +let loadingOlder = false; +let loadingNewer = false; +let reachedTip = false; + +/** + * @param {HTMLElement} parent + * @param {{ onSelect: (block: BlockInfoV1) => void, onCubeClick: (cube: HTMLDivElement) => void }} callbacks + */ +export function initChain(parent, callbacks) { + onSelect = callbacks.onSelect; + onCubeClick = callbacks.onCubeClick; + + chainEl = document.createElement("div"); + chainEl.id = "chain"; + parent.append(chainEl); + + blocksEl = document.createElement("div"); + blocksEl.classList.add("blocks"); + chainEl.append(blocksEl); + + olderObserver = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting) loadOlder(); + }, + { root: chainEl }, + ); + + chainEl.addEventListener( + "scroll", + () => { + const nearStart = + (chainEl.scrollHeight > chainEl.clientHeight && + chainEl.scrollTop <= 50) || + (chainEl.scrollWidth > chainEl.clientWidth && + chainEl.scrollLeft <= 50); + if (nearStart && !reachedTip && !loadingNewer) loadNewer(); + }, + { passive: true }, + ); +} + +/** @param {string} hash */ +export function getBlock(hash) { + return blocksByHash.get(hash); +} + +/** @param {string} hash */ +export function findCube(hash) { + return /** @type {HTMLDivElement | null} */ ( + blocksEl.querySelector(`[data-hash="${hash}"]`) + ); +} + +export function lastCube() { + return /** @type {HTMLDivElement | null} */ (blocksEl.lastElementChild); +} + +/** @param {HTMLDivElement} cube @param {{ scroll?: boolean }} [opts] */ +export function selectCube(cube, { scroll = false } = {}) { + const changed = cube !== selectedCube; + if (changed) { + if (selectedCube) selectedCube.classList.remove("selected"); + selectedCube = cube; + cube.classList.add("selected"); + } + if (scroll) cube.scrollIntoView({ behavior: "smooth" }); + const hash = cube.dataset.hash; + if (hash) { + const block = blocksByHash.get(hash); + if (block) onSelect(block); + } +} + +export function clear() { + newestHeight = -1; + oldestHeight = Infinity; + loadingOlder = false; + loadingNewer = false; + reachedTip = false; + selectedCube = null; + blocksEl.innerHTML = ""; + olderObserver.disconnect(); +} + +function observeOldestEdge() { + olderObserver.disconnect(); + const oldest = blocksEl.firstElementChild; + if (oldest) olderObserver.observe(oldest); +} + +/** @param {BlockInfoV1[]} blocks */ +function appendNewerBlocks(blocks) { + if (!blocks.length) return false; + const anchor = blocksEl.lastElementChild; + const anchorRect = anchor?.getBoundingClientRect(); + for (let i = blocks.length - 1; i >= 0; i--) { + const b = blocks[i]; + if (b.height > newestHeight) { + blocksEl.append(createBlockCube(b)); + } else { + blocksByHash.set(b.id, b); + } + } + newestHeight = Math.max(newestHeight, blocks[0].height); + if (anchor && anchorRect) { + const r = anchor.getBoundingClientRect(); + chainEl.scrollTop += r.top - anchorRect.top; + chainEl.scrollLeft += r.left - anchorRect.left; + } + return true; +} + +/** @param {number | null} [height] */ +export async function loadInitial(height) { + const blocks = + height != null + ? await brk.getBlocksV1FromHeight(height) + : await brk.getBlocksV1(); + + for (const b of blocks) blocksEl.prepend(createBlockCube(b)); + newestHeight = blocks[0].height; + oldestHeight = blocks[blocks.length - 1].height; + reachedTip = height == null; + observeOldestEdge(); + if (!reachedTip) await loadNewer(); +} + +export async function poll() { + if (newestHeight === -1 || !reachedTip) return; + try { + const blocks = await brk.getBlocksV1(); + appendNewerBlocks(blocks); + } catch (e) { + console.error("explorer poll:", e); + } +} + +async function loadOlder() { + if (loadingOlder || oldestHeight <= 0) return; + loadingOlder = true; + try { + const blocks = await brk.getBlocksV1FromHeight(oldestHeight - 1); + for (const block of blocks) blocksEl.prepend(createBlockCube(block)); + if (blocks.length) { + oldestHeight = blocks[blocks.length - 1].height; + observeOldestEdge(); + } + } catch (e) { + console.error("explorer loadOlder:", e); + } + loadingOlder = false; +} + +async function loadNewer() { + if (loadingNewer || newestHeight === -1 || reachedTip) return; + loadingNewer = true; + try { + const blocks = await brk.getBlocksV1FromHeight(newestHeight + LOOKAHEAD); + if (!appendNewerBlocks(blocks)) reachedTip = true; + } catch (e) { + console.error("explorer loadNewer:", e); + } + loadingNewer = false; +} + +/** @param {BlockInfoV1} block */ +function createBlockCube(block) { + const { cubeElement, leftFaceElement, rightFaceElement, topFaceElement } = + createCube(); + + cubeElement.dataset.hash = block.id; + blocksByHash.set(block.id, block); + cubeElement.addEventListener("click", () => onCubeClick(cubeElement)); + + const heightEl = document.createElement("p"); + heightEl.append(createHeightElement(block.height)); + rightFaceElement.append(heightEl); + + const feesEl = document.createElement("div"); + feesEl.classList.add("fees"); + leftFaceElement.append(feesEl); + const extras = block.extras; + const medianFee = extras ? extras.medianFee : 0; + const feeRange = extras ? extras.feeRange : [0, 0, 0, 0, 0, 0, 0]; + const avg = document.createElement("p"); + avg.innerHTML = `~${formatFeeRate(medianFee)}`; + feesEl.append(avg); + const range = document.createElement("p"); + const min = document.createElement("span"); + min.innerHTML = formatFeeRate(feeRange[0]); + const dash = document.createElement("span"); + dash.style.opacity = "0.5"; + dash.innerHTML = `-`; + const max = document.createElement("span"); + max.innerHTML = formatFeeRate(feeRange[6]); + range.append(min, dash, max); + feesEl.append(range); + const unit = document.createElement("p"); + unit.style.opacity = "0.5"; + unit.innerHTML = `sat/vB`; + feesEl.append(unit); + + const miner = document.createElement("span"); + miner.innerHTML = extras ? extras.pool.name : "Unknown"; + topFaceElement.append(miner); + + return cubeElement; +} + +function createCube() { + const cubeElement = document.createElement("div"); + cubeElement.classList.add("cube"); + const rightFaceElement = document.createElement("div"); + rightFaceElement.classList.add("face", "right"); + cubeElement.append(rightFaceElement); + const leftFaceElement = document.createElement("div"); + leftFaceElement.classList.add("face", "left"); + cubeElement.append(leftFaceElement); + const topFaceElement = document.createElement("div"); + topFaceElement.classList.add("face", "top"); + cubeElement.append(topFaceElement); + return { cubeElement, leftFaceElement, rightFaceElement, topFaceElement }; +} diff --git a/website/scripts/explorer/index.js b/website/scripts/explorer/index.js new file mode 100644 index 000000000..d7f990765 --- /dev/null +++ b/website/scripts/explorer/index.js @@ -0,0 +1,241 @@ +import { explorerElement } from "../utils/elements.js"; +import { brk } from "../utils/client.js"; +import { createMapCache } from "../utils/cache.js"; +import { + initChain, + loadInitial, + poll, + selectCube, + findCube, + lastCube, + clear as clearChain, +} from "./chain.js"; +import { + initBlockDetails, + update as updateBlock, + show as showBlock, + hide as hideBlock, +} from "./block.js"; +import { showTxFromData } from "./tx.js"; +import { showAddrDetail } from "./address.js"; + +/** @returns {string[]} */ +function pathSegments() { + return window.location.pathname.split("/").filter((v) => v); +} + +/** @type {HTMLDivElement} */ let secondaryPanel; +/** @type {number | undefined} */ let pollInterval; +/** @type {Transaction | null} */ let pendingTx = null; +let navController = new AbortController(); +const txCache = createMapCache(50); +const addrCache = createMapCache(50); + +function navigate() { + navController.abort(); + navController = new AbortController(); + return navController.signal; +} + +function showBlockPanel() { + showBlock(); + secondaryPanel.hidden = true; +} + +function showSecondaryPanel() { + hideBlock(); + secondaryPanel.hidden = false; +} + +/** @param {MouseEvent} e */ +function handleLinkClick(e) { + const a = /** @type {HTMLAnchorElement | null} */ ( + /** @type {HTMLElement} */ (e.target).closest("a[href]") + ); + if (!a) return; + const m = a.pathname.match(/^\/(block|tx|address)\/(.+)/); + if (!m) return; + e.preventDefault(); + if (m[1] === "block") { + navigateToBlock(m[2]); + } else if (m[1] === "tx") { + history.pushState(null, "", a.href); + navigateToTx(m[2]); + } else { + history.pushState(null, "", a.href); + navigateToAddr(m[2]); + } +} + +export function init() { + initChain(explorerElement, { + onSelect: (block) => { + updateBlock(block); + showBlockPanel(); + }, + onCubeClick: (cube) => { + const hash = cube.dataset.hash; + if (hash) history.pushState(null, "", `/block/${hash}`); + selectCube(cube); + }, + }); + + initBlockDetails(explorerElement, handleLinkClick); + + secondaryPanel = document.createElement("div"); + secondaryPanel.id = "tx-details"; + secondaryPanel.hidden = true; + explorerElement.append(secondaryPanel); + secondaryPanel.addEventListener("click", handleLinkClick); + + new MutationObserver(() => { + if (explorerElement.hidden) stopPolling(); + else startPolling(); + }).observe(explorerElement, { + attributes: true, + attributeFilter: ["hidden"], + }); + + document.addEventListener("visibilitychange", () => { + if (!document.hidden && !explorerElement.hidden) poll(); + }); + + window.addEventListener("popstate", () => { + const [kind, value] = pathSegments(); + if (kind === "block" && value) navigateToBlock(value, false); + else if (kind === "tx" && value) navigateToTx(value); + else if (kind === "address" && value) navigateToAddr(value); + else showBlockPanel(); + }); + + load(); +} + +function startPolling() { + stopPolling(); + poll(); + pollInterval = setInterval(poll, 15_000); +} + +function stopPolling() { + if (pollInterval !== undefined) { + clearInterval(pollInterval); + pollInterval = undefined; + } +} + +async function load() { + try { + const height = await resolveStartHeight(); + await loadInitial(height); + route(); + } catch (e) { + console.error("explorer load:", e); + } +} + +/** @param {AbortSignal} [signal] @returns {Promise} */ +async function resolveStartHeight(signal) { + const [kind, value] = pathSegments(); + if (!value) return null; + if (kind === "block") { + if (/^\d+$/.test(value)) return Number(value); + return (await brk.getBlockV1(value, { signal })).height; + } + if (kind === "tx") { + const tx = txCache.get(value) ?? (await brk.getTx(value, { signal })); + txCache.set(value, tx); + pendingTx = tx; + return tx.status?.blockHeight ?? null; + } + return null; +} + +function route() { + const [kind, value] = pathSegments(); + if (pendingTx) { + const hash = pendingTx.status?.blockHash; + const cube = hash ? findCube(hash) : null; + if (cube) selectCube(cube); + showTxFromData(pendingTx, secondaryPanel); + showSecondaryPanel(); + pendingTx = null; + } else if (kind === "address" && value) { + const cube = lastCube(); + if (cube) selectCube(cube); + navigateToAddr(value); + } else { + const cube = lastCube(); + if (cube) selectCube(cube); + } +} + +/** @param {string} hash @param {boolean} [pushUrl] */ +async function navigateToBlock(hash, pushUrl = true) { + if (pushUrl) history.pushState(null, "", `/block/${hash}`); + const cube = findCube(hash); + if (cube) { + selectCube(cube, { scroll: true }); + } else { + const signal = navigate(); + try { + clearChain(); + await loadInitial(await resolveStartHeight(signal)); + if (signal.aborted) return; + route(); + } catch (e) { + if (!signal.aborted) console.error("explorer block:", e); + } + } +} + +/** @param {string} txid */ +async function navigateToTx(txid) { + const cached = txCache.get(txid); + if (cached) { + navigate(); + showTxAndSelectBlock(cached); + return; + } + const signal = navigate(); + try { + const tx = await brk.getTx(txid, { + signal, + onUpdate: (tx) => { + txCache.set(txid, tx); + if (!signal.aborted) showTxAndSelectBlock(tx); + }, + }); + txCache.set(txid, tx); + } catch (e) { + if (!signal.aborted) console.error("explorer tx:", e); + } +} + +/** @param {Transaction} tx */ +function showTxAndSelectBlock(tx) { + if (tx.status?.blockHash) { + const cube = findCube(tx.status.blockHash); + if (cube) { + selectCube(cube, { scroll: true }); + showTxFromData(tx, secondaryPanel); + showSecondaryPanel(); + return; + } + pendingTx = tx; + clearChain(); + loadInitial(tx.status.blockHeight ?? null).then(() => { + if (!navController.signal.aborted) route(); + }); + return; + } + showTxFromData(tx, secondaryPanel); + showSecondaryPanel(); +} + +/** @param {string} address */ +function navigateToAddr(address) { + const signal = navigate(); + showAddrDetail(address, secondaryPanel, { signal, cache: addrCache }); + showSecondaryPanel(); +} diff --git a/website/scripts/explorer/render.js b/website/scripts/explorer/render.js new file mode 100644 index 000000000..79a15dc55 --- /dev/null +++ b/website/scripts/explorer/render.js @@ -0,0 +1,209 @@ +export const TX_PAGE_SIZE = 25; + +/** @param {number} sats */ +export function formatBtc(sats) { + return (sats / 1e8).toFixed(8); +} + +/** @param {number} rate */ +export function formatFeeRate(rate) { + if (rate >= 100) return Math.round(rate).toLocaleString(); + if (rate >= 10) return rate.toFixed(1); + return rate.toFixed(2); +} + +/** @param {string} text @param {HTMLElement} el */ +export function setAddrContent(text, el) { + el.textContent = ""; + if (text.length <= 6) { + el.textContent = text; + return; + } + const head = document.createElement("span"); + head.classList.add("addr-head"); + head.textContent = text.slice(0, -6); + const tail = document.createElement("span"); + tail.classList.add("addr-tail"); + tail.textContent = text.slice(-6); + el.append(head, tail); +} + +/** @param {number} height */ +export function createHeightElement(height) { + const container = document.createElement("span"); + const str = height.toString(); + const prefix = document.createElement("span"); + prefix.style.opacity = "0.5"; + prefix.style.userSelect = "none"; + prefix.textContent = "#" + "0".repeat(7 - str.length); + const num = document.createElement("span"); + num.textContent = str; + container.append(prefix, num); + return container; +} + +/** + * @param {[string, string, (string | null)?][]} rows + * @param {HTMLElement} parent + */ +export function renderRows(rows, parent) { + for (const [label, value, href] of rows) { + const row = document.createElement("div"); + row.classList.add("row"); + const labelEl = document.createElement("span"); + labelEl.classList.add("label"); + labelEl.textContent = label; + const valueEl = document.createElement(href ? "a" : "span"); + valueEl.classList.add("value"); + valueEl.textContent = value; + if (href) /** @type {HTMLAnchorElement} */ (valueEl).href = href; + row.append(labelEl, valueEl); + parent.append(row); + } +} + +/** + * @param {Transaction} tx + * @param {string} [coinbaseAscii] + */ +const IO_LIMIT = 10; + +/** + * @param {TxIn} vin + * @param {string} [coinbaseAscii] + */ +function renderInput(vin, coinbaseAscii) { + const row = document.createElement("div"); + row.classList.add("tx-io"); + const addr = document.createElement("span"); + addr.classList.add("addr"); + if (vin.isCoinbase) { + addr.textContent = "Coinbase"; + addr.classList.add("coinbase"); + if (coinbaseAscii) { + const sig = document.createElement("div"); + sig.classList.add("coinbase-sig"); + sig.textContent = coinbaseAscii; + row.append(sig); + } + } else { + const addrStr = /** @type {string | undefined} */ ( + /** @type {any} */ (vin.prevout)?.scriptpubkey_address + ); + if (addrStr) { + const link = document.createElement("a"); + link.href = `/address/${addrStr}`; + setAddrContent(addrStr, link); + addr.append(link); + } else { + addr.textContent = "Unknown"; + } + } + const amt = document.createElement("span"); + amt.classList.add("amount"); + amt.textContent = vin.prevout ? `${formatBtc(vin.prevout.value)} BTC` : ""; + row.append(addr, amt); + return row; +} + +/** @param {TxOut} vout */ +function renderOutput(vout) { + const row = document.createElement("div"); + row.classList.add("tx-io"); + const addr = document.createElement("span"); + addr.classList.add("addr"); + const type = /** @type {string | undefined} */ ( + /** @type {any} */ (vout).scriptpubkey_type + ); + const a = /** @type {string | undefined} */ ( + /** @type {any} */ (vout).scriptpubkey_address + ); + if (type === "op_return") { + addr.textContent = "OP_RETURN"; + addr.classList.add("op-return"); + } else if (a) { + const link = document.createElement("a"); + link.href = `/address/${a}`; + setAddrContent(a, link); + addr.append(link); + } else { + setAddrContent(vout.scriptpubkey, addr); + } + const amt = document.createElement("span"); + amt.classList.add("amount"); + amt.textContent = `${formatBtc(vout.value)} BTC`; + row.append(addr, amt); + return row; +} + +/** + * @template T + * @param {T[]} items + * @param {(item: T) => HTMLElement} render + * @param {HTMLElement} container + */ +function renderCapped(items, render, container) { + const limit = Math.min(items.length, IO_LIMIT); + for (let i = 0; i < limit; i++) container.append(render(items[i])); + if (items.length > IO_LIMIT) { + const btn = document.createElement("button"); + btn.classList.add("show-more"); + btn.textContent = `Show ${items.length - IO_LIMIT} more`; + btn.addEventListener("click", () => { + btn.remove(); + for (let i = IO_LIMIT; i < items.length; i++) container.append(render(items[i])); + }); + container.append(btn); + } +} + +/** @param {Transaction} tx @param {string} [coinbaseAscii] */ +export function renderTx(tx, coinbaseAscii) { + const el = document.createElement("div"); + el.classList.add("tx"); + + const head = document.createElement("div"); + head.classList.add("tx-head"); + const txidEl = document.createElement("a"); + txidEl.classList.add("txid"); + txidEl.textContent = tx.txid; + txidEl.href = `/tx/${tx.txid}`; + head.append(txidEl); + if (tx.status?.blockTime) { + const time = document.createElement("span"); + time.classList.add("tx-time"); + time.textContent = new Date(tx.status.blockTime * 1000).toLocaleString(); + head.append(time); + } + el.append(head); + + const body = document.createElement("div"); + body.classList.add("tx-body"); + + const inputs = document.createElement("div"); + inputs.classList.add("tx-inputs"); + renderCapped(tx.vin, (vin) => renderInput(vin, coinbaseAscii), inputs); + + const outputs = document.createElement("div"); + outputs.classList.add("tx-outputs"); + renderCapped(tx.vout, renderOutput, outputs); + + const totalOut = tx.vout.reduce((s, v) => s + v.value, 0); + + body.append(inputs, outputs); + el.append(body); + + const foot = document.createElement("div"); + foot.classList.add("tx-foot"); + const feeInfo = document.createElement("span"); + const vsize = Math.ceil(tx.weight / 4); + const feeRate = vsize > 0 ? tx.fee / vsize : 0; + feeInfo.textContent = `${formatFeeRate(feeRate)} sat/vB \u2013 ${tx.fee.toLocaleString()} sats`; + const total = document.createElement("span"); + total.classList.add("amount", "total"); + total.textContent = `${formatBtc(totalOut)} BTC`; + foot.append(feeInfo, total); + el.append(foot); + + return el; +} diff --git a/website/scripts/explorer/tx.js b/website/scripts/explorer/tx.js new file mode 100644 index 000000000..00c256a37 --- /dev/null +++ b/website/scripts/explorer/tx.js @@ -0,0 +1,58 @@ +import { formatBtc, formatFeeRate, renderRows, renderTx } from "./render.js"; + +/** + * @param {Transaction} tx + * @param {HTMLDivElement} el + */ +export function showTxFromData(tx, el) { + el.hidden = false; + el.scrollTop = 0; + el.innerHTML = ""; + + const title = document.createElement("h1"); + title.textContent = "Transaction"; + el.append(title); + + const vsize = Math.ceil(tx.weight / 4); + const feeRate = vsize > 0 ? tx.fee / vsize : 0; + const totalIn = tx.vin.reduce((s, v) => s + (v.prevout?.value ?? 0), 0); + const totalOut = tx.vout.reduce((s, v) => s + v.value, 0); + + renderRows( + [ + ["TXID", tx.txid], + [ + "Status", + tx.status?.confirmed + ? `Confirmed (block ${tx.status.blockHeight?.toLocaleString()})` + : "Unconfirmed", + tx.status?.blockHash ? `/block/${tx.status.blockHash}` : null, + ], + [ + "Timestamp", + tx.status?.blockTime + ? new Date(tx.status.blockTime * 1000).toUTCString() + : "Pending", + ], + ["Size", `${tx.size.toLocaleString()} B`], + ["Virtual Size", `${vsize.toLocaleString()} vB`], + ["Weight", `${tx.weight.toLocaleString()} WU`], + ["Fee", `${tx.fee.toLocaleString()} sat`], + ["Fee Rate", `${formatFeeRate(feeRate)} sat/vB`], + ["Inputs", `${tx.vin.length}`], + ["Outputs", `${tx.vout.length}`], + ["Total Input", `${formatBtc(totalIn)} BTC`], + ["Total Output", `${formatBtc(totalOut)} BTC`], + ["Version", `${tx.version}`], + ["Locktime", `${tx.locktime}`], + ], + el, + ); + + const section = document.createElement("div"); + section.classList.add("transactions"); + const heading = document.createElement("h2"); + heading.textContent = "Inputs & Outputs"; + section.append(heading, renderTx(tx)); + el.append(section); +} diff --git a/website/scripts/main.js b/website/scripts/main.js index 7581582e5..a8efa0ddf 100644 --- a/website/scripts/main.js +++ b/website/scripts/main.js @@ -1,12 +1,12 @@ import { initPrice, onPrice } from "./utils/price.js"; -import { brk } from "./client.js"; +import { brk } from "./utils/client.js"; import { onFirstIntersection, getElementById, isHidden } from "./utils/dom.js"; import { initOptions } from "./options/full.js"; import { init as initChart, setOption as setChartOption, } from "./panes/chart.js"; -import { init as initExplorer } from "./panes/explorer.js"; +import { init as initExplorer } from "./explorer/index.js"; import { init as initSearch } from "./panes/search.js"; import { idle } from "./utils/timing.js"; import { readStored, removeStored, writeToStorage } from "./utils/storage.js"; diff --git a/website/scripts/options/cointime.js b/website/scripts/options/cointime.js index 1e9844fb4..37ea565d8 100644 --- a/website/scripts/options/cointime.js +++ b/website/scripts/options/cointime.js @@ -1,5 +1,5 @@ import { colors } from "../utils/colors.js"; -import { brk } from "../client.js"; +import { brk } from "../utils/client.js"; import { Unit } from "../utils/units.js"; import { dots, diff --git a/website/scripts/options/constants.js b/website/scripts/options/constants.js index 3f01094a2..1368ed498 100644 --- a/website/scripts/options/constants.js +++ b/website/scripts/options/constants.js @@ -1,7 +1,7 @@ /** Constant helpers for creating price lines and reference lines */ import { colors } from "../utils/colors.js"; -import { brk } from "../client.js"; +import { brk } from "../utils/client.js"; import { line } from "./series.js"; /** @@ -13,9 +13,7 @@ import { line } from "./series.js"; */ export function getConstant(constants, num) { const key = - num >= 0 - ? `_${String(num).replace(".", "")}` - : `minus${Math.abs(num)}`; + num >= 0 ? `_${String(num).replace(".", "")}` : `minus${Math.abs(num)}`; const constant = /** @type {AnySeriesPattern | undefined} */ ( /** @type {Record} */ (constants)[key] ); diff --git a/website/scripts/options/distribution/data.js b/website/scripts/options/distribution/data.js index 216a72009..b85520902 100644 --- a/website/scripts/options/distribution/data.js +++ b/website/scripts/options/distribution/data.js @@ -1,6 +1,6 @@ import { colors } from "../../utils/colors.js"; import { entries } from "../../utils/array.js"; -import { brk } from "../../client.js"; +import { brk } from "../../utils/client.js"; /** @type {readonly AddressableType[]} */ const ADDRESSABLE_TYPES = [ diff --git a/website/scripts/options/full.js b/website/scripts/options/full.js index 880a10f2c..ed01af53b 100644 --- a/website/scripts/options/full.js +++ b/website/scripts/options/full.js @@ -8,7 +8,7 @@ import { setQr } from "../panes/share.js"; import { getConstant } from "./constants.js"; import { colors } from "../utils/colors.js"; import { Unit } from "../utils/units.js"; -import { brk } from "../client.js"; +import { brk } from "../utils/client.js"; export function initOptions() { const LS_SELECTED_KEY = `selected_path`; @@ -435,9 +435,11 @@ export function initOptions() { } else if (!("tree" in match)) { selected.set(match); return; + } else { + break; } } - selected.set(list[0]); + selected.set(!segments.length && savedOption ? savedOption : list[0]); } resolveUrl(); diff --git a/website/scripts/options/investing.js b/website/scripts/options/investing.js index ab9606298..2a114fad2 100644 --- a/website/scripts/options/investing.js +++ b/website/scripts/options/investing.js @@ -1,7 +1,7 @@ /** Investing section - Investment strategy tools and analysis */ import { colors } from "../utils/colors.js"; -import { brk } from "../client.js"; +import { brk } from "../utils/client.js"; import { percentRatioBaseline, price } from "./series.js"; import { satsBtcUsd } from "./shared.js"; import { periodIdToName } from "../utils/time.js"; diff --git a/website/scripts/options/market.js b/website/scripts/options/market.js index 5f70fb5ea..e263436c2 100644 --- a/website/scripts/options/market.js +++ b/website/scripts/options/market.js @@ -2,7 +2,7 @@ import { colors } from "../utils/colors.js"; import { periodIdToName } from "../utils/time.js"; -import { brk } from "../client.js"; +import { brk } from "../utils/client.js"; import { includes } from "../utils/array.js"; import { Unit } from "../utils/units.js"; import { priceLine, priceLines } from "./constants.js"; diff --git a/website/scripts/options/mining.js b/website/scripts/options/mining.js index cadc0e87e..f55a76252 100644 --- a/website/scripts/options/mining.js +++ b/website/scripts/options/mining.js @@ -21,7 +21,7 @@ import { revenueRollingBtcSatsUsd, formatCohortTitle, } from "./shared.js"; -import { brk } from "../client.js"; +import { brk } from "../utils/client.js"; /** Major pools to show in Compare section (by current hashrate dominance) */ const MAJOR_POOL_IDS = /** @type {const} */ ([ @@ -90,20 +90,37 @@ export function createMiningSection() { title: title(metric), bottom: [ ...ROLLING_WINDOWS.flatMap((w) => - percentRatio({ pattern: dominance[w.key], name: w.name, color: w.color, defaultActive: w.key !== "_24h" }), + percentRatio({ + pattern: dominance[w.key], + name: w.name, + color: w.color, + defaultActive: w.key !== "_24h", + }), ), - ...percentRatio({ pattern: dominance, name: "All Time", color: colors.time.all }), + ...percentRatio({ + pattern: dominance, + name: "All Time", + color: colors.time.all, + }), ], }, ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: title(`${w.title} ${metric}`), - bottom: percentRatio({ pattern: dominance[w.key], name: "Dominance", color: w.color }), + bottom: percentRatio({ + pattern: dominance[w.key], + name: "Dominance", + color: w.color, + }), })), { name: "All Time", title: title(`All Time ${metric}`), - bottom: percentRatio({ pattern: dominance, name: "Dominance", color: colors.time.all }), + bottom: percentRatio({ + pattern: dominance, + name: "Dominance", + color: colors.time.all, + }), }, ], }); @@ -151,7 +168,11 @@ export function createMiningSection() { { name: "Dominance", title: title("Dominance"), - bottom: percentRatio({ pattern: pool.dominance, name: "All Time", color: colors.time.all }), + bottom: percentRatio({ + pattern: pool.dominance, + name: "All Time", + color: colors.time.all, + }), }, { name: "Blocks Mined", @@ -205,7 +226,6 @@ export function createMiningSection() { ], }); - return { name: "Mining", tree: [ @@ -342,7 +362,9 @@ export function createMiningSection() { tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Fee Revenue per Block Distribution`, - bottom: distributionBtcSatsUsd(statsAtWindow(mining.rewards.fees, w.key)), + bottom: distributionBtcSatsUsd( + statsAtWindow(mining.rewards.fees, w.key), + ), })), }, ], @@ -354,16 +376,32 @@ export function createMiningSection() { name: w.name, title: `${w.title} Mining Revenue Dominance`, bottom: [ - ...percentRatio({ pattern: mining.rewards.subsidy.dominance[w.key], name: "Subsidy", color: colors.mining.subsidy }), - ...percentRatio({ pattern: mining.rewards.fees.dominance[w.key], name: "Fees", color: colors.mining.fee }), + ...percentRatio({ + pattern: mining.rewards.subsidy.dominance[w.key], + name: "Subsidy", + color: colors.mining.subsidy, + }), + ...percentRatio({ + pattern: mining.rewards.fees.dominance[w.key], + name: "Fees", + color: colors.mining.fee, + }), ], })), { name: "All Time", title: "All Time Mining Revenue Dominance", bottom: [ - ...percentRatio({ pattern: mining.rewards.subsidy.dominance, name: "Subsidy", color: colors.mining.subsidy }), - ...percentRatio({ pattern: mining.rewards.fees.dominance, name: "Fees", color: colors.mining.fee }), + ...percentRatio({ + pattern: mining.rewards.subsidy.dominance, + name: "Subsidy", + color: colors.mining.subsidy, + }), + ...percentRatio({ + pattern: mining.rewards.fees.dominance, + name: "Fees", + color: colors.mining.fee, + }), ], }, ], @@ -373,7 +411,14 @@ export function createMiningSection() { tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Fee-to-Subsidy Ratio`, - bottom: [line({ series: mining.rewards.fees.toSubsidyRatio[w.key].ratio, name: "Ratio", color: colors.mining.fee, unit: Unit.ratio })], + bottom: [ + line({ + series: mining.rewards.fees.toSubsidyRatio[w.key].ratio, + name: "Ratio", + color: colors.mining.fee, + unit: Unit.ratio, + }), + ], })), }, { @@ -395,28 +440,76 @@ export function createMiningSection() { name: "Hash Price", title: "Hash Price", bottom: [ - line({ series: mining.hashrate.price.ths, name: "per TH/s", color: colors.usd, unit: Unit.usdPerThsPerDay }), - line({ series: mining.hashrate.price.phs, name: "per PH/s", color: colors.usd, unit: Unit.usdPerPhsPerDay }), - dotted({ series: mining.hashrate.price.thsMin, name: "per TH/s ATL", color: colors.stat.min, unit: Unit.usdPerThsPerDay }), - dotted({ series: mining.hashrate.price.phsMin, name: "per PH/s ATL", color: colors.stat.min, unit: Unit.usdPerPhsPerDay }), + line({ + series: mining.hashrate.price.ths, + name: "per TH/s", + color: colors.usd, + unit: Unit.usdPerThsPerDay, + }), + line({ + series: mining.hashrate.price.phs, + name: "per PH/s", + color: colors.usd, + unit: Unit.usdPerPhsPerDay, + }), + dotted({ + series: mining.hashrate.price.thsMin, + name: "per TH/s ATL", + color: colors.stat.min, + unit: Unit.usdPerThsPerDay, + }), + dotted({ + series: mining.hashrate.price.phsMin, + name: "per PH/s ATL", + color: colors.stat.min, + unit: Unit.usdPerPhsPerDay, + }), ], }, { name: "Hash Value", title: "Hash Value", bottom: [ - line({ series: mining.hashrate.value.ths, name: "per TH/s", color: colors.bitcoin, unit: Unit.satsPerThsPerDay }), - line({ series: mining.hashrate.value.phs, name: "per PH/s", color: colors.bitcoin, unit: Unit.satsPerPhsPerDay }), - dotted({ series: mining.hashrate.value.thsMin, name: "per TH/s ATL", color: colors.stat.min, unit: Unit.satsPerThsPerDay }), - dotted({ series: mining.hashrate.value.phsMin, name: "per PH/s ATL", color: colors.stat.min, unit: Unit.satsPerPhsPerDay }), + line({ + series: mining.hashrate.value.ths, + name: "per TH/s", + color: colors.bitcoin, + unit: Unit.satsPerThsPerDay, + }), + line({ + series: mining.hashrate.value.phs, + name: "per PH/s", + color: colors.bitcoin, + unit: Unit.satsPerPhsPerDay, + }), + dotted({ + series: mining.hashrate.value.thsMin, + name: "per TH/s ATL", + color: colors.stat.min, + unit: Unit.satsPerThsPerDay, + }), + dotted({ + series: mining.hashrate.value.phsMin, + name: "per PH/s ATL", + color: colors.stat.min, + unit: Unit.satsPerPhsPerDay, + }), ], }, { name: "Recovery", title: "Hash Price & Value Recovery", bottom: [ - ...percentRatio({ pattern: mining.hashrate.price.rebound, name: "Hash Price", color: colors.usd }), - ...percentRatio({ pattern: mining.hashrate.value.rebound, name: "Hash Value", color: colors.bitcoin }), + ...percentRatio({ + pattern: mining.hashrate.price.rebound, + name: "Hash Price", + color: colors.usd, + }), + ...percentRatio({ + pattern: mining.hashrate.value.rebound, + name: "Hash Value", + color: colors.bitcoin, + }), ], }, ], @@ -429,14 +522,28 @@ export function createMiningSection() { name: "Countdown", title: "Next Halving", bottom: [ - line({ series: blocks.halving.blocksToHalving, name: "Blocks", unit: Unit.blocks }), - line({ series: blocks.halving.daysToHalving, name: "Days", unit: Unit.days }), + line({ + series: blocks.halving.blocksToHalving, + name: "Blocks", + unit: Unit.blocks, + }), + line({ + series: blocks.halving.daysToHalving, + name: "Days", + unit: Unit.days, + }), ], }, { name: "Epoch", title: "Halving Epoch", - bottom: [line({ series: blocks.halving.epoch, name: "Epoch", unit: Unit.epoch })], + bottom: [ + line({ + series: blocks.halving.epoch, + name: "Epoch", + unit: Unit.epoch, + }), + ], }, ], }, @@ -447,25 +554,48 @@ export function createMiningSection() { { name: "Current", title: "Mining Difficulty", - bottom: [line({ series: blocks.difficulty.value, name: "Difficulty", unit: Unit.difficulty })], + bottom: [ + line({ + series: blocks.difficulty.value, + name: "Difficulty", + unit: Unit.difficulty, + }), + ], }, { name: "Adjustment", title: "Difficulty Adjustment", - bottom: percentRatioBaseline({ pattern: blocks.difficulty.adjustment, name: "Change" }), + bottom: percentRatioBaseline({ + pattern: blocks.difficulty.adjustment, + name: "Change", + }), }, { name: "Countdown", title: "Next Difficulty Adjustment", bottom: [ - line({ series: blocks.difficulty.blocksToRetarget, name: "Blocks", unit: Unit.blocks }), - line({ series: blocks.difficulty.daysToRetarget, name: "Days", unit: Unit.days }), + line({ + series: blocks.difficulty.blocksToRetarget, + name: "Blocks", + unit: Unit.blocks, + }), + line({ + series: blocks.difficulty.daysToRetarget, + name: "Days", + unit: Unit.days, + }), ], }, { name: "Epoch", title: "Difficulty Epoch", - bottom: [line({ series: blocks.difficulty.epoch, name: "Epoch", unit: Unit.epoch })], + bottom: [ + line({ + series: blocks.difficulty.epoch, + name: "Epoch", + unit: Unit.epoch, + }), + ], }, ], }, diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index 7c76a80ae..275d7ba7c 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -1,7 +1,7 @@ /** Network section - On-chain activity and health */ import { colors } from "../utils/colors.js"; -import { brk } from "../client.js"; +import { brk } from "../utils/client.js"; import { Unit } from "../utils/units.js"; import { entries } from "../utils/array.js"; import { @@ -19,7 +19,12 @@ import { multiSeriesTree, percentRatioDots, } from "./series.js"; -import { satsBtcUsd, satsBtcUsdFrom, satsBtcUsdFullTree, formatCohortTitle } from "./shared.js"; +import { + satsBtcUsd, + satsBtcUsdFrom, + satsBtcUsdFullTree, + formatCohortTitle, +} from "./shared.js"; /** * Create Network section @@ -119,75 +124,79 @@ export function createNetworkSection() { const createAddressSeriesTree = (key, typeName) => { const title = formatCohortTitle(typeName); return [ - { - name: "Count", - tree: [ - { - name: "Compare", - title: title("Address Count"), - bottom: countMetrics.map((m) => - line({ - series: addrs[m.key][key], - name: m.name, - color: m.color, - unit: Unit.count, - }), - ), - }, - ...countMetrics.map((m) => ({ - name: m.name, - title: title(`${m.name} Addresses`), - bottom: [ - line({ series: addrs[m.key][key], name: m.name, unit: Unit.count }), - ], - })), - ], - }, - ...simpleDeltaTree({ - delta: addrs.delta[key], - title, - metric: "Address Count", - unit: Unit.count, - }), - { - name: "New", - tree: chartsFromCount({ - pattern: addrs.new[key], - title, - metric: "New Addresses", - unit: Unit.count, - }), - }, - { - name: "Activity", - tree: [ - { - name: "Compare", - tree: ROLLING_WINDOWS.map((w) => ({ - name: w.name, - title: title(`${w.title} Active Addresses`), - bottom: activityTypes.map((t, i) => + { + name: "Count", + tree: [ + { + name: "Compare", + title: title("Address Count"), + bottom: countMetrics.map((m) => line({ - series: addrs.activity[key][t.key][w.key], - name: t.name, - color: colors.at(i, activityTypes.length), + series: addrs[m.key][key], + name: m.name, + color: m.color, unit: Unit.count, }), ), + }, + ...countMetrics.map((m) => ({ + name: m.name, + title: title(`${m.name} Addresses`), + bottom: [ + line({ + series: addrs[m.key][key], + name: m.name, + unit: Unit.count, + }), + ], })), - }, - ...activityTypes.map((t) => ({ - name: t.name, - tree: averagesArray({ - windows: addrs.activity[key][t.key], - title, - metric: `${t.name} Addresses`, - unit: Unit.count, - }), - })), - ], - }, - ]; + ], + }, + ...simpleDeltaTree({ + delta: addrs.delta[key], + title, + metric: "Address Count", + unit: Unit.count, + }), + { + name: "New", + tree: chartsFromCount({ + pattern: addrs.new[key], + title, + metric: "New Addresses", + unit: Unit.count, + }), + }, + { + name: "Activity", + tree: [ + { + name: "Compare", + tree: ROLLING_WINDOWS.map((w) => ({ + name: w.name, + title: title(`${w.title} Active Addresses`), + bottom: activityTypes.map((t, i) => + line({ + series: addrs.activity[key][t.key][w.key], + name: t.name, + color: colors.at(i, activityTypes.length), + unit: Unit.count, + }), + ), + })), + }, + ...activityTypes.map((t) => ({ + name: t.name, + tree: averagesArray({ + windows: addrs.activity[key][t.key], + title, + metric: `${t.name} Addresses`, + unit: Unit.count, + }), + })), + ], + }, + ]; }; /** @type {Record} */ diff --git a/website/scripts/panes/chart.js b/website/scripts/panes/chart.js index 1ebcab711..27febb323 100644 --- a/website/scripts/panes/chart.js +++ b/website/scripts/panes/chart.js @@ -5,7 +5,7 @@ import { Unit } from "../utils/units.js"; import { createChart } from "../utils/chart/index.js"; import { colors } from "../utils/colors.js"; import { latestPrice, onPrice } from "../utils/price.js"; -import { brk } from "../client.js"; +import { brk } from "../utils/client.js"; const ONE_BTC_IN_SATS = 100_000_000; diff --git a/website/scripts/panes/explorer.js b/website/scripts/panes/explorer.js deleted file mode 100644 index 03c8ef3eb..000000000 --- a/website/scripts/panes/explorer.js +++ /dev/null @@ -1,959 +0,0 @@ -import { explorerElement } from "../utils/elements.js"; -import { brk } from "../client.js"; -import { createPersistedValue } from "../utils/persisted.js"; - -const LOOKAHEAD = 15; -const TX_PAGE_SIZE = 25; - -/** @type {HTMLDivElement} */ let chain; -/** @type {HTMLDivElement} */ let blocksEl; -/** @type {HTMLDivElement} */ let blockDetails; -/** @type {HTMLDivElement} */ let txDetails; -/** @type {HTMLDivElement | null} */ let selectedCube = null; -/** @type {number | undefined} */ let pollInterval; -/** @type {IntersectionObserver} */ let olderObserver; - -/** @type {Map} */ -const blocksByHash = new Map(); - -let newestHeight = -1; -let oldestHeight = Infinity; -let loadingLatest = false; -let loadingOlder = false; -let loadingNewer = false; -let reachedTip = false; - -/** @type {HTMLSpanElement} */ let heightPrefix; -/** @type {HTMLSpanElement} */ let heightNum; -/** @type {{ row: HTMLDivElement, valueEl: HTMLSpanElement }[]} */ let detailRows; -/** @type {HTMLDivElement} */ let txList; -/** @type {HTMLDivElement} */ let txSection; -/** @type {IntersectionObserver} */ let txObserver; - -/** @typedef {{ first: HTMLButtonElement, prev: HTMLButtonElement, label: HTMLSpanElement, next: HTMLButtonElement, last: HTMLButtonElement }} TxNav */ -/** @type {TxNav[]} */ let txNavs = []; -/** @type {BlockInfoV1 | null} */ let txBlock = null; -let txTotalPages = 0; -let txLoading = false; -let txLoaded = false; -const txPageParam = createPersistedValue({ - defaultValue: 0, - urlKey: "page", - serialize: (v) => String(v + 1), - deserialize: (s) => Math.max(0, Number(s) - 1), -}); - -/** @returns {string[]} */ -function pathSegments() { - return window.location.pathname.split("/").filter((v) => v); -} - -export function init() { - chain = document.createElement("div"); - chain.id = "chain"; - explorerElement.append(chain); - - blocksEl = document.createElement("div"); - blocksEl.classList.add("blocks"); - chain.append(blocksEl); - - blockDetails = document.createElement("div"); - blockDetails.id = "block-details"; - explorerElement.append(blockDetails); - - txDetails = document.createElement("div"); - txDetails.id = "tx-details"; - txDetails.hidden = true; - explorerElement.append(txDetails); - - initBlockDetails(); - initTxDetails(); - - olderObserver = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) loadOlder(); - }, - { root: chain }, - ); - - chain.addEventListener( - "scroll", - () => { - const nearStart = - (chain.scrollHeight > chain.clientHeight && chain.scrollTop <= 50) || - (chain.scrollWidth > chain.clientWidth && chain.scrollLeft <= 50); - if (nearStart && !reachedTip && !loadingNewer) loadNewer(); - }, - { passive: true }, - ); - - new MutationObserver(() => { - if (explorerElement.hidden) stopPolling(); - else startPolling(); - }).observe(explorerElement, { - attributes: true, - attributeFilter: ["hidden"], - }); - - document.addEventListener("visibilitychange", () => { - if (!document.hidden && !explorerElement.hidden) loadLatest(); - }); - - window.addEventListener("popstate", () => { - const [kind, value] = pathSegments(); - if (kind === "block" && value) navigateToBlock(value, false); - else if (kind === "tx" && value) showTxDetail(value); - else if (kind === "address" && value) showAddrDetail(value); - else { - blockDetails.hidden = false; - txDetails.hidden = true; - } - }); - - loadLatest(); -} - -function startPolling() { - stopPolling(); - loadLatest(); - pollInterval = setInterval(loadLatest, 15_000); -} - -function stopPolling() { - if (pollInterval !== undefined) { - clearInterval(pollInterval); - pollInterval = undefined; - } -} - -function observeOldestEdge() { - olderObserver.disconnect(); - const oldest = blocksEl.firstElementChild; - if (oldest) olderObserver.observe(oldest); -} - -/** @param {BlockInfoV1[]} blocks */ -function appendNewerBlocks(blocks) { - if (!blocks.length) return false; - const anchor = blocksEl.lastElementChild; - const anchorRect = anchor?.getBoundingClientRect(); - for (const b of [...blocks].reverse()) { - if (b.height > newestHeight) { - blocksEl.append(createBlockCube(b)); - } else { - blocksByHash.set(b.id, b); - } - } - newestHeight = Math.max(newestHeight, blocks[0].height); - if (anchor && anchorRect) { - const r = anchor.getBoundingClientRect(); - chain.scrollTop += r.top - anchorRect.top; - chain.scrollLeft += r.left - anchorRect.left; - } - return true; -} - -/** @param {string} hash @param {boolean} [pushUrl] */ -function navigateToBlock(hash, pushUrl = true) { - if (pushUrl) history.pushState(null, "", `/block/${hash}`); - const cube = /** @type {HTMLDivElement | null} */ ( - blocksEl.querySelector(`[data-hash="${hash}"]`) - ); - if (cube) { - selectCube(cube, { scroll: true }); - } else { - resetExplorer(); - } -} - -function resetExplorer() { - newestHeight = -1; - oldestHeight = Infinity; - loadingLatest = false; - loadingOlder = false; - loadingNewer = false; - reachedTip = false; - selectedCube = null; - blocksEl.innerHTML = ""; - olderObserver.disconnect(); - loadLatest(); -} - -/** @returns {Promise} */ -/** @type {Transaction | null} */ -let pendingTx = null; - -async function getStartHeight() { - if (pendingTx) return pendingTx.status?.blockHeight ?? null; - const [kind, value] = pathSegments(); - if (!value) return null; - if (kind === "block") { - if (/^\d+$/.test(value)) return Number(value); - return (await brk.getBlockV1(value)).height; - } - if (kind === "tx") { - pendingTx = await brk.getTx(value); - return pendingTx.status?.blockHeight ?? null; - } - return null; -} - -async function loadLatest() { - if (loadingLatest) return; - if (newestHeight !== -1 && !reachedTip) return; - loadingLatest = true; - try { - const startHeight = newestHeight === -1 ? await getStartHeight() : null; - const blocks = - startHeight !== null - ? await brk.getBlocksV1FromHeight(startHeight) - : await brk.getBlocksV1(); - - if (newestHeight === -1) { - for (const b of blocks) blocksEl.prepend(createBlockCube(b)); - newestHeight = blocks[0].height; - oldestHeight = blocks[blocks.length - 1].height; - if (startHeight === null) reachedTip = true; - const [kind, value] = pathSegments(); - if (pendingTx) { - const hash = pendingTx.status?.blockHash; - const cube = /** @type {HTMLDivElement | null} */ ( - hash ? blocksEl.querySelector(`[data-hash="${hash}"]`) : null - ); - if (cube) selectCube(cube); - showTxFromData(pendingTx); - pendingTx = null; - } else if (kind === "address" && value) { - selectCube(/** @type {HTMLDivElement} */ (blocksEl.lastElementChild)); - showAddrDetail(value); - } else { - selectCube(/** @type {HTMLDivElement} */ (blocksEl.lastElementChild)); - } - loadingLatest = false; - observeOldestEdge(); - if (!reachedTip) await loadNewer(); - return; - } - - appendNewerBlocks(blocks); - reachedTip = true; - } catch (e) { - console.error("explorer poll:", e); - } - loadingLatest = false; -} - -async function loadOlder() { - if (loadingOlder || oldestHeight <= 0) return; - loadingOlder = true; - try { - const blocks = await brk.getBlocksV1FromHeight(oldestHeight - 1); - for (const block of blocks) blocksEl.prepend(createBlockCube(block)); - if (blocks.length) { - oldestHeight = blocks[blocks.length - 1].height; - observeOldestEdge(); - } - } catch (e) { - console.error("explorer loadOlder:", e); - } - loadingOlder = false; -} - -async function loadNewer() { - if (loadingNewer || newestHeight === -1 || reachedTip) return; - loadingNewer = true; - try { - const blocks = await brk.getBlocksV1FromHeight(newestHeight + LOOKAHEAD); - if (!appendNewerBlocks(blocks)) { - reachedTip = true; - } - } catch (e) { - console.error("explorer loadNewer:", e); - } - loadingNewer = false; -} - -/** @param {HTMLDivElement} cube @param {{ pushUrl?: boolean, scroll?: boolean }} [opts] */ -function selectCube(cube, { pushUrl = false, scroll = false } = {}) { - if (cube === selectedCube) return; - if (selectedCube) selectedCube.classList.remove("selected"); - selectedCube = cube; - if (cube) { - cube.classList.add("selected"); - if (scroll) cube.scrollIntoView({ behavior: "smooth" }); - const hash = cube.dataset.hash; - if (hash) { - updateDetails(blocksByHash.get(hash)); - if (pushUrl) history.pushState(null, "", `/block/${hash}`); - } - } -} - -/** @typedef {[string, (b: BlockInfoV1) => string | null, ((b: BlockInfoV1) => string | null)?]} RowDef */ - -/** @type {RowDef[]} */ -const ROW_DEFS = [ - ["Hash", (b) => b.id, (b) => `/block/${b.id}`], - [ - "Previous Hash", - (b) => b.previousblockhash, - (b) => `/block/${b.previousblockhash}`, - ], - ["Merkle Root", (b) => b.merkleRoot], - ["Timestamp", (b) => new Date(b.timestamp * 1000).toUTCString()], - ["Median Time", (b) => new Date(b.mediantime * 1000).toUTCString()], - ["Version", (b) => `0x${b.version.toString(16)}`], - ["Bits", (b) => b.bits.toString(16)], - ["Nonce", (b) => b.nonce.toLocaleString()], - ["Difficulty", (b) => Number(b.difficulty).toLocaleString()], - ["Size", (b) => `${(b.size / 1_000_000).toFixed(2)} MB`], - ["Weight", (b) => `${(b.weight / 1_000_000).toFixed(2)} MWU`], - ["Transactions", (b) => b.txCount.toLocaleString()], - ["Price", (b) => (b.extras ? `$${b.extras.price.toLocaleString()}` : null)], - ["Pool", (b) => b.extras?.pool.name ?? null], - ["Pool ID", (b) => b.extras?.pool.id.toString() ?? null], - ["Pool Slug", (b) => b.extras?.pool.slug ?? null], - ["Miner Names", (b) => b.extras?.pool.minerNames?.join(", ") || null], - [ - "Reward", - (b) => (b.extras ? `${(b.extras.reward / 1e8).toFixed(8)} BTC` : null), - ], - [ - "Total Fees", - (b) => (b.extras ? `${(b.extras.totalFees / 1e8).toFixed(8)} BTC` : null), - ], - [ - "Median Fee Rate", - (b) => (b.extras ? `${formatFeeRate(b.extras.medianFee)} sat/vB` : null), - ], - [ - "Avg Fee Rate", - (b) => (b.extras ? `${formatFeeRate(b.extras.avgFeeRate)} sat/vB` : null), - ], - [ - "Avg Fee", - (b) => (b.extras ? `${b.extras.avgFee.toLocaleString()} sat` : null), - ], - [ - "Median Fee", - (b) => (b.extras ? `${b.extras.medianFeeAmt.toLocaleString()} sat` : null), - ], - [ - "Fee Range", - (b) => - b.extras - ? b.extras.feeRange.map((f) => formatFeeRate(f)).join(", ") + " sat/vB" - : null, - ], - [ - "Fee Percentiles", - (b) => - b.extras - ? b.extras.feePercentiles.map((f) => f.toLocaleString()).join(", ") + - " sat" - : null, - ], - [ - "Avg Tx Size", - (b) => (b.extras ? `${b.extras.avgTxSize.toLocaleString()} B` : null), - ], - [ - "Virtual Size", - (b) => (b.extras ? `${b.extras.virtualSize.toLocaleString()} vB` : null), - ], - ["Inputs", (b) => b.extras?.totalInputs.toLocaleString() ?? null], - ["Outputs", (b) => b.extras?.totalOutputs.toLocaleString() ?? null], - [ - "Total Input Amount", - (b) => - b.extras ? `${(b.extras.totalInputAmt / 1e8).toFixed(8)} BTC` : null, - ], - [ - "Total Output Amount", - (b) => - b.extras ? `${(b.extras.totalOutputAmt / 1e8).toFixed(8)} BTC` : null, - ], - ["UTXO Set Change", (b) => b.extras?.utxoSetChange.toLocaleString() ?? null], - ["UTXO Set Size", (b) => b.extras?.utxoSetSize.toLocaleString() ?? null], - ["SegWit Txs", (b) => b.extras?.segwitTotalTxs.toLocaleString() ?? null], - [ - "SegWit Size", - (b) => (b.extras ? `${b.extras.segwitTotalSize.toLocaleString()} B` : null), - ], - [ - "SegWit Weight", - (b) => - b.extras ? `${b.extras.segwitTotalWeight.toLocaleString()} WU` : null, - ], - ["Coinbase Address", (b) => b.extras?.coinbaseAddress || null], - ["Coinbase Addresses", (b) => b.extras?.coinbaseAddresses.join(", ") || null], - ["Coinbase Raw", (b) => b.extras?.coinbaseRaw ?? null], - ["Coinbase Signature", (b) => b.extras?.coinbaseSignature ?? null], - ["Coinbase Signature ASCII", (b) => b.extras?.coinbaseSignatureAscii ?? null], - ["Header", (b) => b.extras?.header ?? null], -]; - -/** @param {MouseEvent} e */ -function handleLinkClick(e) { - const a = /** @type {HTMLAnchorElement | null} */ ( - /** @type {HTMLElement} */ (e.target).closest("a[href]") - ); - if (!a) return; - const m = a.pathname.match(/^\/(block|tx|address)\/(.+)/); - if (!m) return; - e.preventDefault(); - if (m[1] === "block") { - navigateToBlock(m[2]); - } else if (m[1] === "tx") { - history.pushState(null, "", a.href); - showTxDetail(m[2]); - } else { - history.pushState(null, "", a.href); - showAddrDetail(m[2]); - } -} - -function initBlockDetails() { - const title = document.createElement("h1"); - title.textContent = "Block "; - const code = document.createElement("code"); - const container = document.createElement("span"); - heightPrefix = document.createElement("span"); - heightPrefix.style.opacity = "0.5"; - heightPrefix.style.userSelect = "none"; - heightNum = document.createElement("span"); - container.append(heightPrefix, heightNum); - code.append(container); - title.append(code); - blockDetails.append(title); - - blockDetails.addEventListener("click", handleLinkClick); - - detailRows = ROW_DEFS.map(([label, , linkFn]) => { - const row = document.createElement("div"); - row.classList.add("row"); - const labelEl = document.createElement("span"); - labelEl.classList.add("label"); - labelEl.textContent = label; - const valueEl = document.createElement(linkFn ? "a" : "span"); - valueEl.classList.add("value"); - row.append(labelEl, valueEl); - blockDetails.append(row); - return { row, valueEl }; - }); - - txSection = document.createElement("div"); - txSection.classList.add("transactions"); - blockDetails.append(txSection); - - const txHeader = document.createElement("div"); - txHeader.classList.add("tx-header"); - const heading = document.createElement("h2"); - heading.textContent = "Transactions"; - txHeader.append(heading, createTxNav()); - txSection.append(txHeader); - - txList = document.createElement("div"); - txList.classList.add("tx-list"); - txSection.append(txList, createTxNav()); - - txObserver = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !txLoaded) { - loadTxPage(txPageParam.value, false); - } - }); - txObserver.observe(txSection); -} - -/** @returns {HTMLDivElement} */ -function createTxNav() { - const nav = document.createElement("div"); - nav.classList.add("pagination"); - const first = document.createElement("button"); - first.textContent = "\u00AB"; - const prev = document.createElement("button"); - prev.textContent = "\u2190"; - const label = document.createElement("span"); - const next = document.createElement("button"); - next.textContent = "\u2192"; - const last = document.createElement("button"); - last.textContent = "\u00BB"; - nav.append(first, prev, label, next, last); - first.addEventListener("click", () => loadTxPage(0)); - prev.addEventListener("click", () => loadTxPage(txPageParam.value - 1)); - next.addEventListener("click", () => loadTxPage(txPageParam.value + 1)); - last.addEventListener("click", () => loadTxPage(txTotalPages - 1)); - txNavs.push({ first, prev, label, next, last }); - return nav; -} - -/** @param {number} page */ -function updateTxNavs(page) { - const atFirst = page <= 0; - const atLast = page >= txTotalPages - 1; - for (const n of txNavs) { - n.label.textContent = `${page + 1} / ${txTotalPages}`; - n.first.disabled = atFirst; - n.prev.disabled = atFirst; - n.next.disabled = atLast; - n.last.disabled = atLast; - } -} - -/** @param {BlockInfoV1 | undefined} block */ -function updateDetails(block) { - if (!block) return; - blockDetails.hidden = false; - txDetails.hidden = true; - blockDetails.scrollTop = 0; - - const str = block.height.toString(); - heightPrefix.textContent = "#" + "0".repeat(7 - str.length); - heightNum.textContent = str; - - ROW_DEFS.forEach(([, getter, linkFn], i) => { - const value = getter(block); - const { row, valueEl } = detailRows[i]; - if (value !== null) { - valueEl.textContent = value; - if (linkFn) - /** @type {HTMLAnchorElement} */ (valueEl).href = linkFn(block) ?? ""; - row.hidden = false; - } else { - row.hidden = true; - } - }); - - txBlock = block; - txTotalPages = Math.ceil(block.txCount / TX_PAGE_SIZE); - if (txLoaded) txPageParam.setImmediate(0); - txLoaded = false; - updateTxNavs(txPageParam.value); - txList.innerHTML = ""; - txObserver.disconnect(); - txObserver.observe(txSection); -} - -function initTxDetails() { - txDetails.addEventListener("click", handleLinkClick); -} - -/** @param {string} txid */ -async function showTxDetail(txid) { - try { - const tx = await brk.getTx(txid); - if (tx.status?.blockHash) { - const cube = /** @type {HTMLDivElement | null} */ ( - blocksEl.querySelector(`[data-hash="${tx.status.blockHash}"]`) - ); - if (cube) { - selectCube(cube, { scroll: true }); - showTxFromData(tx); - return; - } - pendingTx = tx; - resetExplorer(); - return; - } - showTxFromData(tx); - } catch (e) { - console.error("explorer tx:", e); - } -} - -/** @param {Transaction} tx */ -function showTxFromData(tx) { - blockDetails.hidden = true; - txDetails.hidden = false; - txDetails.scrollTop = 0; - txDetails.innerHTML = ""; - - const title = document.createElement("h1"); - title.textContent = "Transaction"; - txDetails.append(title); - - const vsize = Math.ceil(tx.weight / 4); - const feeRate = vsize > 0 ? tx.fee / vsize : 0; - const totalIn = tx.vin.reduce((s, v) => s + (v.prevout?.value ?? 0), 0); - const totalOut = tx.vout.reduce((s, v) => s + v.value, 0); - - /** @type {[string, string, (string | null)?][]} */ - const rows = [ - ["TXID", tx.txid], - [ - "Status", - tx.status?.confirmed - ? `Confirmed (block ${tx.status.blockHeight?.toLocaleString()})` - : "Unconfirmed", - tx.status?.blockHash ? `/block/${tx.status.blockHash}` : null, - ], - [ - "Timestamp", - tx.status?.blockTime - ? new Date(tx.status.blockTime * 1000).toUTCString() - : "Pending", - ], - ["Size", `${tx.size.toLocaleString()} B`], - ["Virtual Size", `${vsize.toLocaleString()} vB`], - ["Weight", `${tx.weight.toLocaleString()} WU`], - ["Fee", `${tx.fee.toLocaleString()} sat`], - ["Fee Rate", `${formatFeeRate(feeRate)} sat/vB`], - ["Inputs", `${tx.vin.length}`], - ["Outputs", `${tx.vout.length}`], - ["Total Input", `${formatBtc(totalIn)} BTC`], - ["Total Output", `${formatBtc(totalOut)} BTC`], - ["Version", `${tx.version}`], - ["Locktime", `${tx.locktime}`], - ]; - - for (const [label, value, href] of rows) { - const row = document.createElement("div"); - row.classList.add("row"); - const labelEl = document.createElement("span"); - labelEl.classList.add("label"); - labelEl.textContent = label; - const valueEl = document.createElement(href ? "a" : "span"); - valueEl.classList.add("value"); - valueEl.textContent = value; - if (href) /** @type {HTMLAnchorElement} */ (valueEl).href = href; - row.append(labelEl, valueEl); - txDetails.append(row); - } - - const section = document.createElement("div"); - section.classList.add("transactions"); - const heading = document.createElement("h2"); - heading.textContent = "Inputs & Outputs"; - section.append(heading); - section.append(renderTx(tx)); - txDetails.append(section); -} - -/** @param {string} address */ -async function showAddrDetail(address) { - blockDetails.hidden = true; - txDetails.hidden = false; - txDetails.scrollTop = 0; - txDetails.innerHTML = ""; - - try { - const stats = await brk.getAddress(address); - const chain = stats.chainStats; - - const title = document.createElement("h1"); - title.textContent = "Address"; - txDetails.append(title); - - const addrEl = document.createElement("div"); - addrEl.classList.add("row"); - const addrLabel = document.createElement("span"); - addrLabel.classList.add("label"); - addrLabel.textContent = "Address"; - const addrValue = document.createElement("span"); - addrValue.classList.add("value"); - addrValue.textContent = address; - addrEl.append(addrLabel, addrValue); - txDetails.append(addrEl); - - const balance = chain.fundedTxoSum - chain.spentTxoSum; - - /** @type {[string, string][]} */ - const rows = [ - ["Balance", `${formatBtc(balance)} BTC`], - ["Total Received", `${formatBtc(chain.fundedTxoSum)} BTC`], - ["Total Sent", `${formatBtc(chain.spentTxoSum)} BTC`], - ["Tx Count", chain.txCount.toLocaleString()], - ["Funded Outputs", chain.fundedTxoCount.toLocaleString()], - ["Spent Outputs", chain.spentTxoCount.toLocaleString()], - ]; - - for (const [label, value] of rows) { - const row = document.createElement("div"); - row.classList.add("row"); - const labelEl = document.createElement("span"); - labelEl.classList.add("label"); - labelEl.textContent = label; - const valueEl = document.createElement("span"); - valueEl.classList.add("value"); - valueEl.textContent = value; - row.append(labelEl, valueEl); - txDetails.append(row); - } - - const section = document.createElement("div"); - section.classList.add("transactions"); - const heading = document.createElement("h2"); - heading.textContent = "Transactions"; - section.append(heading); - txDetails.append(section); - - let loadingAddr = false; - let addrTxCount = 0; - /** @type {string | undefined} */ - let afterTxid; - - const addrTxObserver = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting && !loadingAddr && addrTxCount < chain.txCount) - loadMore(); - }); - - async function loadMore() { - loadingAddr = true; - try { - const txs = await brk.getAddressTxs(address, afterTxid); - for (const tx of txs) section.append(renderTx(tx)); - addrTxCount += txs.length; - if (txs.length) { - afterTxid = txs[txs.length - 1].txid; - addrTxObserver.disconnect(); - const last = section.lastElementChild; - if (last) addrTxObserver.observe(last); - } - } catch (e) { - console.error("explorer addr txs:", e); - addrTxCount = chain.txCount; - } - loadingAddr = false; - } - - await loadMore(); - } catch (e) { - console.error("explorer addr:", e); - txDetails.textContent = "Address not found"; - } -} - -/** @param {number} page @param {boolean} [pushUrl] */ -async function loadTxPage(page, pushUrl = true) { - if (txLoading || !txBlock || page < 0 || page >= txTotalPages) return; - txLoading = true; - txLoaded = true; - if (pushUrl) txPageParam.setImmediate(page); - updateTxNavs(page); - try { - const txs = await brk.getBlockTxsFromIndex(txBlock.id, page * TX_PAGE_SIZE); - txList.innerHTML = ""; - for (const tx of txs) txList.append(renderTx(tx)); - } catch (e) { - console.error("explorer txs:", e); - } - txLoading = false; -} - -/** @param {Transaction} tx */ -function renderTx(tx) { - const el = document.createElement("div"); - el.classList.add("tx"); - - const head = document.createElement("div"); - head.classList.add("tx-head"); - const txidEl = document.createElement("a"); - txidEl.classList.add("txid"); - txidEl.textContent = tx.txid; - txidEl.href = `/tx/${tx.txid}`; - head.append(txidEl); - if (tx.status?.blockTime) { - const time = document.createElement("span"); - time.classList.add("tx-time"); - time.textContent = new Date(tx.status.blockTime * 1000).toLocaleString(); - head.append(time); - } - el.append(head); - - const body = document.createElement("div"); - body.classList.add("tx-body"); - - const inputs = document.createElement("div"); - inputs.classList.add("tx-inputs"); - for (const vin of tx.vin) { - const row = document.createElement("div"); - row.classList.add("tx-io"); - const addr = document.createElement("span"); - addr.classList.add("addr"); - if (vin.isCoinbase) { - addr.textContent = "Coinbase"; - addr.classList.add("coinbase"); - const ascii = txBlock?.extras?.coinbaseSignatureAscii; - if (ascii) { - const sig = document.createElement("span"); - sig.classList.add("coinbase-sig"); - sig.textContent = ascii; - row.append(sig); - } - } else { - const addrStr = /** @type {string | undefined} */ ( - /** @type {any} */ (vin.prevout)?.scriptpubkey_address - ); - if (addrStr) { - const link = document.createElement("a"); - link.href = `/address/${addrStr}`; - setAddrContent(addrStr, link); - addr.append(link); - } else { - addr.textContent = "Unknown"; - } - } - const amt = document.createElement("span"); - amt.classList.add("amount"); - amt.textContent = vin.prevout ? `${formatBtc(vin.prevout.value)} BTC` : ""; - row.append(addr, amt); - inputs.append(row); - } - - const outputs = document.createElement("div"); - outputs.classList.add("tx-outputs"); - let totalOut = 0; - for (const vout of tx.vout) { - totalOut += vout.value; - const row = document.createElement("div"); - row.classList.add("tx-io"); - const addr = document.createElement("span"); - addr.classList.add("addr"); - const type = /** @type {string | undefined} */ ( - /** @type {any} */ (vout).scriptpubkey_type - ); - const a = /** @type {string | undefined} */ ( - /** @type {any} */ (vout).scriptpubkey_address - ); - if (type === "op_return") { - addr.textContent = "OP_RETURN"; - addr.classList.add("op-return"); - } else if (a) { - const link = document.createElement("a"); - link.href = `/address/${a}`; - setAddrContent(a, link); - addr.append(link); - } else { - setAddrContent(vout.scriptpubkey, addr); - } - const amt = document.createElement("span"); - amt.classList.add("amount"); - amt.textContent = `${formatBtc(vout.value)} BTC`; - row.append(addr, amt); - outputs.append(row); - } - - body.append(inputs, outputs); - el.append(body); - - const foot = document.createElement("div"); - foot.classList.add("tx-foot"); - const feeInfo = document.createElement("span"); - const vsize = Math.ceil(tx.weight / 4); - const feeRate = vsize > 0 ? tx.fee / vsize : 0; - feeInfo.textContent = `${formatFeeRate(feeRate)} sat/vB \u2013 ${tx.fee.toLocaleString()} sats`; - const total = document.createElement("span"); - total.classList.add("amount", "total"); - total.textContent = `${formatBtc(totalOut)} BTC`; - foot.append(feeInfo, total); - el.append(foot); - - return el; -} - -/** @param {number} sats */ -function formatBtc(sats) { - return (sats / 1e8).toFixed(8); -} - -/** @param {number} rate */ -function formatFeeRate(rate) { - if (rate >= 100) return Math.round(rate).toLocaleString(); - if (rate >= 10) return rate.toFixed(1); - return rate.toFixed(2); -} - -/** @param {string} text @param {HTMLElement} el */ -function setAddrContent(text, el) { - el.textContent = ""; - if (text.length <= 6) { - el.textContent = text; - return; - } - const head = document.createElement("span"); - head.classList.add("addr-head"); - head.textContent = text.slice(0, -6); - const tail = document.createElement("span"); - tail.classList.add("addr-tail"); - tail.textContent = text.slice(-6); - el.append(head, tail); -} - -/** @param {number} height */ -function createHeightElement(height) { - const container = document.createElement("span"); - const str = height.toString(); - const prefix = document.createElement("span"); - prefix.style.opacity = "0.5"; - prefix.style.userSelect = "none"; - prefix.textContent = "#" + "0".repeat(7 - str.length); - const num = document.createElement("span"); - num.textContent = str; - container.append(prefix, num); - return container; -} - -/** @param {BlockInfoV1} block */ -function createBlockCube(block) { - const { cubeElement, leftFaceElement, rightFaceElement, topFaceElement } = - createCube(); - - cubeElement.dataset.hash = block.id; - blocksByHash.set(block.id, block); - cubeElement.addEventListener("click", () => - selectCube(cubeElement, { pushUrl: true }), - ); - - const heightEl = document.createElement("p"); - heightEl.append(createHeightElement(block.height)); - rightFaceElement.append(heightEl); - - const feesEl = document.createElement("div"); - feesEl.classList.add("fees"); - leftFaceElement.append(feesEl); - const extras = block.extras; - const medianFee = extras ? extras.medianFee : 0; - const feeRange = extras ? extras.feeRange : [0, 0, 0, 0, 0, 0, 0]; - const avg = document.createElement("p"); - avg.innerHTML = `~${formatFeeRate(medianFee)}`; - feesEl.append(avg); - const range = document.createElement("p"); - const min = document.createElement("span"); - min.innerHTML = formatFeeRate(feeRange[0]); - const dash = document.createElement("span"); - dash.style.opacity = "0.5"; - dash.innerHTML = `-`; - const max = document.createElement("span"); - max.innerHTML = formatFeeRate(feeRange[6]); - range.append(min, dash, max); - feesEl.append(range); - const unit = document.createElement("p"); - unit.style.opacity = "0.5"; - unit.innerHTML = `sat/vB`; - feesEl.append(unit); - - const miner = document.createElement("span"); - miner.innerHTML = extras ? extras.pool.name : "Unknown"; - topFaceElement.append(miner); - - return cubeElement; -} - -function createCube() { - const cubeElement = document.createElement("div"); - cubeElement.classList.add("cube"); - - const rightFaceElement = document.createElement("div"); - rightFaceElement.classList.add("face", "right"); - cubeElement.append(rightFaceElement); - - const leftFaceElement = document.createElement("div"); - leftFaceElement.classList.add("face", "left"); - cubeElement.append(leftFaceElement); - - const topFaceElement = document.createElement("div"); - topFaceElement.classList.add("face", "top"); - cubeElement.append(topFaceElement); - - return { cubeElement, leftFaceElement, rightFaceElement, topFaceElement }; -} diff --git a/website/scripts/utils/cache.js b/website/scripts/utils/cache.js new file mode 100644 index 000000000..19be7ae94 --- /dev/null +++ b/website/scripts/utils/cache.js @@ -0,0 +1,32 @@ +/** + * @template V + * @param {number} [maxSize] + */ +export function createMapCache(maxSize = 100) { + /** @type {Map} */ + const map = new Map(); + + return { + /** @param {string} key @returns {V | undefined} */ + get(key) { + return map.get(key); + }, + /** @param {string} key @returns {boolean} */ + has(key) { + return map.has(key); + }, + /** @param {string} key @param {V} value */ + set(key, value) { + if (map.size >= maxSize && !map.has(key)) { + const first = map.keys().next().value; + if (first !== undefined) map.delete(first); + } + map.set(key, value); + }, + }; +} + +/** + * @template V + * @typedef {{ get: (key: string) => V | undefined, has: (key: string) => boolean, set: (key: string, value: V) => void }} MapCache + */ diff --git a/website/scripts/utils/chart/index.js b/website/scripts/utils/chart/index.js index e19033458..5906061f4 100644 --- a/website/scripts/utils/chart/index.js +++ b/website/scripts/utils/chart/index.js @@ -10,6 +10,7 @@ import { capture } from "./capture.js"; import { colors } from "../colors.js"; import { createRadios, createSelect, getElementById } from "../dom.js"; import { createPersistedValue } from "../persisted.js"; +import { createMapCache } from "../cache.js"; import { onChange as onThemeChange } from "../theme.js"; import { throttle, debounce } from "../timing.js"; import { serdeBool, INDEX_FROM_LABEL } from "../serde.js"; @@ -190,9 +191,7 @@ export function createChart({ parent, brk, fitContent }) { }, }; - // Memory cache for instant index switching - /** @type {Map} */ - const cache = new Map(); + const cache = createMapCache(Infinity); // Range state: localStorage stores all ranges per-index, URL stores current range only /** @typedef {{ from: number, to: number }} Range */ diff --git a/website/scripts/client.js b/website/scripts/utils/client.js similarity index 71% rename from website/scripts/client.js rename to website/scripts/utils/client.js index d0c957495..f20a8bdff 100644 --- a/website/scripts/client.js +++ b/website/scripts/utils/client.js @@ -1,4 +1,4 @@ -import { BrkClient } from "./modules/brk-client/index.js"; +import { BrkClient } from "../modules/brk-client/index.js"; // const brk = new BrkClient("https://bitview.space"); const brk = new BrkClient("/"); diff --git a/website/scripts/utils/units.js b/website/scripts/utils/units.js index 0fe3deb54..bd20716c2 100644 --- a/website/scripts/utils/units.js +++ b/website/scripts/utils/units.js @@ -40,7 +40,7 @@ export const Unit = /** @type {const} */ ({ epoch: { id: "epoch", name: "Epoch" }, // Fees - feeRate: { id: "feerate", name: "Sats/vByte" }, + feeRate: { id: "feerate", name: "Sat/vByte" }, // Rates perSec: { id: "per-sec", name: "Per Second" }, diff --git a/website/styles/panes/explorer.css b/website/styles/panes/explorer.css index da7b93c93..47f64a07e 100644 --- a/website/styles/panes/explorer.css +++ b/website/styles/panes/explorer.css @@ -210,7 +210,7 @@ } .label { - opacity: 0.5; + color: var(--off-color); white-space: nowrap; } @@ -253,6 +253,8 @@ border: 1px solid var(--border-color); padding: 0.5rem; margin-bottom: 0.5rem; + content-visibility: auto; + contain-intrinsic-block-size: auto 8rem; .tx-head { display: flex; @@ -273,7 +275,7 @@ .tx-time { flex-shrink: 0; - opacity: 0.5; + color: var(--off-color); } } @@ -309,6 +311,11 @@ white-space: nowrap; color: var(--off-color); + a { + display: flex; + min-width: 0; + } + .addr-head { overflow: hidden; text-overflow: ellipsis; @@ -325,14 +332,14 @@ .coinbase-sig { font-family: Lilex; font-size: var(--font-size-xs); - opacity: 0.5; + color: var(--off-color); display: block; overflow: hidden; text-overflow: ellipsis; } &.op-return { - opacity: 0.5; + color: var(--off-color); } } @@ -342,6 +349,12 @@ } } + .show-more { + color: var(--off-color); + font-size: var(--font-size-xs); + padding: 0.25rem 0; + } + .tx-foot { display: flex; justify-content: space-between; @@ -349,10 +362,9 @@ margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--border-color); - opacity: 0.5; + color: var(--off-color); .total { - opacity: 1; color: var(--orange); } }