mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-10 10:38:14 -07:00
website_next: part 5
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
export const DIFFICULTY_EPOCH_BLOCKS = 2_016;
|
||||
export const HALVING_EPOCH_BLOCKS = 210_000;
|
||||
|
||||
const MAX_BLOCK_WEIGHT = 4_000_000;
|
||||
export const MAX_BLOCK_WEIGHT = 4_000_000;
|
||||
|
||||
/** @param {number} value */
|
||||
export function formatNumber(value) {
|
||||
|
||||
@@ -4,19 +4,33 @@ import { createDifficultyPane } from "./difficulty.js";
|
||||
import { createRewardsPane } from "./rewards.js";
|
||||
import { createTransactionPane } from "./transactions.js";
|
||||
import { createFeeChart } from "./fee-chart.js";
|
||||
import { createBlockPreviewPane } from "./preview/index.js";
|
||||
import { appendPane } from "./pane.js";
|
||||
import { createBlockReceipt } from "./receipt.js";
|
||||
|
||||
function noop() {}
|
||||
|
||||
export function createBlockDetails() {
|
||||
const element = document.createElement("section");
|
||||
const receipt = createBlockReceipt();
|
||||
const header = createBlockHeader([receipt.button]);
|
||||
const content = document.createElement("div");
|
||||
let destroyPreview = noop;
|
||||
|
||||
element.id = "block-details";
|
||||
element.hidden = true;
|
||||
element.append(header.element, content);
|
||||
|
||||
function clearContent() {
|
||||
destroyPreview();
|
||||
destroyPreview = noop;
|
||||
|
||||
for (const chart of content.querySelectorAll("[data-fee-chart]")) {
|
||||
chart.dispatchEvent(new Event("chart:destroy"));
|
||||
}
|
||||
content.textContent = "";
|
||||
}
|
||||
|
||||
/** @param {import("../../modules/brk-client/index.js").BlockInfoV1} block */
|
||||
function update(block) {
|
||||
const extras = block.extras;
|
||||
@@ -25,15 +39,16 @@ export function createBlockDetails() {
|
||||
header.update(block);
|
||||
receipt.update(block);
|
||||
|
||||
for (const chart of content.querySelectorAll("[data-fee-chart]")) {
|
||||
chart.dispatchEvent(new Event("chart:destroy"));
|
||||
}
|
||||
content.textContent = "";
|
||||
clearContent();
|
||||
|
||||
const preview = createBlockPreviewPane(block);
|
||||
|
||||
destroyPreview = preview.destroy;
|
||||
appendPane(content, "mining", [createMinerPane(block)]);
|
||||
appendPane(content, "difficulty", [createDifficultyPane(block)]);
|
||||
appendPane(content, "rewards", [createRewardsPane(extras)]);
|
||||
appendPane(content, "block", [createTransactionPane(block)]);
|
||||
appendPane(content, "preview", [preview.element]);
|
||||
appendPane(content, "fees", [
|
||||
createFeeChart(extras.feeRange, extras.avgFeeRate),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { brk } from "../../../utils/client.js";
|
||||
|
||||
/**
|
||||
* @param {import("../../../modules/brk-client/index.js").BlockInfoV1} block
|
||||
*/
|
||||
export async function loadBlockPreview(block) {
|
||||
const firstTxIndex = (
|
||||
await brk.series.transactions.raw.firstTxIndex.by.height
|
||||
.get(block.height)
|
||||
.fetch()
|
||||
).data[0];
|
||||
const start = /** @type {number} */ (firstTxIndex);
|
||||
const end = start + block.txCount;
|
||||
const tx = brk.series.transactions;
|
||||
const [
|
||||
txids,
|
||||
versions,
|
||||
weights,
|
||||
feeRates,
|
||||
] = await Promise.all([
|
||||
tx.raw.txid.by.tx_index.slice(start, end).fetch(),
|
||||
tx.raw.txVersion.by.tx_index.slice(start, end).fetch(),
|
||||
tx.size.weight.txIndex.by.tx_index.slice(start, end).fetch(),
|
||||
tx.fees.feeRate.by.tx_index.slice(start, end).fetch(),
|
||||
]);
|
||||
|
||||
return txids.data.map((txid, index) => ({
|
||||
txid,
|
||||
version: versions.data[index],
|
||||
weight: weights.data[index],
|
||||
feeRate: feeRates.data[index],
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
export const FEE_BUCKETS = /** @type {const} */ ([
|
||||
{ label: "min", color: "var(--cyan)" },
|
||||
{ label: "10%", color: "var(--blue)" },
|
||||
{ label: "25%", color: "var(--violet)" },
|
||||
{ label: "50%", color: "var(--white)" },
|
||||
{ label: "75%", color: "var(--yellow)" },
|
||||
{ label: "90%", color: "var(--orange)" },
|
||||
{ label: "max", color: "var(--red)" },
|
||||
]);
|
||||
|
||||
/**
|
||||
* @param {number} feeRate
|
||||
* @param {number[]} ranges
|
||||
*/
|
||||
export function getFeeBucket(feeRate, ranges) {
|
||||
for (let index = 0; index < ranges.length; index += 1) {
|
||||
if (feeRate <= ranges[index]) return FEE_BUCKETS[index];
|
||||
}
|
||||
|
||||
return FEE_BUCKETS[FEE_BUCKETS.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
* @param {number[]} ranges
|
||||
*/
|
||||
export function countFees(transactions, ranges) {
|
||||
const counts = new Map();
|
||||
|
||||
for (const transaction of transactions) {
|
||||
const bucket = getFeeBucket(transaction.feeRate, ranges);
|
||||
|
||||
counts.set(bucket.label, (counts.get(bucket.label) ?? 0) + 1);
|
||||
}
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
*/
|
||||
export function orderTransactions(transactions) {
|
||||
return transactions
|
||||
.toSorted((a, b) => b.feeRate - a.feeRate || b.weight - a.weight);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreviewTransaction
|
||||
* @property {string} txid
|
||||
* @property {number} version
|
||||
* @property {number} weight
|
||||
* @property {number} feeRate
|
||||
*/
|
||||
@@ -0,0 +1,102 @@
|
||||
import { createHeatmap } from "../../../heatmap/index.js";
|
||||
import { formatFeeRate } from "../../../utils/fee-rate.js";
|
||||
import { formatWeight, MAX_BLOCK_WEIGHT } from "../format.js";
|
||||
import { loadBlockPreview } from "./data.js";
|
||||
import { getFeeBucket, orderTransactions } from "./fees.js";
|
||||
import { createPreviewLegend } from "./legend.js";
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction} transaction
|
||||
* @param {number[]} ranges
|
||||
*/
|
||||
function createPreviewItem(transaction, ranges) {
|
||||
const bucket = getFeeBucket(transaction.feeRate, ranges);
|
||||
|
||||
return {
|
||||
color: bucket.color,
|
||||
group: bucket.label,
|
||||
weight: transaction.weight,
|
||||
title: [
|
||||
transaction.txid,
|
||||
`v${transaction.version}`,
|
||||
`${formatFeeRate(transaction.feeRate)} sat/vB`,
|
||||
formatWeight(transaction.weight),
|
||||
].join(" · "),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} content
|
||||
* @param {number[]} ranges
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
*/
|
||||
function renderPreview(content, ranges, transactions) {
|
||||
const figure = document.createElement("figure");
|
||||
const caption = document.createElement("figcaption");
|
||||
const title = document.createElement("h5");
|
||||
const ordered = orderTransactions(transactions);
|
||||
const items = ordered.map((transaction) => {
|
||||
return createPreviewItem(transaction, ranges);
|
||||
});
|
||||
|
||||
figure.dataset.blockPreviewFigure = "";
|
||||
caption.dataset.blockPreviewLegend = "";
|
||||
title.append("Fees");
|
||||
caption.append(title, createPreviewLegend(ordered, ranges));
|
||||
figure.append(
|
||||
caption,
|
||||
createHeatmap(items, {
|
||||
origin: "bottom",
|
||||
shape: "square",
|
||||
capacity: MAX_BLOCK_WEIGHT,
|
||||
columns: 100,
|
||||
}),
|
||||
);
|
||||
content.replaceChildren(figure);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} content
|
||||
* @param {string} status
|
||||
*/
|
||||
function renderStatus(content, status) {
|
||||
const p = document.createElement("p");
|
||||
|
||||
p.dataset.blockPreviewStatus = status;
|
||||
p.textContent = status;
|
||||
content.replaceChildren(p);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../../modules/brk-client/index.js").BlockInfoV1} block
|
||||
*/
|
||||
export function createBlockPreviewPane(block) {
|
||||
const content = document.createElement("div");
|
||||
let live = true;
|
||||
|
||||
content.dataset.blockPreview = "";
|
||||
renderStatus(content, "Loading");
|
||||
|
||||
void loadBlockPreview(block)
|
||||
.then((transactions) => {
|
||||
if (!live) return;
|
||||
renderPreview(content, block.extras.feeRange, transactions);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!live) return;
|
||||
console.error(error);
|
||||
renderStatus(content, "Unavailable");
|
||||
});
|
||||
|
||||
return {
|
||||
element: content,
|
||||
destroy() {
|
||||
live = false;
|
||||
for (const heatmap of content.querySelectorAll("[data-heatmap]")) {
|
||||
heatmap.dispatchEvent(new Event("heatmap:destroy"));
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef {import("./fees.js").BlockPreviewTransaction} BlockPreviewTransaction */
|
||||
@@ -0,0 +1,36 @@
|
||||
import {
|
||||
appendLegendListItem,
|
||||
createLegendItem,
|
||||
createLegendList,
|
||||
} from "../../../legend/index.js";
|
||||
import { formatFeeRate } from "../../../utils/fee-rate.js";
|
||||
import { formatNumber } from "../format.js";
|
||||
import { countFees, FEE_BUCKETS } from "./fees.js";
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction[]} transactions
|
||||
* @param {number[]} ranges
|
||||
*/
|
||||
export function createPreviewLegend(transactions, ranges) {
|
||||
const counts = countFees(transactions, ranges);
|
||||
const list = createLegendList({ scroll: true });
|
||||
|
||||
for (let index = 0; index < FEE_BUCKETS.length; index += 1) {
|
||||
const bucket = FEE_BUCKETS[index];
|
||||
const count = counts.get(bucket.label) ?? 0;
|
||||
const { button, value } = createLegendItem({
|
||||
label: bucket.label,
|
||||
color: bucket.color,
|
||||
ariaLabel: `${bucket.label} fee rate bucket`,
|
||||
detail: `${formatFeeRate(ranges[index])} sat/vB`,
|
||||
});
|
||||
|
||||
if (count === 0) button.dataset.muted = "";
|
||||
value.textContent = formatNumber(count);
|
||||
appendLegendListItem(list, button);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/** @typedef {import("./fees.js").BlockPreviewTransaction} BlockPreviewTransaction */
|
||||
@@ -0,0 +1,30 @@
|
||||
[data-block-preview] {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-block-preview-figure] {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
[data-block-preview-legend] {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
|
||||
> h5 {
|
||||
color: var(--white);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-regular);
|
||||
line-height: var(--line-height-sm);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
|
||||
[data-block-preview-status] {
|
||||
color: var(--gray);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export function createReceiptQr(block, url) {
|
||||
section.dataset.receiptQr = "";
|
||||
label.textContent = "Scan to verify";
|
||||
image.alt = `QR code for block ${block.height}`;
|
||||
image.src = createQrDataUrl(url, { padX: 0, padY: 0, scale: 8 });
|
||||
image.src = createQrDataUrl(url, { scale: 8 });
|
||||
link.href = url;
|
||||
link.textContent = formatDisplayUrl(url);
|
||||
section.append(label, image, link);
|
||||
|
||||
Reference in New Issue
Block a user