/** Network section - On-chain activity and health */ import { colors } from "../utils/colors.js"; import { brk } from "../client.js"; import { Unit } from "../utils/units.js"; import { entries } from "../utils/array.js"; import { priceLine } from "./constants.js"; import { line, dots, baseline, fromSupplyPattern, fromBaseStatsPattern, chartsFromFullPerBlock, chartsFromCount, chartsFromValueFull, fromStatsPattern, chartsFromSumPerBlock, statsAtWindow, rollingWindowsTree, } from "./series.js"; import { satsBtcUsd, satsBtcUsdFrom } from "./shared.js"; /** * Create Network section * @returns {PartialOptionsGroup} */ export function createNetworkSection() { const { blocks, transactions, inputs, outputs, scripts, supply, distribution, } = brk.metrics; const st = colors.scriptType; // Addressable types - newest to oldest (for addresses/counts that only support addressable types) const addressTypes = /** @type {const} */ ([ { key: "p2a", name: "P2A", color: st.p2a, defaultActive: false }, { key: "p2tr", name: "P2TR", color: st.p2tr, defaultActive: true }, { key: "p2wsh", name: "P2WSH", color: st.p2wsh, defaultActive: true }, { key: "p2wpkh", name: "P2WPKH", color: st.p2wpkh, defaultActive: true }, { key: "p2sh", name: "P2SH", color: st.p2sh, defaultActive: true }, { key: "p2pkh", name: "P2PKH", color: st.p2pkh, defaultActive: true }, { key: "p2pk33", name: "P2PK33", color: st.p2pk33, defaultActive: false }, { key: "p2pk65", name: "P2PK65", color: st.p2pk65, defaultActive: false }, ]); // Non-addressable script types const nonAddressableTypes = /** @type {const} */ ([ { key: "p2ms", name: "P2MS", color: st.p2ms, defaultActive: false }, { key: "opreturn", name: "OP_RETURN", color: st.opreturn, defaultActive: false, }, { key: "emptyoutput", name: "Empty", color: st.empty, defaultActive: false, }, { key: "unknownoutput", name: "Unknown", color: st.unknown, defaultActive: false, }, ]); // All script types = addressable + non-addressable const scriptTypes = [...addressTypes, ...nonAddressableTypes]; // Address type groups (by era) const taprootAddresses = /** @type {const} */ ([ { key: "p2a", name: "P2A", color: st.p2a }, { key: "p2tr", name: "P2TR", color: st.p2tr }, ]); const segwitAddresses = /** @type {const} */ ([ { key: "p2wsh", name: "P2WSH", color: st.p2wsh }, { key: "p2wpkh", name: "P2WPKH", color: st.p2wpkh }, ]); const legacyAddresses = /** @type {const} */ ([ { key: "p2sh", name: "P2SH", color: st.p2sh }, { key: "p2pkh", name: "P2PKH", color: st.p2pkh }, { key: "p2pk33", name: "P2PK33", color: st.p2pk33 }, { key: "p2pk65", name: "P2PK65", color: st.p2pk65 }, ]); // Transacting types (transaction participation) const transactingTypes = /** @type {const} */ ([ { key: "sending", name: "Sending", title: "Unique Sending Addresses per Block", compareTitle: "Unique Sending Addresses per Block by Type", }, { key: "receiving", name: "Receiving", title: "Unique Receiving Addresses per Block", compareTitle: "Unique Receiving Addresses per Block by Type", }, { key: "both", name: "Sending & Receiving", title: "Unique Addresses Sending & Receiving per Block", compareTitle: "Unique Addresses Sending & Receiving per Block by Type", }, ]); // Balance change types const balanceTypes = /** @type {const} */ ([ { key: "balanceIncreased", name: "Accumulating", title: "Accumulating Addresses per Block", compareTitle: "Accumulating Addresses per Block by Type", }, { key: "balanceDecreased", name: "Distributing", title: "Distributing Addresses per Block", compareTitle: "Distributing Addresses per Block by Type", }, ]); // Count types for comparison charts // addrCount and emptyAddrCount have .count, totalAddrCount doesn't const countTypes = /** @type {const} */ ([ { name: "Funded", title: "Address Count by Type", /** @param {AddressableType} t */ getMetric: (t) => distribution.addrCount[t].count, }, { name: "Empty", title: "Empty Address Count by Type", /** @param {AddressableType} t */ getMetric: (t) => distribution.emptyAddrCount[t].count, }, { name: "Total", title: "Total Address Count by Type", /** @param {AddressableType} t */ getMetric: (t) => distribution.totalAddrCount[t], }, ]); /** * 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].count, name: "Funded", unit: Unit.count, }), line({ metric: distribution.emptyAddrCount[key].count, name: "Empty", color: colors.gray, unit: Unit.count, defaultActive: false, }), line({ metric: distribution.totalAddrCount[key], name: "Total", color: colors.default, unit: Unit.count, defaultActive: false, }), ], }, { name: "Trends", tree: [ { name: "30d Change", title: `${titlePrefix}Address Count 30d Change`, bottom: [ baseline({ metric: distribution.addrCount[key]._30dChange, name: "Funded", unit: Unit.count, }), baseline({ metric: distribution.emptyAddrCount[key]._30dChange, name: "Empty", color: colors.gray, unit: Unit.count, defaultActive: false, }), ], }, { name: "New", tree: (() => { const p = distribution.newAddrCount[key]; const t = `${titlePrefix}New Address Count`; return [ { name: "Sum", title: t, bottom: [ line({ metric: p.height, name: "base", unit: Unit.count }), ], }, rollingWindowsTree({ windows: p.sum, title: t, unit: Unit.count, }), { name: "Cumulative", title: `${t} (Total)`, bottom: [ line({ metric: p.cumulative, name: "all-time", unit: Unit.count, }), ], }, ]; })(), }, { name: "Reactivated", title: `${titlePrefix}Reactivated Addresses per Block`, bottom: fromBaseStatsPattern({ pattern: distribution.addressActivity[key].reactivated, window: "_24h", unit: Unit.count, }), }, { name: "Growth Rate", title: `${titlePrefix}Address Growth Rate per Block`, bottom: fromBaseStatsPattern({ pattern: distribution.growthRate[key], window: "_24h", unit: Unit.ratio, }), }, ], }, { name: "Transacting", tree: transactingTypes.map((t) => ({ name: t.name, title: `${titlePrefix}${t.title}`, bottom: fromBaseStatsPattern({ pattern: distribution.addressActivity[key][t.key], window: "_24h", unit: Unit.count, }), })), }, { name: "Balance", tree: balanceTypes.map((b) => ({ name: b.name, title: `${titlePrefix}${b.title}`, bottom: fromBaseStatsPattern({ pattern: distribution.addressActivity[key][b.key], window: "_24h", unit: Unit.count, }), })), }, ]; /** * Create Compare charts for an address group * @template {AddressableType} K * @param {string} groupName * @param {ReadonlyArray<{key: K, name: string, color: Color}>} types */ const createAddressCompare = (groupName, types) => ({ name: "Compare", tree: [ { name: "Count", tree: countTypes.map((c) => ({ name: c.name, title: `${groupName} ${c.title}`, bottom: types.map((t) => line({ metric: c.getMetric(t.key), name: t.name, color: t.color, unit: Unit.count, }), ), })), }, { name: "New", title: `${groupName} New Address Count`, bottom: types.flatMap((t) => [ line({ metric: distribution.newAddrCount[t.key].height, name: t.name, color: t.color, unit: Unit.count, }), line({ metric: distribution.newAddrCount[t.key].sum._24h, name: t.name, color: t.color, unit: Unit.count, }), ]), }, { name: "Reactivated", title: `${groupName} Reactivated Addresses per Block`, bottom: types.flatMap((t) => [ line({ metric: distribution.addressActivity[t.key].reactivated.height, name: t.name, color: t.color, unit: Unit.count, }), line({ metric: distribution.addressActivity[t.key].reactivated.average._24h, name: t.name, color: t.color, unit: Unit.count, }), ]), }, { name: "Growth Rate", title: `${groupName} Address Growth Rate per Block`, bottom: types.flatMap((t) => [ dots({ metric: distribution.growthRate[t.key].height, name: t.name, color: t.color, unit: Unit.ratio, }), dots({ metric: distribution.growthRate[t.key].average._24h, name: t.name, color: t.color, unit: Unit.ratio, }), ]), }, { name: "Transacting", tree: transactingTypes.map((tr) => ({ name: tr.name, title: `${groupName} ${tr.compareTitle}`, bottom: types.flatMap((t) => [ line({ metric: distribution.addressActivity[t.key][tr.key].height, name: t.name, color: t.color, unit: Unit.count, }), line({ metric: distribution.addressActivity[t.key][tr.key].average._24h, name: t.name, color: t.color, unit: Unit.count, }), ]), })), }, { name: "Balance", tree: balanceTypes.map((b) => ({ name: b.name, title: `${groupName} ${b.compareTitle}`, bottom: types.flatMap((t) => [ line({ metric: distribution.addressActivity[t.key][b.key].height, name: t.name, color: t.color, unit: Unit.count, }), line({ metric: distribution.addressActivity[t.key][b.key].average._24h, name: t.name, color: t.color, unit: Unit.count, }), ]), })), }, ], }); // Script type groups for Output Counts const legacyScripts = legacyAddresses.slice(1); // p2pkh, p2pk33, p2pk65 const scriptHashScripts = [legacyAddresses[0], nonAddressableTypes[0]]; // p2sh, p2ms const segwitScripts = [ /** @type {const} */ ({ key: "segwit", name: "All SegWit", color: colors.segwit, }), ...segwitAddresses, ]; const otherScripts = nonAddressableTypes.slice(1); // opreturn, empty, unknown /** * Create Compare charts for a script group * @template {keyof typeof scripts.count} K * @param {string} groupName * @param {ReadonlyArray<{key: K, name: string, color: Color}>} types */ const createScriptCompare = (groupName, types) => ({ name: "Compare", tree: [ { name: "Sum", title: `${groupName} Output Count`, bottom: types.map((t) => line({ metric: /** @type {CountPattern} */ (scripts.count[t.key]) .sum._24h, name: t.name, color: t.color, unit: Unit.count, }), ), }, { name: "Cumulative", title: `${groupName} Output Count (Total)`, bottom: types.map((t) => line({ metric: /** @type {CountPattern} */ (scripts.count[t.key]) .cumulative, name: t.name, color: t.color, unit: Unit.count, }), ), }, ], }); return { name: "Network", tree: [ // Supply { name: "Supply", tree: [ { name: "Circulating", title: "Circulating Supply", bottom: fromSupplyPattern({ pattern: supply.circulating, title: "Supply", }), }, { name: "Inflation", title: "Inflation Rate", bottom: [ dots({ metric: supply.inflation, name: "Rate", unit: Unit.percentage, }), ], }, { name: "Unspendable", tree: [ { name: "Sum", title: "Unspendable Supply", bottom: satsBtcUsdFrom({ source: supply.burned.unspendable, key: "base", name: "sum", }), }, { name: "Cumulative", title: "Unspendable Supply (Total)", bottom: satsBtcUsdFrom({ source: supply.burned.unspendable, key: "cumulative", name: "all-time", }), }, ], }, { name: "OP_RETURN", tree: chartsFromValueFull({ pattern: scripts.value.opreturn, title: "OP_RETURN Burned", }), }, ], }, // Transactions { name: "Transactions", tree: [ { name: "Count", tree: chartsFromFullPerBlock({ pattern: transactions.count.txCount, title: "Transaction Count", unit: Unit.count, }), }, { name: "Fee Rate", title: "Transaction Fee Rate", bottom: fromStatsPattern({ pattern: transactions.fees.feeRate.block, unit: Unit.feeRate, }), }, { name: "Volume", tree: [ { name: "Transferred", title: "Transaction Volume", bottom: [ ...satsBtcUsd({ pattern: transactions.volume.sentSum, name: "Sent", }), ...satsBtcUsd({ pattern: transactions.volume.receivedSum, name: "Received", color: colors.entity.output, }), ], }, { name: "Annualized", title: "Annualized Transaction Volume", bottom: satsBtcUsd({ pattern: transactions.volume.annualizedVolume, name: "Annualized", }), }, ], }, { name: "Size", title: "Transaction Size", bottom: [ ...fromStatsPattern({ pattern: transactions.size.weight.block, unit: Unit.wu, }), ...fromStatsPattern({ pattern: transactions.size.vsize.block, unit: Unit.vb, }), ], }, { name: "Versions", tree: [ { name: "Base", title: "Transaction Versions", bottom: entries(transactions.versions).map( ([v, data], i, arr) => line({ metric: data.height, name: v, color: colors.at(i, arr.length), unit: Unit.count, }), ), }, { name: "Cumulative", title: "Transaction Versions (Total)", bottom: entries(transactions.versions).map( ([v, data], i, arr) => line({ metric: data.cumulative, name: v, color: colors.at(i, arr.length), unit: Unit.count, }), ), }, ], }, { name: "Velocity", title: "Transaction Velocity", bottom: [ line({ metric: supply.velocity.btc, name: "BTC", unit: Unit.ratio, }), line({ metric: supply.velocity.usd, name: "USD", color: colors.usd, unit: Unit.ratio, }), ], }, ], }, // Blocks { name: "Blocks", tree: [ { name: "Count", tree: [ { name: "Base", title: "Block Count", bottom: [ line({ metric: blocks.count.blockCount.height, name: "base", unit: Unit.count, }), line({ metric: blocks.count.blockCountTarget, name: "Target", color: colors.gray, unit: Unit.count, options: { lineStyle: 4 }, }), ], }, rollingWindowsTree({ windows: blocks.count.blockCountSum, title: "Block Count", unit: Unit.count, }), { name: "Cumulative", title: "Block Count (Total)", bottom: [ line({ metric: blocks.count.blockCount.cumulative, name: "all-time", unit: Unit.count, }), ], }, ], }, { name: "Interval", title: "Block Interval", bottom: [ ...fromBaseStatsPattern({ pattern: blocks.interval, window: "_24h", unit: Unit.secs, }), priceLine({ unit: Unit.secs, name: "Target", number: 600 }), ], }, { name: "Size", tree: [ { name: "Base", title: "Block Size", bottom: [ line({ metric: blocks.totalSize, name: "base", unit: Unit.bytes, }), ], }, rollingWindowsTree({ windows: blocks.size.sum, title: "Block Size", unit: Unit.bytes, }), { name: "Distribution", title: "Block Size Distribution", bottom: [ line({ metric: blocks.totalSize, name: "base", unit: Unit.bytes, }), ...fromStatsPattern({ pattern: statsAtWindow(blocks.size, "_24h"), unit: Unit.bytes, }), ], }, { name: "Cumulative", title: "Block Size (Total)", bottom: [ line({ metric: blocks.size.cumulative, name: "all-time", unit: Unit.bytes, }), ], }, ], }, { name: "Weight", tree: [ { name: "Base", title: "Block Weight", bottom: [ line({ metric: blocks.weight.base, name: "base", unit: Unit.wu, }), ], }, rollingWindowsTree({ windows: blocks.weight.sum, title: "Block Weight", unit: Unit.wu, }), { name: "Distribution", title: "Block Weight Distribution", bottom: [ line({ metric: blocks.weight.base, name: "base", unit: Unit.wu, }), ...fromStatsPattern({ pattern: statsAtWindow(blocks.weight, "_24h"), unit: Unit.wu, }), ], }, { name: "Cumulative", title: "Block Weight (Total)", bottom: [ line({ metric: blocks.weight.cumulative, name: "all-time", unit: Unit.wu, }), ], }, ], }, { name: "vBytes", tree: [ { name: "Base", title: "Block vBytes", bottom: [ line({ metric: blocks.vbytes.height, name: "base", unit: Unit.vb, }), ], }, rollingWindowsTree({ windows: blocks.vbytes.sum, title: "Block vBytes", unit: Unit.vb, }), { name: "Distribution", title: "Block vBytes Distribution", bottom: [ line({ metric: blocks.vbytes.height, name: "base", unit: Unit.vb, }), ...fromStatsPattern({ pattern: statsAtWindow(blocks.vbytes, "_24h"), unit: Unit.vb, }), ], }, { name: "Cumulative", title: "Block vBytes (Total)", bottom: [ line({ metric: blocks.vbytes.cumulative, name: "all-time", unit: Unit.vb, }), ], }, ], }, { name: "Fullness", title: "Block Fullness", bottom: fromBaseStatsPattern({ pattern: blocks.fullness, window: "_24h", unit: Unit.percentage, }), }, ], }, // UTXOs { name: "UTXOs", tree: [ { name: "Count", title: "UTXO Count", bottom: [ line({ metric: outputs.count.utxoCount, name: "Count", unit: Unit.count, }), ], }, { name: "30d Change", title: "UTXO Count 30d Change", bottom: [ baseline({ metric: distribution.utxoCohorts.all.outputs.utxoCount30dChange, name: "30d Change", unit: Unit.count, }), ], }, { name: "Flow", title: "UTXO Flow", bottom: [ line({ metric: outputs.count.totalCount.sum, name: "Created", color: colors.entity.output, unit: Unit.count, }), line({ metric: inputs.count.sum, name: "Spent", color: colors.entity.input, unit: Unit.count, }), ], }, ], }, { name: "Inputs", tree: chartsFromSumPerBlock({ pattern: inputs.count, title: "Input Count", unit: Unit.count, }), }, { name: "Outputs", tree: chartsFromSumPerBlock({ pattern: outputs.count.totalCount, title: "Output Count", unit: Unit.count, }), }, { name: "Activity Rate", title: "Activity Rate", bottom: [ dots({ metric: transactions.volume.txPerSec, name: "TX/sec", color: colors.entity.tx, unit: Unit.perSec, }), dots({ metric: transactions.volume.inputsPerSec, name: "Inputs/sec", color: colors.entity.input, unit: Unit.perSec, }), dots({ metric: transactions.volume.outputsPerSec, name: "Outputs/sec", color: colors.entity.output, unit: Unit.perSec, }), ], }, // Addresses { name: "Addresses", tree: [ // Overview - global metrics for all addresses { name: "Overview", tree: createAddressMetricsTree("all", "") }, // Top-level Compare - all types { name: "Compare", tree: [ { name: "Count", tree: countTypes.map((c) => ({ name: c.name, title: c.title, bottom: addressTypes.map((t) => line({ metric: c.getMetric(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].height, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), line({ metric: distribution.newAddrCount[t.key].sum._24h, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ]), }, { name: "Reactivated", title: "Reactivated Addresses per Block by Type", bottom: addressTypes.flatMap((t) => [ line({ metric: distribution.addressActivity[t.key].reactivated.height, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), line({ metric: distribution.addressActivity[t.key].reactivated.average ._24h, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ]), }, { name: "Growth Rate", title: "Address Growth Rate per Block by Type", bottom: addressTypes.flatMap((t) => [ dots({ metric: distribution.growthRate[t.key].height, name: t.name, color: t.color, unit: Unit.ratio, defaultActive: t.defaultActive, }), dots({ metric: distribution.growthRate[t.key].average._24h, name: t.name, color: t.color, unit: Unit.ratio, defaultActive: t.defaultActive, }), ]), }, { name: "Transacting", tree: transactingTypes.map((tr) => ({ name: tr.name, title: tr.compareTitle, bottom: addressTypes.flatMap((t) => [ line({ metric: distribution.addressActivity[t.key][tr.key].height, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), line({ metric: distribution.addressActivity[t.key][tr.key].average ._24h, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ]), })), }, { name: "Balance", tree: balanceTypes.map((b) => ({ name: b.name, title: b.compareTitle, bottom: addressTypes.flatMap((t) => [ line({ metric: distribution.addressActivity[t.key][b.key].height, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), line({ metric: distribution.addressActivity[t.key][b.key].average._24h, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ]), })), }, ], }, // Legacy (pre-SegWit) { name: "Legacy", tree: [ createAddressCompare("Legacy", legacyAddresses), ...legacyAddresses.map((t) => ({ name: t.name, tree: createAddressMetricsTree(t.key, `${t.name} `), })), ], }, // SegWit { name: "SegWit", tree: [ createAddressCompare("SegWit", segwitAddresses), ...segwitAddresses.map((t) => ({ name: t.name, tree: createAddressMetricsTree(t.key, `${t.name} `), })), ], }, // Taproot { name: "Taproot", tree: [ createAddressCompare("Taproot", taprootAddresses), ...taprootAddresses.map((t) => ({ name: t.name, tree: createAddressMetricsTree(t.key, `${t.name} `), })), ], }, ], }, // Scripts { name: "Scripts", tree: [ { name: "By Type", tree: [ // Compare section { name: "Compare", tree: [ { name: "Sum", title: "Output Count by Script Type", bottom: scriptTypes.map((t) => line({ metric: /** @type {CountPattern} */ ( scripts.count[t.key] ).sum._24h, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ), }, { name: "Cumulative", title: "Output Count by Script Type (Total)", bottom: scriptTypes.map((t) => line({ metric: scripts.count[t.key].cumulative, name: t.name, color: t.color, unit: Unit.count, defaultActive: t.defaultActive, }), ), }, ], }, { name: "Legacy", tree: [ createScriptCompare("Legacy", legacyScripts), ...legacyScripts.map((t) => ({ name: t.name, tree: chartsFromCount({ pattern: /** @type {CountPattern} */ ( scripts.count[t.key] ), title: `${t.name} Output Count`, unit: Unit.count, }), })), ], }, { name: "Script Hash", tree: [ createScriptCompare("Script Hash", scriptHashScripts), ...scriptHashScripts.map((t) => ({ name: t.name, tree: chartsFromCount({ pattern: /** @type {CountPattern} */ ( scripts.count[t.key] ), title: `${t.name} Output Count`, unit: Unit.count, }), })), ], }, { name: "SegWit", tree: [ createScriptCompare("SegWit", segwitScripts), ...segwitScripts.map((t) => ({ name: t.name, tree: chartsFromCount({ pattern: /** @type {CountPattern} */ ( scripts.count[t.key] ), title: `${t.name} Output Count`, unit: Unit.count, }), })), ], }, { name: "Taproot", tree: [ createScriptCompare("Taproot", taprootAddresses), ...taprootAddresses.map((t) => ({ name: t.name, tree: chartsFromCount({ pattern: /** @type {CountPattern} */ ( scripts.count[t.key] ), title: `${t.name} Output Count`, unit: Unit.count, }), })), ], }, { name: "Other", tree: [ createScriptCompare("Other", otherScripts), ...otherScripts.map((t) => ({ name: t.name, tree: chartsFromCount({ pattern: /** @type {CountPattern} */ ( scripts.count[t.key] ), title: `${t.name} Output Count`, unit: Unit.count, }), })), ], }, ], }, { name: "Adoption", tree: [ { name: "Compare", title: "Script Adoption", bottom: [ line({ metric: scripts.adoption.segwit, name: "SegWit", color: colors.segwit, unit: Unit.percentage, }), line({ metric: scripts.adoption.taproot, name: "Taproot", color: taprootAddresses[1].color, unit: Unit.percentage, }), ], }, { name: "SegWit", title: "SegWit Adoption", bottom: [ line({ metric: scripts.adoption.segwit, name: "Adoption", unit: Unit.percentage, }), ], }, { name: "Taproot", title: "Taproot Adoption", bottom: [ line({ metric: scripts.adoption.taproot, name: "Adoption", unit: Unit.percentage, }), ], }, ], }, ], }, ], }; }