mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 06:39:58 -07:00
1238 lines
39 KiB
JavaScript
1238 lines
39 KiB
JavaScript
/** Chain section builder - typed tree-based patterns */
|
|
|
|
import { Unit } from "../utils/units.js";
|
|
import { priceLine } from "./constants.js";
|
|
import { line, baseline, dots } from "./series.js";
|
|
import { satsBtcUsd } from "./shared.js";
|
|
import { spendableTypeColors } from "./colors/index.js";
|
|
|
|
/** Major pools to show in Compare section (by current hashrate dominance) */
|
|
const MAJOR_POOL_IDS = [
|
|
"foundryusa", // ~32% - largest pool
|
|
"antpool", // ~18% - Bitmain-owned
|
|
"viabtc", // ~14% - independent
|
|
"f2pool", // ~10% - one of the oldest pools
|
|
"marapool", // MARA Holdings
|
|
"braiinspool", // formerly Slush Pool
|
|
"spiderpool", // growing Asian pool
|
|
"ocean", // decentralization-focused
|
|
];
|
|
|
|
/**
|
|
* AntPool & friends - pools sharing AntPool's block templates
|
|
* Based on b10c's research: https://b10c.me/blog/015-bitcoin-mining-centralization/
|
|
* Collectively ~35-40% of network hashrate
|
|
*/
|
|
const ANTPOOL_AND_FRIENDS_IDS = [
|
|
"antpool", // Bitmain-owned, template source
|
|
"poolin", // shares AntPool templates
|
|
"btccom", // CloverPool (formerly BTC.com)
|
|
"braiinspool", // shares AntPool templates
|
|
"ultimuspool", // shares AntPool templates
|
|
"binancepool", // shares AntPool templates
|
|
"secpool", // shares AntPool templates
|
|
"sigmapoolcom", // SigmaPool
|
|
"rawpool", // shares AntPool templates
|
|
"luxor", // shares AntPool templates
|
|
];
|
|
|
|
/**
|
|
* Create Chain section
|
|
* @param {PartialContext} ctx
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createChainSection(ctx) {
|
|
const {
|
|
colors,
|
|
brk,
|
|
fromSumStatsPattern,
|
|
fromBaseStatsPattern,
|
|
fromFullStatsPattern,
|
|
fromStatsPattern,
|
|
fromCoinbasePattern,
|
|
fromValuePattern,
|
|
fromCountPattern,
|
|
fromSupplyPattern,
|
|
} = ctx;
|
|
const {
|
|
blocks,
|
|
transactions,
|
|
pools,
|
|
inputs,
|
|
outputs,
|
|
scripts,
|
|
supply,
|
|
distribution,
|
|
} = brk.metrics;
|
|
|
|
// Address types for mapping (using spendableTypeColors for consistency)
|
|
/** @type {ReadonlyArray<{key: AddressableType, name: string, color: Color, defaultActive?: boolean}>} */
|
|
const addressTypes = [
|
|
{ key: "p2pkh", name: "P2PKH", color: colors[spendableTypeColors.p2pkh] },
|
|
{ key: "p2sh", name: "P2SH", color: colors[spendableTypeColors.p2sh] },
|
|
{ key: "p2wpkh", name: "P2WPKH", color: colors[spendableTypeColors.p2wpkh] },
|
|
{ key: "p2wsh", name: "P2WSH", color: colors[spendableTypeColors.p2wsh] },
|
|
{ key: "p2tr", name: "P2TR", color: colors[spendableTypeColors.p2tr] },
|
|
{ key: "p2pk65", name: "P2PK65", color: colors[spendableTypeColors.p2pk65], defaultActive: false },
|
|
{ key: "p2pk33", name: "P2PK33", color: colors[spendableTypeColors.p2pk33], defaultActive: false },
|
|
{ key: "p2a", name: "P2A", color: colors[spendableTypeColors.p2a], defaultActive: false },
|
|
];
|
|
|
|
// Activity types for mapping
|
|
/** @type {ReadonlyArray<{key: "sending" | "receiving" | "both" | "reactivated" | "balanceIncreased" | "balanceDecreased", name: string, title: string, compareTitle: string}>} */
|
|
const activityTypes = [
|
|
{ key: "sending", name: "Sending", title: "Sending Address Count", compareTitle: "Sending Address Count by Type" },
|
|
{ key: "receiving", name: "Receiving", title: "Receiving Address Count", compareTitle: "Receiving Address Count by Type" },
|
|
{ key: "both", name: "Both", title: "Addresses Sending & Receiving (Same Block)", compareTitle: "Addresses Sending & Receiving by Type" },
|
|
{ key: "reactivated", name: "Reactivated", title: "Reactivated Address Count (Was Empty)", compareTitle: "Reactivated Address Count by Type" },
|
|
{ key: "balanceIncreased", name: "Balance Increased", title: "Addresses with Increased Balance", compareTitle: "Addresses with Increased Balance by Type" },
|
|
{ key: "balanceDecreased", name: "Balance Decreased", title: "Addresses with Decreased Balance", compareTitle: "Addresses with Decreased Balance by Type" },
|
|
];
|
|
|
|
// Count types for comparison charts
|
|
/** @type {ReadonlyArray<{key: "addrCount" | "emptyAddrCount" | "totalAddrCount", name: string, title: string}>} */
|
|
const countTypes = [
|
|
{ key: "addrCount", name: "Loaded", title: "Address Count by Type" },
|
|
{ key: "emptyAddrCount", name: "Empty", title: "Empty Address Count by Type" },
|
|
{ key: "totalAddrCount", name: "Total", title: "Total Address Count by Type" },
|
|
];
|
|
|
|
/**
|
|
* Create address metrics tree for a given type key
|
|
* @param {AddressableType | "all"} key
|
|
* @param {string} titlePrefix
|
|
*/
|
|
const createAddressMetricsTree = (key, titlePrefix) => [
|
|
{
|
|
name: "Count",
|
|
title: `${titlePrefix}Address Count`,
|
|
bottom: [
|
|
line({
|
|
metric: distribution.addrCount[key],
|
|
name: "Loaded",
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
metric: distribution.totalAddrCount[key],
|
|
name: "Total",
|
|
color: colors.default,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: distribution.emptyAddrCount[key],
|
|
name: "Empty",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "New",
|
|
title: `${titlePrefix}New Address Count`,
|
|
bottom: fromFullStatsPattern(distribution.newAddrCount[key], Unit.count),
|
|
},
|
|
{
|
|
name: "Growth Rate",
|
|
title: `${titlePrefix}Address Growth Rate`,
|
|
bottom: fromBaseStatsPattern(distribution.growthRate[key], Unit.ratio),
|
|
},
|
|
{
|
|
name: "Activity",
|
|
tree: activityTypes.map((a) => ({
|
|
name: a.name,
|
|
title: `${titlePrefix}${a.name} Address Count`,
|
|
bottom: fromBaseStatsPattern(
|
|
distribution.addressActivity[key][a.key],
|
|
Unit.count,
|
|
),
|
|
})),
|
|
},
|
|
];
|
|
|
|
// Build pools tree dynamically
|
|
const poolEntries = Object.entries(pools.vecs);
|
|
const poolsTree = poolEntries.map(([key, pool]) => {
|
|
const poolName =
|
|
brk.POOL_ID_TO_POOL_NAME[
|
|
/** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (key.toLowerCase())
|
|
] || key;
|
|
return {
|
|
name: poolName,
|
|
tree: [
|
|
{
|
|
name: "Dominance",
|
|
title: `${poolName} Dominance`,
|
|
bottom: [
|
|
dots({
|
|
metric: pool._24hDominance,
|
|
name: "24h",
|
|
color: colors.pink,
|
|
unit: Unit.percentage,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: pool._1wDominance,
|
|
name: "1w",
|
|
color: colors.red,
|
|
unit: Unit.percentage,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: pool._1mDominance,
|
|
name: "1m",
|
|
unit: Unit.percentage,
|
|
}),
|
|
line({
|
|
metric: pool._1yDominance,
|
|
name: "1y",
|
|
color: colors.lime,
|
|
unit: Unit.percentage,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: pool.dominance,
|
|
name: "All Time",
|
|
color: colors.teal,
|
|
unit: Unit.percentage,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Blocks mined",
|
|
title: `${poolName} Blocks`,
|
|
bottom: [
|
|
dots({
|
|
metric: pool.blocksMined.sum,
|
|
name: "Sum",
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
metric: pool.blocksMined.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.blue,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: pool._24hBlocksMined,
|
|
name: "24h sum",
|
|
color: colors.pink,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: pool._1wBlocksMined,
|
|
name: "1w sum",
|
|
color: colors.red,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: pool._1mBlocksMined,
|
|
name: "1m sum",
|
|
color: colors.pink,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: pool._1yBlocksMined,
|
|
name: "1y sum",
|
|
color: colors.purple,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Rewards",
|
|
title: `${poolName} Rewards`,
|
|
bottom: [
|
|
...fromValuePattern(
|
|
pool.coinbase,
|
|
"coinbase",
|
|
colors.orange,
|
|
colors.red,
|
|
),
|
|
...fromValuePattern(
|
|
pool.subsidy,
|
|
"subsidy",
|
|
colors.lime,
|
|
colors.emerald,
|
|
),
|
|
...fromValuePattern(pool.fee, "fee", colors.cyan, colors.indigo),
|
|
],
|
|
},
|
|
{
|
|
name: "Since last block",
|
|
title: `${poolName} Since Last Block`,
|
|
bottom: [
|
|
line({
|
|
metric: pool.blocksSinceBlock,
|
|
name: "Blocks",
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
metric: pool.daysSinceBlock,
|
|
name: "Days",
|
|
unit: Unit.days,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
};
|
|
});
|
|
|
|
return {
|
|
name: "Chain",
|
|
tree: [
|
|
// Block
|
|
{
|
|
name: "Block",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
title: "Block Count",
|
|
bottom: [
|
|
...fromCountPattern(blocks.count.blockCount, Unit.count),
|
|
line({
|
|
metric: blocks.count.blockCountTarget,
|
|
name: "Target",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
options: { lineStyle: 4 },
|
|
}),
|
|
line({
|
|
metric: blocks.count._24hBlockCount,
|
|
name: "24h sum",
|
|
color: colors.pink,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.count._1wBlockCount,
|
|
name: "1w sum",
|
|
color: colors.red,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.count._1mBlockCount,
|
|
name: "1m sum",
|
|
color: colors.orange,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.count._1yBlockCount,
|
|
name: "1y sum",
|
|
color: colors.purple,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Interval",
|
|
title: "Block Interval",
|
|
bottom: [
|
|
...fromBaseStatsPattern(blocks.interval, Unit.secs, "", { avgActive: false }),
|
|
priceLine({ ctx, unit: Unit.secs, name: "Target", number: 600 }),
|
|
],
|
|
},
|
|
{
|
|
name: "Size",
|
|
title: "Block Size",
|
|
bottom: [
|
|
...fromSumStatsPattern(blocks.size, Unit.bytes),
|
|
line({
|
|
metric: blocks.totalSize,
|
|
name: "Total",
|
|
color: colors.purple,
|
|
unit: Unit.bytes,
|
|
defaultActive: false,
|
|
}),
|
|
...fromBaseStatsPattern(blocks.vbytes, Unit.vb),
|
|
...fromBaseStatsPattern(blocks.weight, Unit.wu),
|
|
line({
|
|
metric: blocks.weight.sum,
|
|
name: "Sum",
|
|
color: colors.stat.sum,
|
|
unit: Unit.wu,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.weight.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.stat.cumulative,
|
|
unit: Unit.wu,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Fullness",
|
|
title: "Block Fullness",
|
|
bottom: fromBaseStatsPattern(blocks.fullness, Unit.percentage),
|
|
},
|
|
],
|
|
},
|
|
|
|
// Transaction
|
|
{
|
|
name: "Transaction",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
title: "Transaction Count",
|
|
bottom: fromFullStatsPattern(transactions.count.txCount, Unit.count),
|
|
},
|
|
{
|
|
name: "Speed",
|
|
title: "Transactions Per Second",
|
|
bottom: [
|
|
dots({
|
|
metric: transactions.volume.txPerSec,
|
|
name: "Transactions",
|
|
unit: Unit.perSec,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Volume",
|
|
title: "Transaction Volume",
|
|
bottom: [
|
|
...satsBtcUsd(transactions.volume.sentSum, "Sent"),
|
|
...satsBtcUsd(
|
|
transactions.volume.receivedSum,
|
|
"Received",
|
|
colors.cyan,
|
|
{
|
|
defaultActive: false,
|
|
},
|
|
),
|
|
...satsBtcUsd(
|
|
transactions.volume.annualizedVolume,
|
|
"Annualized",
|
|
colors.red,
|
|
{ defaultActive: false },
|
|
),
|
|
],
|
|
},
|
|
{
|
|
name: "Size",
|
|
title: "Transaction Size",
|
|
bottom: [
|
|
...fromStatsPattern(transactions.size.weight, Unit.wu),
|
|
...fromStatsPattern(transactions.size.vsize, Unit.vb),
|
|
],
|
|
},
|
|
{
|
|
name: "Fee Rate",
|
|
title: "Fee Rate",
|
|
bottom: fromStatsPattern(transactions.fees.feeRate, Unit.feeRate),
|
|
},
|
|
{
|
|
name: "Versions",
|
|
title: "Transaction Versions",
|
|
bottom: [
|
|
...fromCountPattern(
|
|
transactions.versions.v1,
|
|
Unit.count,
|
|
"v1",
|
|
colors.orange,
|
|
colors.red,
|
|
),
|
|
...fromCountPattern(
|
|
transactions.versions.v2,
|
|
Unit.count,
|
|
"v2",
|
|
colors.cyan,
|
|
colors.blue,
|
|
),
|
|
...fromCountPattern(
|
|
transactions.versions.v3,
|
|
Unit.count,
|
|
"v3",
|
|
colors.lime,
|
|
colors.green,
|
|
),
|
|
],
|
|
},
|
|
{
|
|
name: "Velocity",
|
|
title: "Transactions Velocity",
|
|
bottom: [
|
|
line({
|
|
metric: supply.velocity.btc,
|
|
name: "Bitcoin",
|
|
unit: Unit.ratio,
|
|
}),
|
|
line({
|
|
metric: supply.velocity.usd,
|
|
name: "Dollars",
|
|
color: colors.emerald,
|
|
unit: Unit.ratio,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// UTXO Set (merged Input, Output, UTXO)
|
|
{
|
|
name: "UTXO Set",
|
|
tree: [
|
|
{
|
|
name: "Input Count",
|
|
title: "Input Count",
|
|
bottom: [...fromSumStatsPattern(inputs.count, Unit.count)],
|
|
},
|
|
{
|
|
name: "Output Count",
|
|
title: "Output Count",
|
|
bottom: [...fromSumStatsPattern(outputs.count.totalCount, Unit.count)],
|
|
},
|
|
{
|
|
name: "Inputs/sec",
|
|
title: "Inputs Per Second",
|
|
bottom: [
|
|
dots({
|
|
metric: transactions.volume.inputsPerSec,
|
|
name: "Inputs",
|
|
unit: Unit.perSec,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Outputs/sec",
|
|
title: "Outputs Per Second",
|
|
bottom: [
|
|
dots({
|
|
metric: transactions.volume.outputsPerSec,
|
|
name: "Outputs",
|
|
unit: Unit.perSec,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "UTXO Count",
|
|
title: "UTXO Count",
|
|
bottom: [
|
|
line({
|
|
metric: outputs.count.utxoCount,
|
|
name: "Count",
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// Scripts
|
|
{
|
|
name: "Scripts",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: [
|
|
// Legacy scripts
|
|
{
|
|
name: "Legacy",
|
|
tree: [
|
|
{
|
|
name: "P2PKH",
|
|
title: "P2PKH Output Count",
|
|
bottom: fromFullStatsPattern(scripts.count.p2pkh, Unit.count),
|
|
},
|
|
{
|
|
name: "P2PK33",
|
|
title: "P2PK33 Output Count",
|
|
bottom: fromFullStatsPattern(
|
|
scripts.count.p2pk33,
|
|
Unit.count,
|
|
),
|
|
},
|
|
{
|
|
name: "P2PK65",
|
|
title: "P2PK65 Output Count",
|
|
bottom: fromFullStatsPattern(
|
|
scripts.count.p2pk65,
|
|
Unit.count,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
// Script Hash
|
|
{
|
|
name: "Script Hash",
|
|
tree: [
|
|
{
|
|
name: "P2SH",
|
|
title: "P2SH Output Count",
|
|
bottom: fromFullStatsPattern(scripts.count.p2sh, Unit.count),
|
|
},
|
|
{
|
|
name: "P2MS",
|
|
title: "P2MS Output Count",
|
|
bottom: fromFullStatsPattern(scripts.count.p2ms, Unit.count),
|
|
},
|
|
],
|
|
},
|
|
// SegWit scripts
|
|
{
|
|
name: "SegWit",
|
|
tree: [
|
|
{
|
|
name: "All SegWit",
|
|
title: "SegWit Output Count",
|
|
bottom: fromFullStatsPattern(
|
|
scripts.count.segwit,
|
|
Unit.count,
|
|
),
|
|
},
|
|
{
|
|
name: "P2WPKH",
|
|
title: "P2WPKH Output Count",
|
|
bottom: fromFullStatsPattern(
|
|
scripts.count.p2wpkh,
|
|
Unit.count,
|
|
),
|
|
},
|
|
{
|
|
name: "P2WSH",
|
|
title: "P2WSH Output Count",
|
|
bottom: fromFullStatsPattern(scripts.count.p2wsh, Unit.count),
|
|
},
|
|
],
|
|
},
|
|
// Taproot scripts
|
|
{
|
|
name: "Taproot",
|
|
tree: [
|
|
{
|
|
name: "P2TR",
|
|
title: "P2TR Output Count",
|
|
bottom: fromFullStatsPattern(scripts.count.p2tr, Unit.count),
|
|
},
|
|
{
|
|
name: "P2A",
|
|
title: "P2A Output Count",
|
|
bottom: fromFullStatsPattern(scripts.count.p2a, Unit.count),
|
|
},
|
|
],
|
|
},
|
|
// Other scripts
|
|
{
|
|
name: "Other",
|
|
tree: [
|
|
{
|
|
name: "OP_RETURN",
|
|
title: "OP_RETURN Output Count",
|
|
bottom: fromFullStatsPattern(
|
|
scripts.count.opreturn,
|
|
Unit.count,
|
|
),
|
|
},
|
|
{
|
|
name: "Empty",
|
|
title: "Empty Output Count",
|
|
bottom: fromFullStatsPattern(
|
|
scripts.count.emptyoutput,
|
|
Unit.count,
|
|
),
|
|
},
|
|
{
|
|
name: "Unknown",
|
|
title: "Unknown Output Count",
|
|
bottom: fromFullStatsPattern(
|
|
scripts.count.unknownoutput,
|
|
Unit.count,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Adoption",
|
|
tree: [
|
|
{
|
|
name: "SegWit",
|
|
title: "SegWit Adoption",
|
|
bottom: [
|
|
line({
|
|
metric: scripts.count.segwitAdoption.base,
|
|
name: "Base",
|
|
unit: Unit.percentage,
|
|
}),
|
|
line({
|
|
metric: scripts.count.segwitAdoption.sum,
|
|
name: "Sum",
|
|
color: colors.stat.sum,
|
|
unit: Unit.percentage,
|
|
}),
|
|
line({
|
|
metric: scripts.count.segwitAdoption.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.stat.cumulative,
|
|
unit: Unit.percentage,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Taproot",
|
|
title: "Taproot Adoption",
|
|
bottom: [
|
|
line({
|
|
metric: scripts.count.taprootAdoption.base,
|
|
name: "Base",
|
|
unit: Unit.percentage,
|
|
}),
|
|
line({
|
|
metric: scripts.count.taprootAdoption.sum,
|
|
name: "Sum",
|
|
color: colors.stat.sum,
|
|
unit: Unit.percentage,
|
|
}),
|
|
line({
|
|
metric: scripts.count.taprootAdoption.cumulative,
|
|
name: "Cumulative",
|
|
color: colors.stat.cumulative,
|
|
unit: Unit.percentage,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "OP_RETURN Value",
|
|
title: "OP_RETURN Value",
|
|
bottom: fromCoinbasePattern(scripts.value.opreturn),
|
|
},
|
|
],
|
|
},
|
|
|
|
// Supply
|
|
{
|
|
name: "Supply",
|
|
tree: [
|
|
{
|
|
name: "Circulating",
|
|
title: "Circulating Supply",
|
|
bottom: fromSupplyPattern(supply.circulating, "Supply"),
|
|
},
|
|
{
|
|
name: "Inflation",
|
|
title: "Inflation Rate",
|
|
bottom: [
|
|
dots({
|
|
metric: supply.inflation,
|
|
name: "Rate",
|
|
unit: Unit.percentage,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Unspendable",
|
|
title: "Unspendable Supply",
|
|
bottom: fromValuePattern(supply.burned.unspendable),
|
|
},
|
|
{
|
|
name: "OP_RETURN",
|
|
title: "OP_RETURN Supply",
|
|
bottom: fromValuePattern(supply.burned.opreturn),
|
|
},
|
|
],
|
|
},
|
|
|
|
// Rewards
|
|
{
|
|
name: "Rewards",
|
|
tree: [
|
|
{
|
|
name: "Coinbase",
|
|
title: "Coinbase Rewards",
|
|
bottom: [
|
|
...fromCoinbasePattern(blocks.rewards.coinbase),
|
|
...satsBtcUsd(
|
|
blocks.rewards._24hCoinbaseSum,
|
|
"24h sum",
|
|
colors.pink,
|
|
{ defaultActive: false },
|
|
),
|
|
],
|
|
},
|
|
{
|
|
name: "Subsidy",
|
|
title: "Block Subsidy",
|
|
bottom: [
|
|
...fromCoinbasePattern(blocks.rewards.subsidy),
|
|
line({
|
|
metric: blocks.rewards.subsidyDominance,
|
|
name: "Dominance",
|
|
color: colors.purple,
|
|
unit: Unit.percentage,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.rewards.subsidyUsd1ySma,
|
|
name: "1y SMA",
|
|
color: colors.lime,
|
|
unit: Unit.usd,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Fee",
|
|
title: "Transaction Fees",
|
|
bottom: [
|
|
...fromSumStatsPattern(transactions.fees.fee.bitcoin, Unit.btc),
|
|
...fromSumStatsPattern(transactions.fees.fee.sats, Unit.sats),
|
|
...fromSumStatsPattern(transactions.fees.fee.dollars, Unit.usd),
|
|
line({
|
|
metric: blocks.rewards.feeDominance,
|
|
name: "Dominance",
|
|
color: colors.purple,
|
|
unit: Unit.percentage,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Unclaimed",
|
|
title: "Unclaimed Rewards",
|
|
bottom: fromValuePattern(
|
|
blocks.rewards.unclaimedRewards,
|
|
"Unclaimed",
|
|
),
|
|
},
|
|
],
|
|
},
|
|
|
|
// Addresses
|
|
{
|
|
name: "Addresses",
|
|
tree: [
|
|
// Overview - global metrics for all addresses
|
|
{ name: "Overview", tree: createAddressMetricsTree("all", "") },
|
|
|
|
// Compare - cross-type comparisons (base + average, system selects appropriate one)
|
|
{
|
|
name: "Compare",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: countTypes.map((c) => ({
|
|
name: c.name,
|
|
title: c.title,
|
|
bottom: addressTypes.map((t) =>
|
|
line({
|
|
metric: distribution[c.key][t.key],
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
),
|
|
})),
|
|
},
|
|
{
|
|
name: "New",
|
|
title: "New Address Count by Type",
|
|
bottom: addressTypes.flatMap((t) => [
|
|
line({
|
|
metric: distribution.newAddrCount[t.key].base,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
line({
|
|
metric: distribution.newAddrCount[t.key].average,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Growth Rate",
|
|
title: "Address Growth Rate by Type",
|
|
bottom: addressTypes.flatMap((t) => [
|
|
line({
|
|
metric: distribution.growthRate[t.key].base,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.ratio,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
line({
|
|
metric: distribution.growthRate[t.key].average,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.ratio,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
]),
|
|
},
|
|
{
|
|
name: "Activity",
|
|
tree: activityTypes.map((a) => ({
|
|
name: a.name,
|
|
title: a.compareTitle,
|
|
bottom: addressTypes.flatMap((t) => [
|
|
line({
|
|
metric: distribution.addressActivity[t.key][a.key].base,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
line({
|
|
metric: distribution.addressActivity[t.key][a.key].average,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
]),
|
|
})),
|
|
},
|
|
],
|
|
},
|
|
|
|
// Individual address types - each with same structure as Overview
|
|
...addressTypes.map((t) => ({
|
|
name: t.name,
|
|
tree: createAddressMetricsTree(t.key, `${t.name} `),
|
|
})),
|
|
],
|
|
},
|
|
|
|
// Mining
|
|
{
|
|
name: "Mining",
|
|
tree: [
|
|
// Hashrate
|
|
{
|
|
name: "Hashrate",
|
|
title: "Network Hashrate",
|
|
bottom: [
|
|
dots({
|
|
metric: blocks.mining.hashRate,
|
|
name: "Hashrate",
|
|
unit: Unit.hashRate,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashRate1wSma,
|
|
name: "1w SMA",
|
|
color: colors.red,
|
|
unit: Unit.hashRate,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashRate1mSma,
|
|
name: "1m SMA",
|
|
color: colors.orange,
|
|
unit: Unit.hashRate,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashRate2mSma,
|
|
name: "2m SMA",
|
|
color: colors.yellow,
|
|
unit: Unit.hashRate,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashRate1ySma,
|
|
name: "1y SMA",
|
|
color: colors.lime,
|
|
unit: Unit.hashRate,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
metric: blocks.difficulty.asHash,
|
|
name: "Difficulty",
|
|
color: colors.default,
|
|
unit: Unit.hashRate,
|
|
options: { lineStyle: 1 },
|
|
}),
|
|
],
|
|
},
|
|
|
|
// Difficulty group
|
|
{
|
|
name: "Difficulty",
|
|
tree: [
|
|
{
|
|
name: "Level",
|
|
title: "Network Difficulty",
|
|
bottom: [
|
|
line({
|
|
metric: blocks.difficulty.raw,
|
|
name: "Difficulty",
|
|
unit: Unit.difficulty,
|
|
}),
|
|
line({
|
|
metric: blocks.difficulty.epoch,
|
|
name: "Epoch",
|
|
color: colors.teal,
|
|
unit: Unit.epoch,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Adjustment",
|
|
title: "Difficulty Adjustment",
|
|
bottom: [
|
|
baseline({
|
|
metric: blocks.difficulty.adjustment,
|
|
name: "Difficulty Change",
|
|
unit: Unit.percentage,
|
|
}),
|
|
priceLine({ ctx, number: 0, unit: Unit.percentage }),
|
|
],
|
|
},
|
|
{
|
|
name: "Countdown",
|
|
title: "Next Adjustment",
|
|
bottom: [
|
|
line({
|
|
metric: blocks.difficulty.blocksBeforeNextAdjustment,
|
|
name: "Before Next",
|
|
color: colors.indigo,
|
|
unit: Unit.blocks,
|
|
}),
|
|
line({
|
|
metric: blocks.difficulty.daysBeforeNextAdjustment,
|
|
name: "Before Next",
|
|
color: colors.purple,
|
|
unit: Unit.days,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// Economics group
|
|
{
|
|
name: "Economics",
|
|
tree: [
|
|
{
|
|
name: "Hash Price",
|
|
title: "Hash Price",
|
|
bottom: [
|
|
line({
|
|
metric: blocks.mining.hashPriceThs,
|
|
name: "TH/s",
|
|
color: colors.emerald,
|
|
unit: Unit.usdPerThsPerDay,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashPricePhs,
|
|
name: "PH/s",
|
|
color: colors.emerald,
|
|
unit: Unit.usdPerPhsPerDay,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashPriceRebound,
|
|
name: "Rebound",
|
|
color: colors.yellow,
|
|
unit: Unit.percentage,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashPriceThsMin,
|
|
name: "TH/s Min",
|
|
color: colors.red,
|
|
unit: Unit.usdPerThsPerDay,
|
|
options: { lineStyle: 1 },
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashPricePhsMin,
|
|
name: "PH/s Min",
|
|
color: colors.red,
|
|
unit: Unit.usdPerPhsPerDay,
|
|
options: { lineStyle: 1 },
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Hash Value",
|
|
title: "Hash Value",
|
|
bottom: [
|
|
line({
|
|
metric: blocks.mining.hashValueThs,
|
|
name: "TH/s",
|
|
color: colors.orange,
|
|
unit: Unit.satsPerThsPerDay,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashValuePhs,
|
|
name: "PH/s",
|
|
color: colors.orange,
|
|
unit: Unit.satsPerPhsPerDay,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashValueRebound,
|
|
name: "Rebound",
|
|
color: colors.yellow,
|
|
unit: Unit.percentage,
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashValueThsMin,
|
|
name: "TH/s Min",
|
|
color: colors.red,
|
|
unit: Unit.satsPerThsPerDay,
|
|
options: { lineStyle: 1 },
|
|
}),
|
|
line({
|
|
metric: blocks.mining.hashValuePhsMin,
|
|
name: "PH/s Min",
|
|
color: colors.red,
|
|
unit: Unit.satsPerPhsPerDay,
|
|
options: { lineStyle: 1 },
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// Halving (at top level for quick access)
|
|
{
|
|
name: "Halving",
|
|
title: "Halving",
|
|
bottom: [
|
|
line({
|
|
metric: blocks.halving.epoch,
|
|
name: "Epoch",
|
|
color: colors.purple,
|
|
unit: Unit.epoch,
|
|
}),
|
|
line({
|
|
metric: blocks.halving.blocksBeforeNextHalving,
|
|
name: "Before Next",
|
|
unit: Unit.blocks,
|
|
}),
|
|
line({
|
|
metric: blocks.halving.daysBeforeNextHalving,
|
|
name: "Before Next",
|
|
color: colors.blue,
|
|
unit: Unit.days,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// Pools
|
|
{
|
|
name: "Pools",
|
|
tree: [
|
|
// Compare section (major pools only)
|
|
{
|
|
name: "Compare",
|
|
tree: [
|
|
{
|
|
name: "Dominance",
|
|
title: "Pool Dominance (Major Pools)",
|
|
bottom: poolEntries
|
|
.filter(([key]) => MAJOR_POOL_IDS.includes(key.toLowerCase()))
|
|
.map(([key, pool]) => {
|
|
const poolName =
|
|
brk.POOL_ID_TO_POOL_NAME[
|
|
/** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (
|
|
key.toLowerCase()
|
|
)
|
|
] || key;
|
|
return line({
|
|
metric: pool._1mDominance,
|
|
name: poolName,
|
|
unit: Unit.percentage,
|
|
});
|
|
}),
|
|
},
|
|
{
|
|
name: "Blocks Mined",
|
|
title: "Blocks Mined - 1m (Major Pools)",
|
|
bottom: poolEntries
|
|
.filter(([key]) => MAJOR_POOL_IDS.includes(key.toLowerCase()))
|
|
.map(([key, pool]) => {
|
|
const poolName =
|
|
brk.POOL_ID_TO_POOL_NAME[
|
|
/** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (
|
|
key.toLowerCase()
|
|
)
|
|
] || key;
|
|
return line({
|
|
metric: pool._1mBlocksMined,
|
|
name: poolName,
|
|
unit: Unit.count,
|
|
});
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
// AntPool & friends - pools sharing block templates
|
|
{
|
|
name: "AntPool & Friends",
|
|
tree: [
|
|
{
|
|
name: "Dominance",
|
|
title: "AntPool & Friends Dominance",
|
|
bottom: poolEntries
|
|
.filter(([key]) =>
|
|
ANTPOOL_AND_FRIENDS_IDS.includes(key.toLowerCase()),
|
|
)
|
|
.map(([key, pool]) => {
|
|
const poolName =
|
|
brk.POOL_ID_TO_POOL_NAME[
|
|
/** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (
|
|
key.toLowerCase()
|
|
)
|
|
] || key;
|
|
return line({
|
|
metric: pool._1mDominance,
|
|
name: poolName,
|
|
unit: Unit.percentage,
|
|
});
|
|
}),
|
|
},
|
|
{
|
|
name: "Blocks Mined",
|
|
title: "AntPool & Friends Blocks Mined (1m)",
|
|
bottom: poolEntries
|
|
.filter(([key]) =>
|
|
ANTPOOL_AND_FRIENDS_IDS.includes(key.toLowerCase()),
|
|
)
|
|
.map(([key, pool]) => {
|
|
const poolName =
|
|
brk.POOL_ID_TO_POOL_NAME[
|
|
/** @type {keyof typeof brk.POOL_ID_TO_POOL_NAME} */ (
|
|
key.toLowerCase()
|
|
)
|
|
] || key;
|
|
return line({
|
|
metric: pool._1mBlocksMined,
|
|
name: poolName,
|
|
unit: Unit.count,
|
|
});
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
// Individual pools
|
|
...poolsTree,
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|