/** Mining section - Network security and miner economics */ import { Unit } from "../utils/units.js"; import { entries, includes } from "../utils/array.js"; import { colors } from "../utils/colors.js"; import { line, dots, dotted, distributionBtcSatsUsd, statsAtWindow, ROLLING_WINDOWS, percentRatio, percentRatioBaseline, chartsFromCount, } from "./series.js"; import { satsBtcUsdFrom, satsBtcUsdFullTree, revenueBtcSatsUsd, revenueRollingBtcSatsUsd, formatCohortTitle, } from "./shared.js"; import { brk } from "../utils/client.js"; /** Major pools to show in Compare section (by current hashrate dominance) */ const MAJOR_POOL_IDS = /** @type {const} */ ([ "foundryusa", "antpool", "viabtc", "f2pool", "marapool", "braiinspool", "spiderpool", "ocean", ]); /** * AntPool & friends - pools sharing AntPool's block templates * Based on b10c's research: https://b10c.me/blog/015-bitcoin-mining-centralization/ */ const ANTPOOL_AND_FRIENDS_IDS = /** @type {const} */ ([ "antpool", "poolin", "btccom", "braiinspool", "ultimuspool", "binancepool", "secpool", "sigmapoolcom", "rawpool", "luxor", ]); /** * Create Mining section * @returns {PartialOptionsGroup} */ export function createMiningSection() { const { blocks, pools, mining } = brk.series; const majorPoolData = entries(pools.major).map(([id, pool]) => ({ id, name: brk.POOL_ID_TO_POOL_NAME[id], pool, })); const minorPoolData = entries(pools.minor).map(([id, pool]) => ({ id, name: brk.POOL_ID_TO_POOL_NAME[id], pool, })); const featuredPools = majorPoolData.filter((p) => includes(MAJOR_POOL_IDS, p.id), ); const antpoolFriends = majorPoolData.filter((p) => includes(ANTPOOL_AND_FRIENDS_IDS, p.id), ); /** * @param {(metric: string) => string} title * @param {string} metric * @param {DominancePattern} dominance */ const dominanceTree = (title, metric, dominance) => ({ name: "Dominance", tree: [ { name: "Compare", 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, 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, }), })), { name: "All Time", title: title(`All Time ${metric}`), bottom: percentRatio({ pattern: dominance, name: "Dominance", color: colors.time.all, }), }, ], }); /** * @param {typeof majorPoolData} poolList */ const createPoolTree = (poolList) => poolList.map(({ name, pool }) => { const title = formatCohortTitle(name); return { name, tree: [ dominanceTree(title, "Dominance", pool.dominance), { name: "Blocks Mined", tree: chartsFromCount({ pattern: pool.blocksMined, title, metric: "Blocks Mined", unit: Unit.count, }), }, { name: "Rewards", tree: satsBtcUsdFullTree({ pattern: pool.rewards, title, metric: "Rewards", }), }, ], }; }); /** * @param {typeof minorPoolData} poolList */ const createMinorPoolTree = (poolList) => poolList.map(({ name, pool }) => { const title = formatCohortTitle(name); return { name, tree: [ { name: "Dominance", title: title("Dominance"), bottom: percentRatio({ pattern: pool.dominance, name: "All Time", color: colors.time.all, }), }, { name: "Blocks Mined", tree: chartsFromCount({ pattern: pool.blocksMined, title, metric: "Blocks Mined", unit: Unit.count, }), }, ], }; }); /** * @param {string} groupTitle * @param {typeof majorPoolData} poolList */ const createPoolCompare = (groupTitle, poolList) => ({ name: "Compare", tree: [ { name: "Dominance", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: formatCohortTitle(groupTitle)(`${w.title} Dominance`), bottom: poolList.flatMap((p, i) => percentRatio({ pattern: p.pool.dominance[w.key], name: p.name, color: colors.at(i, poolList.length), }), ), })), }, { name: "Blocks Mined", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: formatCohortTitle(groupTitle)(`${w.title} Blocks Mined`), bottom: poolList.map((p, i) => line({ series: p.pool.blocksMined.sum[w.key], name: p.name, color: colors.at(i, poolList.length), unit: Unit.count, }), ), })), }, ], }); return { name: "Mining", tree: [ { name: "Hashrate", tree: [ { name: "Current", title: "Network Hashrate", bottom: [ dots({ series: mining.hashrate.rate.base, name: "Hashrate", unit: Unit.hashRate, }), line({ series: mining.hashrate.rate.sma._1w, name: "1w SMA", color: colors.time._1w, unit: Unit.hashRate, defaultActive: false, }), line({ series: mining.hashrate.rate.sma._1m, name: "1m SMA", color: colors.time._1m, unit: Unit.hashRate, defaultActive: false, }), line({ series: mining.hashrate.rate.sma._2m, name: "2m SMA", color: colors.indicator.main, unit: Unit.hashRate, defaultActive: false, }), line({ series: mining.hashrate.rate.sma._1y, name: "1y SMA", color: colors.time._1y, unit: Unit.hashRate, defaultActive: false, }), dotted({ series: blocks.difficulty.hashrate, name: "From Difficulty", color: colors.default, unit: Unit.hashRate, }), line({ series: mining.hashrate.rate.ath, name: "ATH", color: colors.loss, unit: Unit.hashRate, defaultActive: false, }), ], }, { name: "ATH", title: "Network Hashrate ATH", bottom: [ line({ series: mining.hashrate.rate.ath, name: "ATH", color: colors.loss, unit: Unit.hashRate, }), dots({ series: mining.hashrate.rate.base, name: "Hashrate", color: colors.bitcoin, unit: Unit.hashRate, }), ], }, { name: "Drawdown", title: "Network Hashrate Drawdown", bottom: percentRatio({ pattern: mining.hashrate.rate.drawdown, name: "Drawdown", color: colors.loss, }), }, ], }, { name: "Revenue", tree: [ ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Mining Revenue`, bottom: revenueRollingBtcSatsUsd({ coinbase: mining.rewards.coinbase.average[w.key], subsidy: mining.rewards.subsidy.average[w.key], fee: mining.rewards.fees.average[w.key], }), })), { name: "Cumulative", title: "Cumulative Mining Revenue", bottom: revenueBtcSatsUsd({ coinbase: mining.rewards.coinbase, subsidy: mining.rewards.subsidy, fee: mining.rewards.fees, key: "cumulative", }), }, { name: "Coinbase", tree: satsBtcUsdFullTree({ pattern: mining.rewards.coinbase, metric: "Coinbase Rewards", }), }, { name: "Subsidy", tree: satsBtcUsdFullTree({ pattern: mining.rewards.subsidy, metric: "Block Subsidy", }), }, { name: "Fees", tree: [ ...satsBtcUsdFullTree({ pattern: mining.rewards.fees, metric: "Transaction Fee Revenue", }), { name: "Distribution", tree: ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Fee Revenue per Block Distribution`, bottom: distributionBtcSatsUsd( statsAtWindow(mining.rewards.fees, w.key), ), })), }, ], }, { name: "Dominance", tree: [ ...ROLLING_WINDOWS.map((w) => ({ 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, }), ], })), { 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, }), ], }, ], }, { name: "Fee-to-Subsidy", 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, }), ], })), }, { name: "Unclaimed", title: "Unclaimed Rewards", bottom: satsBtcUsdFrom({ source: mining.rewards.unclaimed, key: "cumulative", name: "All Time", }), }, ], }, { name: "Economics", tree: [ { 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, }), ], }, { 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, }), ], }, { 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, }), ], }, ], }, { name: "Halving", tree: [ { 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, }), ], }, { name: "Epoch", title: "Halving Epoch", bottom: [ line({ series: blocks.halving.epoch, name: "Epoch", unit: Unit.epoch, }), ], }, ], }, { name: "Difficulty", tree: [ { name: "Current", title: "Mining 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", }), }, { 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, }), ], }, { name: "Epoch", title: "Difficulty Epoch", bottom: [ line({ series: blocks.difficulty.epoch, name: "Epoch", unit: Unit.epoch, }), ], }, ], }, { name: "Pools", tree: [ createPoolCompare("Major Pools", featuredPools), { name: "AntPool & Friends", tree: [ createPoolCompare("AntPool & Friends", antpoolFriends), ...createPoolTree(antpoolFriends), ], }, { name: "Major", tree: createPoolTree(majorPoolData), }, { name: "Minor", tree: createMinorPoolTree(minorPoolData), }, ], }, ], }; }