website_next: snapshot

This commit is contained in:
nym21
2026-07-04 22:09:40 +02:00
parent a99c06013b
commit 0bf2cd77dc
5 changed files with 295 additions and 180 deletions
+8 -1
View File
@@ -208,6 +208,7 @@ impl Query {
let computer = self.computer();
let reader = self.reader();
let all_pools = pools();
let pool_heights = computer.pools.pool_heights.read();
// Bulk read all indexed data
let blockhashes = indexer.vecs.blocks.blockhash.collect_range_at(begin, end);
@@ -398,6 +399,11 @@ impl Query {
let pool_slug = pool_slugs[i];
let pool = all_pools.get(pool_slug);
let height = begin + i;
let block_number = pool_heights
.get(&pool_slug)
.map(|heights| heights.partition_point(|h| h.to_usize() <= height) as u64)
.unwrap_or(0);
let miner_names = if pool_slug == PoolSlug::Ocean {
Self::parse_datum_miner_names(&scriptsig_bytes)
@@ -410,7 +416,7 @@ impl Query {
let info = BlockInfo {
id: blockhashes[i],
height: Height::from(begin + i),
height: Height::from(height),
version: header.version,
timestamp: timestamps[i],
bits: header.bits,
@@ -444,6 +450,7 @@ impl Query {
id: pool.mempool_unique_id(),
name: pool.name.to_string(),
slug: pool_slug,
block_number,
miner_names,
},
avg_fee: Sats::from(total_fees_u64.checked_div(non_coinbase).unwrap_or(0)),
+4
View File
@@ -18,6 +18,10 @@ pub struct BlockPool {
/// URL-friendly pool identifier
pub slug: PoolSlug,
/// This block's ordinal among blocks attributed to this pool
#[schemars(example = 215_000)]
pub block_number: u64,
/// Miner name tags found in coinbase scriptsig
pub miner_names: Option<Vec<String>>,
}
+1
View File
@@ -259,6 +259,7 @@ Matches mempool.space/bitcoin-cli behavior.
* @property {number} id - Unique pool identifier
* @property {string} name - Pool name
* @property {PoolSlug} slug - URL-friendly pool identifier
* @property {number} blockNumber - This block's ordinal among blocks attributed to this pool
* @property {?string[]=} minerNames - Miner name tags found in coinbase scriptsig
*/
/**
+121 -85
View File
@@ -12,6 +12,8 @@ import { createFeeChart } from "./fee-chart.js";
/** @typedef {Awaited<ReturnType<typeof brk.getBlocksV1>>[number]} Block */
const MAX_BLOCK_WEIGHT = 4_000_000;
const DIFFICULTY_EPOCH_BLOCKS = 2_016;
const HALVING_EPOCH_BLOCKS = 210_000;
/** @param {number} bytes */
function formatBytes(bytes) {
@@ -100,54 +102,11 @@ function createRow(term, value) {
return row;
}
/**
* @param {string} label
* @param {string | Node} value
*/
function createStat(label, value) {
const stat = document.createElement("div");
const name = document.createElement("span");
const amount = document.createElement("strong");
stat.dataset.stat = "";
name.textContent = label;
amount.append(value);
stat.append(name, amount);
return stat;
}
/** @param {[string, string | Node][]} items */
function createStats(items) {
const stats = document.createElement("div");
stats.dataset.stats = "";
stats.append(...items.map(([label, value]) => createStat(label, value)));
return stats;
}
/** @param {string} title */
function groupName(title) {
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
}
/**
* @param {string} title
* @param {[string, string | Node][]} stats
* @param {Node[]} [children]
*/
function createStatBox(title, stats, children = []) {
const box = document.createElement("div");
const heading = document.createElement("h3");
box.dataset.statBox = groupName(title);
heading.textContent = title;
box.append(heading, createStats(stats), ...children);
return box;
}
/**
* @param {string} label
* @param {(string | Node)[]} values
@@ -168,11 +127,12 @@ function createInlineRow(label, values) {
/**
* @param {string} label
* @param {string | Node} value
* @param {string} [type]
*/
function createInlineBox(label, value) {
function createInlineBox(label, value, type = "inline") {
const box = document.createElement("div");
box.dataset.blockBox = "inline";
box.dataset.blockBox = type;
box.append(createInlineRow(label, [value]));
return box;
@@ -183,21 +143,16 @@ function formatBlockFill(block) {
return `${((block.weight / MAX_BLOCK_WEIGHT) * 100).toFixed(1)}%`;
}
/** @param {number | string} bits */
function formatBits(bits) {
return typeof bits === "number" ? `0x${bits.toString(16)}` : bits;
}
/**
* @param {string} label
* @param {string} value
*/
function createMinerStat(label, value) {
function createMetricStat(label, value) {
const stat = document.createElement("div");
const name = document.createElement("span");
const amount = document.createElement("strong");
stat.dataset.minerStat = "";
stat.dataset.metricStat = "";
name.textContent = label;
amount.textContent = value;
stat.append(name, amount);
@@ -205,34 +160,114 @@ function createMinerStat(label, value) {
return stat;
}
/** @param {string} raw */
function getCoinbaseMessage(raw) {
return (raw.match(/[\x20-\x7e]{2,}/g) ?? [])
.map((value) => value.trim())
.filter((value) => /[A-Za-z0-9]/.test(value))
.join(" · ");
}
/** @param {string} raw */
function createCoinbaseMessage(raw) {
const message = getCoinbaseMessage(raw);
if (!message) return null;
const element = document.createElement("p");
element.dataset.coinbaseMessage = "";
element.textContent = message;
return element;
}
/**
* @param {string} label
* @param {number} height
* @param {number} length
* @param {string} color
*/
function createEpochProgress(label, height, length, color) {
const progress = (height % length) + 1;
const row = document.createElement("div");
const head = document.createElement("div");
const name = document.createElement("span");
const value = document.createElement("strong");
const bar = document.createElement("div");
const done = document.createElement("span");
const remaining = document.createElement("span");
row.dataset.epoch = "";
head.dataset.epochHead = "";
bar.dataset.epochBar = "";
done.dataset.epochSegment = "done";
remaining.dataset.epochSegment = "remaining";
row.style.setProperty("--epoch-color", color);
done.style.setProperty("--share", `${(progress / length) * 100}%`);
remaining.style.setProperty("--share", `${((length - progress) / length) * 100}%`);
name.textContent = label;
value.textContent = `${((progress / length) * 100).toFixed(1)}%`;
head.append(name, value);
bar.append(done, remaining);
row.append(head, bar);
return row;
}
/** @param {Block} block */
function createMinerSummary(block) {
const { pool } = block.extras;
const pane = document.createElement("div");
const head = document.createElement("div");
const identity = document.createElement("div");
const title = document.createElement("div");
const name = document.createElement("strong");
const blockNumber = document.createElement("span");
const slug = document.createElement("span");
const stats = document.createElement("div");
const logo = createPoolLogo(pool);
const coinbaseMessage = createCoinbaseMessage(block.extras.coinbaseSignatureAscii);
pane.dataset.minerPane = "";
head.dataset.minerHead = "";
identity.dataset.minerIdentity = "";
title.dataset.minerTitle = "";
slug.dataset.minerSlug = "";
stats.dataset.minerStats = "";
logo.dataset.minerLogo = "";
name.textContent = pool.name;
slug.textContent = `#${pool.slug}`;
identity.append(name, slug);
// TODO: remove fallback after the server includes pool.blockNumber everywhere.
blockNumber.textContent = `#${(pool.blockNumber || 0).toLocaleString()}`;
slug.textContent = pool.slug;
title.append(name, blockNumber);
identity.append(title, slug);
head.append(identity, logo);
stats.append(
createMinerStat("Difficulty", block.difficulty.toLocaleString()),
createMinerStat("Bits", formatBits(block.bits)),
createMinerStat("Nonce", block.nonce.toLocaleString()),
pane.append(head, ...(coinbaseMessage ? [coinbaseMessage] : []));
return pane;
}
/** @param {Block} block */
function createDifficultySummary(block) {
const pane = document.createElement("div");
pane.dataset.metricList = "";
pane.append(
createMetricStat("Difficulty", block.difficulty.toLocaleString()),
createEpochProgress(
"Difficulty epoch",
block.height,
DIFFICULTY_EPOCH_BLOCKS,
"var(--orange)",
),
createEpochProgress(
"Halving epoch",
block.height,
HALVING_EPOCH_BLOCKS,
"var(--red)",
),
);
pane.append(head, stats);
return pane;
}
@@ -320,16 +355,19 @@ function createRewardPart(type, label, sats, total, price) {
}
/**
* @param {string} label
* @param {number} sats
* @param {number} price
*/
function createRewardTotal(sats, price) {
function createRewardTotal(label, sats, price) {
const total = document.createElement("div");
const name = document.createElement("span");
const amount = createBtcAmount("strong", sats);
const usd = createSatsUsdAmount(sats, price);
total.dataset.rewardTotal = "";
total.append(amount, usd);
name.textContent = label;
total.append(name, amount, usd);
return total;
}
@@ -365,18 +403,11 @@ function setRewardPreview(rewards, activeKey) {
/** @param {Block["extras"]} extras */
function createRewardSummary(extras) {
const subsidy = extras.reward - extras.totalFees;
const rewards = document.createElement("div");
const bar = document.createElement("div");
const split = createLegendList({ fill: true });
const rewards = createStatBox(
"Rewards",
[],
[
createRewardTotal(extras.reward, extras.price),
bar,
split,
],
);
rewards.dataset.statBox = "rewards";
appendLegendListItem(
split,
createRewardPart("subsidy", "Subsidy", subsidy, extras.reward, extras.price),
@@ -390,6 +421,7 @@ function createRewardSummary(extras) {
createRewardSegment("subsidy", subsidy, extras.reward),
createRewardSegment("fees", extras.totalFees, extras.reward),
);
rewards.append(createRewardTotal("Rewards", extras.reward, extras.price), bar, split);
rewards.addEventListener("pointerenter", (event) => {
setRewardPreview(rewards, getRewardKey(event.target));
@@ -415,8 +447,8 @@ function createTransactionSummary(block) {
transactions.dataset.blockBox = "tx";
io.dataset.blockIo = "";
io.append(
createInlineBox("Input", extras.totalInputs.toLocaleString()),
createInlineBox("Output", extras.totalOutputs.toLocaleString()),
createInlineBox("Input", extras.totalInputs.toLocaleString(), "input"),
createInlineBox("Output", extras.totalOutputs.toLocaleString(), "output"),
);
transactions.append(
createInlineRow("Tx", [block.txCount.toLocaleString()]),
@@ -466,9 +498,10 @@ export function createBlockDetails() {
const header = document.createElement("header");
const titleRow = document.createElement("div");
const title = document.createElement("h1");
const summary = document.createElement("p");
const date = document.createElement("time");
const meta = document.createElement("div");
const hash = document.createElement("p");
const price = createUsdAmount("output", 0, {
size: "title",
tone: "positive",
});
const content = document.createElement("div");
@@ -476,8 +509,12 @@ export function createBlockDetails() {
element.id = "block-details";
element.hidden = true;
titleRow.dataset.blockTitle = "";
titleRow.append(title, price);
header.append(titleRow, summary);
date.dataset.blockDate = "";
meta.dataset.blockMeta = "";
hash.dataset.blockHashLine = "";
titleRow.append(title, date);
meta.append(hash, price);
header.append(titleRow, meta);
element.append(header, content);
/** @param {Block} block */
@@ -486,13 +523,10 @@ export function createBlockDetails() {
element.hidden = false;
title.replaceChildren(...createTitle(block.height));
summary.replaceChildren(
createHashElement(block.id),
document.createElement("br"),
formatDateTime(block.timestamp),
);
date.dateTime = new Date(block.timestamp * 1_000).toISOString();
date.textContent = formatDateTime(block.timestamp);
hash.replaceChildren(createHashElement(block.id));
renderUsdAmount(price, extras.price, {
size: "title",
tone: "positive",
});
@@ -503,13 +537,15 @@ export function createBlockDetails() {
appendGroup(content, "Mining", [], [createMinerSummary(block)], false);
appendGroup(content, "Difficulty", [], [createDifficultySummary(block)], false);
appendGroup(content, "Rewards", [], [createRewardSummary(extras)], false);
appendGroup(content, "Block", [], [createTransactionSummary(block)], false);
appendGroup(content, "Fees", [], [
createFeeChart(extras.feeRange, extras.avgFeeRate, formatFeeRate),
]);
], false);
}
return /** @type {const} */ ({
+161 -94
View File
@@ -22,12 +22,12 @@
display: grid;
padding-bottom: 1.25rem;
[data-block-title] {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: space-between;
:is([data-block-title], [data-block-meta]) {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.35rem 1rem;
align-items: baseline;
min-width: 0;
}
h1 {
@@ -56,15 +56,28 @@
line-height: var(--line-height-lg);
}
p {
[data-block-date],
[data-block-hash-line] {
color: var(--gray);
}
[data-block-date] {
white-space: nowrap;
text-align: right;
}
[data-block-hash-line] {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
> div {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
gap: 2rem;
}
section {
@@ -82,7 +95,7 @@
[data-miner-pane] {
display: grid;
gap: 0.75rem;
gap: 1rem;
min-width: 0;
}
@@ -90,12 +103,21 @@
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 1rem;
align-items: start;
align-items: center;
min-width: 0;
}
[data-miner-identity] {
display: grid;
gap: 0.125rem;
min-width: 0;
}
[data-miner-title] {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: baseline;
min-width: 0;
> strong {
@@ -106,29 +128,49 @@
font-weight: 450;
line-height: var(--line-height-sm);
}
> span {
color: var(--gray);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
}
}
[data-miner-slug] {
min-width: 0;
overflow-wrap: anywhere;
color: var(--gray);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
font-size: var(--font-size-sm);
line-height: var(--line-height-sm);
}
[data-miner-logo] {
width: 2rem;
height: 2rem;
width: 1.25rem;
height: 1.25rem;
object-fit: contain;
}
[data-miner-stats] {
[data-coinbase-message] {
min-width: 0;
overflow: hidden;
color: var(--white);
font-size: var(--font-size-sm);
font-style: italic;
line-height: var(--line-height-sm);
text-overflow: ellipsis;
white-space: nowrap;
}
}
&:is([data-group="mining"], [data-group="difficulty"]) {
[data-metric-list] {
display: grid;
gap: 0.35rem;
gap: 0.5rem;
min-width: 0;
}
[data-miner-stat] {
[data-metric-stat] {
display: grid;
grid-template-columns: minmax(5.5rem, auto) minmax(0, 1fr);
gap: 0.75rem;
@@ -152,6 +194,61 @@
text-align: right;
}
}
[data-epoch] {
display: grid;
gap: 0.25rem;
min-width: 0;
}
[data-epoch-head] {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.75rem;
align-items: center;
min-width: 0;
> span {
min-width: 0;
overflow-wrap: anywhere;
color: var(--epoch-color);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
text-transform: uppercase;
}
strong {
color: var(--white);
font-size: var(--font-size-sm);
font-weight: 450;
line-height: var(--line-height-sm);
text-align: right;
}
}
[data-epoch-bar] {
display: flex;
gap: 0.125rem;
height: 0.5rem;
min-width: 0;
}
[data-epoch-segment] {
width: var(--share);
border-radius: 0.125rem;
&[data-epoch-segment="done"] {
background: var(--epoch-color);
}
&[data-epoch-segment="remaining"] {
background: color-mix(in oklch, var(--gray) 35%, transparent);
}
}
}
&[data-group="difficulty"] {
--section-color: var(--orange);
}
&[data-group="block"] {
@@ -169,59 +266,9 @@
&[data-group="rewards"] {
[data-stat-box] {
display: grid;
gap: 0.75rem;
min-width: 0;
border: 1px solid
color-mix(in oklch, var(--section-color) 28%, transparent);
border-radius: 0.25rem;
padding: 0.75rem;
h3 {
color: var(--section-color);
font-family: var(--font-mono);
font-size: var(--font-size-xs);
font-weight: 450;
line-height: var(--line-height-xs);
text-transform: uppercase;
}
[data-stat-box] {
border-color: color-mix(
in oklch,
var(--section-color) 18%,
transparent
);
}
}
[data-stats] {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.5rem;
min-width: 0;
}
[data-stat] {
display: grid;
gap: 0.25rem;
min-width: 0;
> span {
color: var(--section-color);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
text-transform: uppercase;
}
strong {
min-width: 0;
overflow-wrap: anywhere;
color: var(--white);
font-size: var(--font-size-sm);
font-weight: 450;
line-height: var(--line-height-sm);
}
}
}
&[data-group="block"] {
@@ -230,16 +277,20 @@
gap: 0.5rem;
min-width: 0;
border: 1px solid
color-mix(in oklch, var(--section-color) 28%, transparent);
color-mix(in oklch, var(--section-color) 35%, transparent);
border-radius: 0.25rem;
padding: 0.75rem;
[data-block-box] {
border-color: color-mix(
in oklch,
var(--section-color) 18%,
transparent
);
&[data-block-box="tx"] {
border-color: color-mix(in oklch, var(--orange) 35%, transparent);
}
&[data-block-box="input"] {
border-color: color-mix(in oklch, var(--yellow) 55%, transparent);
}
&[data-block-box="output"] {
border-color: color-mix(in oklch, var(--red) 55%, transparent);
}
}
@@ -263,7 +314,7 @@
align-items: center;
justify-content: flex-end;
min-width: 0;
color: var(--white);
color: var(--section-color);
font-size: var(--font-size-sm);
font-weight: 450;
line-height: var(--line-height-sm);
@@ -274,38 +325,55 @@
[data-block-io] {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 0.5rem;
gap: 0.75rem;
min-width: 0;
[data-block-box] {
aspect-ratio: 1 / 1;
place-content: center;
align-content: center;
padding: 0.5rem;
}
[data-inline-row] {
grid-template-columns: minmax(0, 1fr);
justify-items: center;
gap: 0.25rem;
strong {
justify-content: center;
text-align: center;
}
grid-template-columns: auto auto;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
}
}
[data-block-box="tx"] > [data-inline-row] {
> span,
strong {
color: var(--orange);
}
}
:is([data-block-box="input"], [data-block-box="output"]) > [data-inline-row] {
> span,
strong {
color: var(--block-box-color);
}
}
[data-block-box="input"] {
--block-box-color: var(--yellow);
}
[data-block-box="output"] {
--block-box-color: var(--red);
}
}
&[data-group="rewards"] {
[data-reward-total] {
display: grid;
gap: 0.25rem;
justify-items: center;
gap: 0.125rem;
justify-items: start;
min-width: 0;
color: var(--gray);
font-size: var(--font-size-xs);
line-height: var(--line-height-xs);
text-align: center;
text-align: left;
strong {
min-width: 0;
@@ -315,6 +383,11 @@
font-weight: 450;
line-height: var(--line-height-sm);
}
> span:first-child {
color: var(--section-color);
text-transform: uppercase;
}
}
[data-reward-bar] {
@@ -416,12 +489,6 @@
grid-column: auto;
}
section[data-group="rewards"] {
[data-stats] {
grid-template-columns: minmax(0, 1fr);
}
}
dl > div {
grid-template-columns: minmax(0, 1fr);
gap: 0.15rem;