mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 06:01:57 -07:00
global: snapshot
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -404,11 +404,14 @@ class BrkClientBase {{
|
||||
|
||||
/**
|
||||
* @param {{string}} path
|
||||
* @param {{{{ signal?: AbortSignal }}}} [options]
|
||||
* @returns {{Promise<Response>}}
|
||||
*/
|
||||
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<T>}}
|
||||
*/
|
||||
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<string>}}
|
||||
*/
|
||||
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<T>}} */ raw) => onUpdate(_wrapSeriesData(raw)) : undefined;
|
||||
const raw = await this.getJson(path, wrappedOnUpdate);
|
||||
const raw = await this.getJson(path, {{ onUpdate: wrappedOnUpdate }});
|
||||
return _wrapSeriesData(raw);
|
||||
}}
|
||||
}}
|
||||
|
||||
@@ -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)*
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -147,9 +147,10 @@ impl AddrRoutes for ApiRouter<AppState> {
|
||||
.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::<AddrValidation>()
|
||||
.not_modified()
|
||||
.server_error()
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
.description("Returns block details with extras by hash.\n\n*[Mempool.space docs](https://mempool.space/docs/api/rest#get-block-v1)*")
|
||||
.json_response::<BlockInfoV1>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error()
|
||||
},
|
||||
@@ -78,9 +79,10 @@ impl BlockRoutes for ApiRouter<AppState> {
|
||||
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()
|
||||
},
|
||||
|
||||
@@ -55,9 +55,10 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
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::<PoolsSummary>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -107,9 +108,10 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
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::<Vec<PoolHashrateEntry>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -195,9 +197,10 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
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::<HashrateSummary>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -229,9 +232,10 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
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::<Vec<DifficultyAdjustmentEntry>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -249,6 +253,7 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
.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::<RewardStats>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -263,9 +268,10 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
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::<Vec<BlockFeesEntry>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -280,9 +286,10 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
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::<Vec<BlockRewardsEntry>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -297,9 +304,10 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
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::<Vec<BlockFeeRatesEntry>>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
@@ -314,9 +322,10 @@ impl MiningRoutes for ApiRouter<AppState> {
|
||||
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::<BlockSizesWeights>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.server_error()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -32,9 +32,10 @@ impl TxRoutes for ApiRouter<AppState> {
|
||||
.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::<CpfpInfo>()
|
||||
.not_modified()
|
||||
.bad_request()
|
||||
.not_found()
|
||||
.server_error(),
|
||||
),
|
||||
|
||||
@@ -3,6 +3,7 @@ use serde::Deserialize;
|
||||
|
||||
use brk_types::Addr;
|
||||
|
||||
/// Bitcoin address path parameter
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct AddrParam {
|
||||
#[serde(rename = "address")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@ use serde::Deserialize;
|
||||
|
||||
use brk_types::BlockHash;
|
||||
|
||||
/// Block hash path parameter
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct BlockHashParam {
|
||||
pub hash: BlockHash,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,6 +3,7 @@ use serde::Deserialize;
|
||||
|
||||
use brk_types::Height;
|
||||
|
||||
/// Block height path parameter
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct HeightParam {
|
||||
pub height: Height,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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<Timestamp>,
|
||||
|
||||
@@ -3,6 +3,7 @@ use serde::Deserialize;
|
||||
|
||||
use brk_types::Txid;
|
||||
|
||||
/// Transaction ID path parameter
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct TxidParam {
|
||||
pub txid: Txid,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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<HashrateEntry>,
|
||||
/// Historical difficulty adjustments.
|
||||
/// Historical difficulty adjustments
|
||||
pub difficulty: Vec<DifficultyEntry>,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct MerkleProof {
|
||||
pub block_height: Height,
|
||||
/// Merkle proof path (hex-encoded hashes)
|
||||
pub merkle: Vec<String>,
|
||||
/// Transaction position in the block
|
||||
/// Transaction position in the block (0-indexed)
|
||||
#[schemars(example = 42)]
|
||||
pub pos: usize,
|
||||
}
|
||||
|
||||
@@ -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<u128>,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,15 @@ pub struct PoolsSummary {
|
||||
/// List of pools sorted by block count descending
|
||||
pub pools: Vec<PoolStats>,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@@ -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<LockTime> for RawLockTime {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<bitcoin::transaction::Version> for TxVersionRaw {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+337
-218
File diff suppressed because it is too large
Load Diff
@@ -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)*
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
*_old.js
|
||||
*dump*
|
||||
TODO.md
|
||||
_explorer.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)
|
||||
|
||||
@@ -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<Transaction[]>} */
|
||||
const addrTxCache = createMapCache(200);
|
||||
|
||||
/**
|
||||
* @param {string} address
|
||||
* @param {HTMLDivElement} el
|
||||
* @param {{ signal: AbortSignal, cache: MapCache<AddrStats> }} 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";
|
||||
}
|
||||
}
|
||||
@@ -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<BlockInfoV1["extras"]>) => 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;
|
||||
}
|
||||
@@ -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<BlockHash, BlockInfoV1>} */
|
||||
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 };
|
||||
}
|
||||
@@ -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<number | null>} */
|
||||
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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string, AnySeriesPattern>} */ (constants)[key]
|
||||
);
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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<string, typeof scriptTypes[number]>} */
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<BlockHash, BlockInfoV1>} */
|
||||
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<number | null>} */
|
||||
/** @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 };
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/**
|
||||
* @template V
|
||||
* @param {number} [maxSize]
|
||||
*/
|
||||
export function createMapCache(maxSize = 100) {
|
||||
/** @type {Map<string, V>} */
|
||||
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
|
||||
*/
|
||||
@@ -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<string, AnySeriesData>} */
|
||||
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 */
|
||||
|
||||
@@ -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("/");
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user