mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-30 12:18:11 -07:00
website_next: part 2
This commit is contained in:
@@ -128,10 +128,11 @@ export function getBtcParts(sats, options = {}) {
|
|||||||
* @param {BtcAmountOptions} [options]
|
* @param {BtcAmountOptions} [options]
|
||||||
*/
|
*/
|
||||||
export function renderBtcAmount(element, sats, options = {}) {
|
export function renderBtcAmount(element, sats, options = {}) {
|
||||||
|
element.dataset.btcAmount = "";
|
||||||
element.replaceChildren(...getBtcParts(sats, options).map((part) => {
|
element.replaceChildren(...getBtcParts(sats, options).map((part) => {
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
|
|
||||||
if (part.muted) span.classList.add("muted");
|
if (part.muted) span.dataset.btcMuted = "";
|
||||||
span.append(part.text);
|
span.append(part.text);
|
||||||
|
|
||||||
return span;
|
return span;
|
||||||
@@ -147,7 +148,6 @@ export function renderBtcAmount(element, sats, options = {}) {
|
|||||||
export function createBtcAmount(tag, sats, options = {}) {
|
export function createBtcAmount(tag, sats, options = {}) {
|
||||||
const element = document.createElement(tag);
|
const element = document.createElement(tag);
|
||||||
|
|
||||||
element.classList.add("amount");
|
|
||||||
renderBtcAmount(element, sats, options);
|
renderBtcAmount(element, sats, options);
|
||||||
|
|
||||||
return element;
|
return element;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.amount {
|
[data-btc-amount] {
|
||||||
.muted {
|
[data-btc-muted] {
|
||||||
color: color-mix(in oklch, currentColor 45%, transparent);
|
color: color-mix(in oklch, currentColor 45%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/** @typedef {import("../../modules/brk-client/index.js").BlockInfoV1} Block */
|
||||||
|
|
||||||
|
const DIFFICULTY_EPOCH_BLOCKS = 2_016;
|
||||||
|
const HALVING_EPOCH_BLOCKS = 210_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} label
|
||||||
|
* @param {string} value
|
||||||
|
*/
|
||||||
|
function createMetricStat(label, value) {
|
||||||
|
const stat = document.createElement("div");
|
||||||
|
const name = document.createElement("span");
|
||||||
|
const amount = document.createElement("strong");
|
||||||
|
|
||||||
|
stat.dataset.metricStat = "";
|
||||||
|
name.textContent = label;
|
||||||
|
amount.textContent = value;
|
||||||
|
stat.append(name, amount);
|
||||||
|
|
||||||
|
return stat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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 */
|
||||||
|
export function createDifficultyPane(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)",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { createXyChart } from "../../chart/xy/index.js";
|
|||||||
import { createChartPoint, createChartPoints } from "../../chart/points.js";
|
import { createChartPoint, createChartPoints } from "../../chart/points.js";
|
||||||
import { createLinearXScale } from "../../chart/x.js";
|
import { createLinearXScale } from "../../chart/x.js";
|
||||||
import { createValueYScale } from "../../chart/y.js";
|
import { createValueYScale } from "../../chart/y.js";
|
||||||
|
import { formatFeeRate } from "../../utils/fee-rate.js";
|
||||||
|
|
||||||
export const FEE_PERCENTILE_LABELS = /** @type {const} */ ([
|
export const FEE_PERCENTILE_LABELS = /** @type {const} */ ([
|
||||||
"min",
|
"min",
|
||||||
@@ -151,9 +152,8 @@ function plotSeries(percentileSamples, entries, frame) {
|
|||||||
/**
|
/**
|
||||||
* @param {number[]} values
|
* @param {number[]} values
|
||||||
* @param {number} averageRate
|
* @param {number} averageRate
|
||||||
* @param {(value: number) => string} formatRate
|
|
||||||
*/
|
*/
|
||||||
export function createFeeChart(values, averageRate, formatRate) {
|
export function createFeeChart(values, averageRate) {
|
||||||
const percentileSamples = createPercentileSamples(values);
|
const percentileSamples = createPercentileSamples(values);
|
||||||
const entries = createEntries(percentileSamples, averageRate);
|
const entries = createEntries(percentileSamples, averageRate);
|
||||||
const figure = createXyChart({
|
const figure = createXyChart({
|
||||||
@@ -161,11 +161,11 @@ export function createFeeChart(values, averageRate, formatRate) {
|
|||||||
unit: {
|
unit: {
|
||||||
id: "sat/vB",
|
id: "sat/vB",
|
||||||
name: "satoshis per virtual byte",
|
name: "satoshis per virtual byte",
|
||||||
format: formatRate,
|
format: formatFeeRate,
|
||||||
},
|
},
|
||||||
ariaLabel: `Fee rate percentiles from ${formatRate(
|
ariaLabel: `Fee rate percentiles from ${formatFeeRate(
|
||||||
values[0],
|
values[0],
|
||||||
)} to ${formatRate(values[values.length - 1])} sat/vB`,
|
)} to ${formatFeeRate(values[values.length - 1])} sat/vB`,
|
||||||
fallbackHeight: VIEWBOX_HEIGHT,
|
fallbackHeight: VIEWBOX_HEIGHT,
|
||||||
series: createSeries(entries),
|
series: createSeries(entries),
|
||||||
plot: (frame) => plotSeries(percentileSamples, entries, frame),
|
plot: (frame) => plotSeries(percentileSamples, entries, frame),
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { createUsdAmount, renderUsdAmount } from "../../usd/index.js";
|
||||||
|
|
||||||
|
/** @typedef {import("../../modules/brk-client/index.js").BlockInfoV1} Block */
|
||||||
|
|
||||||
|
/** @param {number} unixSeconds */
|
||||||
|
function formatDateTime(unixSeconds) {
|
||||||
|
return new Date(unixSeconds * 1_000).toLocaleString(undefined, {
|
||||||
|
dateStyle: "medium",
|
||||||
|
timeStyle: "medium",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} height */
|
||||||
|
function createHeightElement(height) {
|
||||||
|
const element = document.createElement("span");
|
||||||
|
const prefix = document.createElement("span");
|
||||||
|
const value = document.createElement("span");
|
||||||
|
|
||||||
|
prefix.dataset.dim = "";
|
||||||
|
prefix.textContent = `#${"0".repeat(Math.max(0, 7 - String(height).length))}`;
|
||||||
|
value.textContent = String(height);
|
||||||
|
element.append(prefix, value);
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} hash */
|
||||||
|
function createHashElement(hash) {
|
||||||
|
const element = document.createElement("span");
|
||||||
|
const prefix = document.createElement("span");
|
||||||
|
const value = document.createElement("span");
|
||||||
|
const firstNonZero = hash.search(/[^0]/);
|
||||||
|
const visibleStart = firstNonZero === -1 ? hash.length : firstNonZero;
|
||||||
|
|
||||||
|
element.dataset.blockHash = "";
|
||||||
|
prefix.dataset.dim = "";
|
||||||
|
prefix.textContent = hash.slice(0, visibleStart);
|
||||||
|
value.textContent = hash.slice(visibleStart);
|
||||||
|
element.append(prefix, value);
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} height */
|
||||||
|
function createTitle(height) {
|
||||||
|
const label = document.createElement("span");
|
||||||
|
const value = document.createElement("span");
|
||||||
|
|
||||||
|
label.dataset.titleLabel = "";
|
||||||
|
value.dataset.titleHeight = "";
|
||||||
|
label.textContent = "Block";
|
||||||
|
value.append(createHeightElement(height));
|
||||||
|
|
||||||
|
return [label, value];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createBlockHeader() {
|
||||||
|
const element = document.createElement("header");
|
||||||
|
const titleRow = document.createElement("div");
|
||||||
|
const title = document.createElement("h1");
|
||||||
|
const date = document.createElement("time");
|
||||||
|
const meta = document.createElement("div");
|
||||||
|
const hash = document.createElement("p");
|
||||||
|
const price = createUsdAmount("output", 0, {
|
||||||
|
tone: "positive",
|
||||||
|
});
|
||||||
|
|
||||||
|
titleRow.dataset.blockTitle = "";
|
||||||
|
date.dataset.blockDate = "";
|
||||||
|
meta.dataset.blockMeta = "";
|
||||||
|
hash.dataset.blockHashLine = "";
|
||||||
|
titleRow.append(title, date);
|
||||||
|
meta.append(hash, price);
|
||||||
|
element.append(titleRow, meta);
|
||||||
|
|
||||||
|
/** @param {Block} block */
|
||||||
|
function update(block) {
|
||||||
|
title.replaceChildren(...createTitle(block.height));
|
||||||
|
date.dateTime = new Date(block.timestamp * 1_000).toISOString();
|
||||||
|
date.textContent = formatDateTime(block.timestamp);
|
||||||
|
hash.replaceChildren(createHashElement(block.id));
|
||||||
|
renderUsdAmount(price, block.extras.price, {
|
||||||
|
tone: "positive",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return /** @type {const} */ ({
|
||||||
|
element,
|
||||||
|
update,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,551 +1,39 @@
|
|||||||
import { createBtcAmount, SATS_PER_BTC } from "../../btc/index.js";
|
import { createBlockHeader } from "./header.js";
|
||||||
import {
|
import { createMinerPane } from "./miner.js";
|
||||||
appendLegendListItem,
|
import { createDifficultyPane } from "./difficulty.js";
|
||||||
createLegendItem,
|
import { createRewardsPane } from "./rewards.js";
|
||||||
createLegendList,
|
import { createTransactionPane } from "./transactions.js";
|
||||||
} from "../../legend/index.js";
|
|
||||||
import { createPoolLogo } from "../../pools/index.js";
|
|
||||||
import { createUsdAmount, renderUsdAmount } from "../../usd/index.js";
|
|
||||||
import { brk } from "../../utils/client.js";
|
|
||||||
import { createFeeChart } from "./fee-chart.js";
|
import { createFeeChart } from "./fee-chart.js";
|
||||||
|
import { appendPane } from "./pane.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) {
|
|
||||||
return bytes >= 1_000_000
|
|
||||||
? `${(bytes / 1_000_000).toFixed(2)} MB`
|
|
||||||
: `${bytes.toLocaleString()} B`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} rate */
|
|
||||||
function formatFeeRate(rate) {
|
|
||||||
if (rate >= 1_000_000) return `${(rate / 1_000_000).toFixed(1)}M`;
|
|
||||||
if (rate >= 100_000) return `${Math.round(rate / 1_000)}k`;
|
|
||||||
if (rate >= 1_000) return `${(rate / 1_000).toFixed(1)}k`;
|
|
||||||
if (rate >= 100) return Math.round(rate).toLocaleString();
|
|
||||||
if (rate >= 10) return rate.toFixed(1);
|
|
||||||
return rate.toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} unixSeconds */
|
|
||||||
function formatDateTime(unixSeconds) {
|
|
||||||
return new Date(unixSeconds * 1_000).toLocaleString(undefined, {
|
|
||||||
dateStyle: "medium",
|
|
||||||
timeStyle: "medium",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} height */
|
|
||||||
function createHeightElement(height) {
|
|
||||||
const element = document.createElement("span");
|
|
||||||
const prefix = document.createElement("span");
|
|
||||||
const value = document.createElement("span");
|
|
||||||
|
|
||||||
prefix.classList.add("dim");
|
|
||||||
prefix.textContent = `#${"0".repeat(Math.max(0, 7 - String(height).length))}`;
|
|
||||||
value.textContent = String(height);
|
|
||||||
element.append(prefix, value);
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} hash */
|
|
||||||
function createHashElement(hash) {
|
|
||||||
const element = document.createElement("span");
|
|
||||||
const prefix = document.createElement("span");
|
|
||||||
const value = document.createElement("span");
|
|
||||||
const firstNonZero = hash.search(/[^0]/);
|
|
||||||
const visibleStart = firstNonZero === -1 ? hash.length : firstNonZero;
|
|
||||||
|
|
||||||
element.dataset.blockHash = "";
|
|
||||||
prefix.classList.add("dim");
|
|
||||||
prefix.textContent = hash.slice(0, visibleStart);
|
|
||||||
value.textContent = hash.slice(visibleStart);
|
|
||||||
element.append(prefix, value);
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} height */
|
|
||||||
function createTitle(height) {
|
|
||||||
const label = document.createElement("span");
|
|
||||||
const value = document.createElement("span");
|
|
||||||
|
|
||||||
label.classList.add("title-label");
|
|
||||||
value.classList.add("title-height");
|
|
||||||
label.textContent = "Block";
|
|
||||||
value.append(createHeightElement(height));
|
|
||||||
|
|
||||||
return [label, value];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} term
|
|
||||||
* @param {string | Node | null | undefined} value
|
|
||||||
*/
|
|
||||||
function createRow(term, value) {
|
|
||||||
if (value == null || value === "") return null;
|
|
||||||
|
|
||||||
const row = document.createElement("div");
|
|
||||||
const dt = document.createElement("dt");
|
|
||||||
const dd = document.createElement("dd");
|
|
||||||
|
|
||||||
dt.textContent = term;
|
|
||||||
dd.append(value);
|
|
||||||
row.append(dt, dd);
|
|
||||||
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} title */
|
|
||||||
function groupName(title) {
|
|
||||||
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} label
|
|
||||||
* @param {(string | Node)[]} values
|
|
||||||
*/
|
|
||||||
function createInlineRow(label, values) {
|
|
||||||
const row = document.createElement("div");
|
|
||||||
const name = document.createElement("span");
|
|
||||||
const data = document.createElement("strong");
|
|
||||||
|
|
||||||
row.dataset.inlineRow = "";
|
|
||||||
name.textContent = label;
|
|
||||||
data.append(...values);
|
|
||||||
row.append(name, data);
|
|
||||||
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} label
|
|
||||||
* @param {string | Node} value
|
|
||||||
* @param {string} [type]
|
|
||||||
*/
|
|
||||||
function createInlineBox(label, value, type = "inline") {
|
|
||||||
const box = document.createElement("div");
|
|
||||||
|
|
||||||
box.dataset.blockBox = type;
|
|
||||||
box.append(createInlineRow(label, [value]));
|
|
||||||
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {Block} block */
|
|
||||||
function formatBlockFill(block) {
|
|
||||||
return `${((block.weight / MAX_BLOCK_WEIGHT) * 100).toFixed(1)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} label
|
|
||||||
* @param {string} value
|
|
||||||
*/
|
|
||||||
function createMetricStat(label, value) {
|
|
||||||
const stat = document.createElement("div");
|
|
||||||
const name = document.createElement("span");
|
|
||||||
const amount = document.createElement("strong");
|
|
||||||
|
|
||||||
stat.dataset.metricStat = "";
|
|
||||||
name.textContent = label;
|
|
||||||
amount.textContent = value;
|
|
||||||
stat.append(name, amount);
|
|
||||||
|
|
||||||
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 logo = createPoolLogo(pool);
|
|
||||||
const coinbaseMessage = createCoinbaseMessage(block.extras.coinbaseSignatureAscii);
|
|
||||||
|
|
||||||
pane.dataset.minerPane = "";
|
|
||||||
head.dataset.minerHead = "";
|
|
||||||
identity.dataset.minerIdentity = "";
|
|
||||||
title.dataset.minerTitle = "";
|
|
||||||
slug.dataset.minerSlug = "";
|
|
||||||
logo.dataset.minerLogo = "";
|
|
||||||
|
|
||||||
name.textContent = pool.name;
|
|
||||||
// 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);
|
|
||||||
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)",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return pane;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} sats
|
|
||||||
* @param {number} total
|
|
||||||
*/
|
|
||||||
function formatShare(sats, total) {
|
|
||||||
return `${((sats / total) * 100).toFixed(2)}%`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} sats
|
|
||||||
* @param {number} price
|
|
||||||
*/
|
|
||||||
function getSatsUsd(sats, price) {
|
|
||||||
return (sats / SATS_PER_BTC) * price;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} sats
|
|
||||||
* @param {number} price
|
|
||||||
*/
|
|
||||||
function createSatsUsdAmount(sats, price) {
|
|
||||||
return createUsdAmount("span", getSatsUsd(sats, price));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number} sats
|
|
||||||
* @param {number} total
|
|
||||||
* @param {number} price
|
|
||||||
*/
|
|
||||||
function createRewardDetail(sats, total, price) {
|
|
||||||
const detail = document.createDocumentFragment();
|
|
||||||
|
|
||||||
detail.append(createSatsUsdAmount(sats, price), " · ", formatShare(sats, total));
|
|
||||||
|
|
||||||
return detail;
|
|
||||||
}
|
|
||||||
|
|
||||||
const REWARD_COLORS = /** @type {const} */ ({
|
|
||||||
subsidy: "var(--orange)",
|
|
||||||
fees: "var(--green)",
|
|
||||||
});
|
|
||||||
|
|
||||||
/** @typedef {keyof typeof REWARD_COLORS} RewardType */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {RewardType} type
|
|
||||||
* @param {number} sats
|
|
||||||
* @param {number} total
|
|
||||||
*/
|
|
||||||
function createRewardSegment(type, sats, total) {
|
|
||||||
const segment = document.createElement("span");
|
|
||||||
|
|
||||||
segment.dataset.rewardSegment = type;
|
|
||||||
segment.dataset.rewardKey = type;
|
|
||||||
segment.style.setProperty("--share", `${(sats / total) * 100}%`);
|
|
||||||
|
|
||||||
return segment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {RewardType} type
|
|
||||||
* @param {string} label
|
|
||||||
* @param {number} sats
|
|
||||||
* @param {number} total
|
|
||||||
* @param {number} price
|
|
||||||
*/
|
|
||||||
function createRewardPart(type, label, sats, total, price) {
|
|
||||||
const { button: part, value } = createLegendItem({
|
|
||||||
label,
|
|
||||||
color: REWARD_COLORS[type],
|
|
||||||
ariaLabel: `Highlight ${label}`,
|
|
||||||
detail: createRewardDetail(sats, total, price),
|
|
||||||
});
|
|
||||||
const amount = createBtcAmount("span", sats);
|
|
||||||
|
|
||||||
part.dataset.rewardPart = type;
|
|
||||||
part.dataset.rewardKey = type;
|
|
||||||
value.replaceChildren(amount);
|
|
||||||
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} label
|
|
||||||
* @param {number} sats
|
|
||||||
* @param {number} 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 = "";
|
|
||||||
name.textContent = label;
|
|
||||||
total.append(name, amount, usd);
|
|
||||||
|
|
||||||
return total;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {EventTarget | null} target */
|
|
||||||
function getRewardKey(target) {
|
|
||||||
if (!(target instanceof HTMLElement)) return null;
|
|
||||||
|
|
||||||
return target.closest("[data-reward-key]")?.getAttribute("data-reward-key") ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} rewards
|
|
||||||
* @param {string | null} activeKey
|
|
||||||
*/
|
|
||||||
function setRewardPreview(rewards, activeKey) {
|
|
||||||
for (const element of rewards.querySelectorAll("[data-reward-key]")) {
|
|
||||||
if (!(element instanceof HTMLElement)) continue;
|
|
||||||
|
|
||||||
if (element.dataset.rewardKey === activeKey) {
|
|
||||||
element.dataset.preview = "";
|
|
||||||
delete element.dataset.muted;
|
|
||||||
} else if (activeKey) {
|
|
||||||
element.dataset.muted = "";
|
|
||||||
delete element.dataset.preview;
|
|
||||||
} else {
|
|
||||||
delete element.dataset.muted;
|
|
||||||
delete element.dataset.preview;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @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 });
|
|
||||||
|
|
||||||
rewards.dataset.statBox = "rewards";
|
|
||||||
appendLegendListItem(
|
|
||||||
split,
|
|
||||||
createRewardPart("subsidy", "Subsidy", subsidy, extras.reward, extras.price),
|
|
||||||
);
|
|
||||||
appendLegendListItem(
|
|
||||||
split,
|
|
||||||
createRewardPart("fees", "Fees", extras.totalFees, extras.reward, extras.price),
|
|
||||||
);
|
|
||||||
bar.dataset.rewardBar = "";
|
|
||||||
bar.append(
|
|
||||||
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));
|
|
||||||
}, true);
|
|
||||||
rewards.addEventListener("pointerleave", () => setRewardPreview(rewards, null));
|
|
||||||
rewards.addEventListener("pointerdown", (event) => {
|
|
||||||
setRewardPreview(rewards, getRewardKey(event.target));
|
|
||||||
});
|
|
||||||
rewards.addEventListener("pointerup", () => setRewardPreview(rewards, null));
|
|
||||||
rewards.addEventListener("pointercancel", () => setRewardPreview(rewards, null));
|
|
||||||
|
|
||||||
return rewards;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {Block} block */
|
|
||||||
function createTransactionSummary(block) {
|
|
||||||
const { extras } = block;
|
|
||||||
const box = document.createElement("div");
|
|
||||||
const transactions = document.createElement("div");
|
|
||||||
const io = document.createElement("div");
|
|
||||||
|
|
||||||
box.dataset.blockBox = "";
|
|
||||||
transactions.dataset.blockBox = "tx";
|
|
||||||
io.dataset.blockIo = "";
|
|
||||||
io.append(
|
|
||||||
createInlineBox("Input", extras.totalInputs.toLocaleString(), "input"),
|
|
||||||
createInlineBox("Output", extras.totalOutputs.toLocaleString(), "output"),
|
|
||||||
);
|
|
||||||
transactions.append(
|
|
||||||
createInlineRow("Tx", [block.txCount.toLocaleString()]),
|
|
||||||
io,
|
|
||||||
);
|
|
||||||
box.append(
|
|
||||||
createInlineRow("Block", [`${formatBytes(block.size)} · ${formatBlockFill(block)}`]),
|
|
||||||
transactions,
|
|
||||||
);
|
|
||||||
|
|
||||||
return box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {HTMLElement} parent
|
|
||||||
* @param {string} title
|
|
||||||
* @param {[string, string | Node | null | undefined][]} rows
|
|
||||||
* @param {Node[]} [children]
|
|
||||||
* @param {boolean} [showHeading]
|
|
||||||
*/
|
|
||||||
function appendGroup(parent, title, rows, children = [], showHeading = true) {
|
|
||||||
const visibleRows = rows.flatMap(([term, value]) => {
|
|
||||||
const row = createRow(term, value);
|
|
||||||
|
|
||||||
return row ? [row] : [];
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!visibleRows.length && !children.length) return;
|
|
||||||
|
|
||||||
const section = document.createElement("section");
|
|
||||||
const heading = document.createElement("h2");
|
|
||||||
|
|
||||||
section.dataset.group = groupName(title);
|
|
||||||
heading.textContent = title;
|
|
||||||
section.append(...(showHeading ? [heading] : []), ...children);
|
|
||||||
if (visibleRows.length) {
|
|
||||||
const list = document.createElement("dl");
|
|
||||||
|
|
||||||
list.append(...visibleRows);
|
|
||||||
section.append(list);
|
|
||||||
}
|
|
||||||
parent.append(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBlockDetails() {
|
export function createBlockDetails() {
|
||||||
const element = document.createElement("section");
|
const element = document.createElement("section");
|
||||||
const header = document.createElement("header");
|
const header = createBlockHeader();
|
||||||
const titleRow = document.createElement("div");
|
|
||||||
const title = document.createElement("h1");
|
|
||||||
const date = document.createElement("time");
|
|
||||||
const meta = document.createElement("div");
|
|
||||||
const hash = document.createElement("p");
|
|
||||||
const price = createUsdAmount("output", 0, {
|
|
||||||
tone: "positive",
|
|
||||||
});
|
|
||||||
const content = document.createElement("div");
|
const content = document.createElement("div");
|
||||||
|
|
||||||
element.id = "block-details";
|
element.id = "block-details";
|
||||||
element.hidden = true;
|
element.hidden = true;
|
||||||
titleRow.dataset.blockTitle = "";
|
element.append(header.element, content);
|
||||||
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 */
|
/** @param {import("../../modules/brk-client/index.js").BlockInfoV1} block */
|
||||||
function update(block) {
|
function update(block) {
|
||||||
const extras = block.extras;
|
const extras = block.extras;
|
||||||
|
|
||||||
element.hidden = false;
|
element.hidden = false;
|
||||||
title.replaceChildren(...createTitle(block.height));
|
header.update(block);
|
||||||
date.dateTime = new Date(block.timestamp * 1_000).toISOString();
|
|
||||||
date.textContent = formatDateTime(block.timestamp);
|
|
||||||
hash.replaceChildren(createHashElement(block.id));
|
|
||||||
renderUsdAmount(price, extras.price, {
|
|
||||||
tone: "positive",
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const chart of content.querySelectorAll("[data-fee-chart]")) {
|
for (const chart of content.querySelectorAll("[data-fee-chart]")) {
|
||||||
chart.dispatchEvent(new Event("chart:destroy"));
|
chart.dispatchEvent(new Event("chart:destroy"));
|
||||||
}
|
}
|
||||||
content.textContent = "";
|
content.textContent = "";
|
||||||
|
|
||||||
appendGroup(content, "Mining", [], [createMinerSummary(block)], false);
|
appendPane(content, "Mining", [createMinerPane(block)]);
|
||||||
|
appendPane(content, "Difficulty", [createDifficultyPane(block)]);
|
||||||
appendGroup(content, "Difficulty", [], [createDifficultySummary(block)], false);
|
appendPane(content, "Rewards", [createRewardsPane(extras)]);
|
||||||
|
appendPane(content, "Block", [createTransactionPane(block)]);
|
||||||
appendGroup(content, "Rewards", [], [createRewardSummary(extras)], false);
|
appendPane(content, "Fees", [
|
||||||
|
createFeeChart(extras.feeRange, extras.avgFeeRate),
|
||||||
appendGroup(content, "Block", [], [createTransactionSummary(block)], false);
|
]);
|
||||||
|
|
||||||
appendGroup(content, "Fees", [], [
|
|
||||||
createFeeChart(extras.feeRange, extras.avgFeeRate, formatFeeRate),
|
|
||||||
], false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return /** @type {const} */ ({
|
return /** @type {const} */ ({
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { createPoolLogo } from "../../pools/index.js";
|
||||||
|
|
||||||
|
/** @typedef {import("../../modules/brk-client/index.js").BlockInfoV1} Block */
|
||||||
|
|
||||||
|
/** @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 {Block} block */
|
||||||
|
export function createMinerPane(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 logo = createPoolLogo(pool);
|
||||||
|
const coinbaseMessage = createCoinbaseMessage(block.extras.coinbaseSignatureAscii);
|
||||||
|
|
||||||
|
pane.dataset.minerPane = "";
|
||||||
|
head.dataset.minerHead = "";
|
||||||
|
identity.dataset.minerIdentity = "";
|
||||||
|
title.dataset.minerTitle = "";
|
||||||
|
slug.dataset.minerSlug = "";
|
||||||
|
logo.dataset.minerLogo = "";
|
||||||
|
|
||||||
|
name.textContent = pool.name;
|
||||||
|
// 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);
|
||||||
|
pane.append(head, ...(coinbaseMessage ? [coinbaseMessage] : []));
|
||||||
|
|
||||||
|
return pane;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/** @param {string} title */
|
||||||
|
function groupName(title) {
|
||||||
|
return title.toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} parent
|
||||||
|
* @param {string} title
|
||||||
|
* @param {Node[]} children
|
||||||
|
*/
|
||||||
|
export function appendPane(parent, title, children) {
|
||||||
|
if (!children.length) return;
|
||||||
|
|
||||||
|
const section = document.createElement("section");
|
||||||
|
|
||||||
|
section.dataset.group = groupName(title);
|
||||||
|
section.append(...children);
|
||||||
|
parent.append(section);
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { createBtcAmount, SATS_PER_BTC } from "../../btc/index.js";
|
||||||
|
import {
|
||||||
|
appendLegendListItem,
|
||||||
|
createLegendItem,
|
||||||
|
createLegendList,
|
||||||
|
} from "../../legend/index.js";
|
||||||
|
import { createUsdAmount } from "../../usd/index.js";
|
||||||
|
|
||||||
|
/** @typedef {import("../../modules/brk-client/index.js").BlockExtras} BlockExtras */
|
||||||
|
|
||||||
|
const REWARD_COLORS = /** @type {const} */ ({
|
||||||
|
subsidy: "var(--orange)",
|
||||||
|
fees: "var(--green)",
|
||||||
|
});
|
||||||
|
|
||||||
|
/** @typedef {keyof typeof REWARD_COLORS} RewardType */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} sats
|
||||||
|
* @param {number} total
|
||||||
|
*/
|
||||||
|
function formatShare(sats, total) {
|
||||||
|
return `${((sats / total) * 100).toFixed(2)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} sats
|
||||||
|
* @param {number} price
|
||||||
|
*/
|
||||||
|
function getSatsUsd(sats, price) {
|
||||||
|
return (sats / SATS_PER_BTC) * price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} sats
|
||||||
|
* @param {number} price
|
||||||
|
*/
|
||||||
|
function createSatsUsdAmount(sats, price) {
|
||||||
|
return createUsdAmount("span", getSatsUsd(sats, price));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} sats
|
||||||
|
* @param {number} total
|
||||||
|
* @param {number} price
|
||||||
|
*/
|
||||||
|
function createRewardDetail(sats, total, price) {
|
||||||
|
const detail = document.createDocumentFragment();
|
||||||
|
|
||||||
|
detail.append(createSatsUsdAmount(sats, price), " · ", formatShare(sats, total));
|
||||||
|
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RewardType} type
|
||||||
|
* @param {number} sats
|
||||||
|
* @param {number} total
|
||||||
|
*/
|
||||||
|
function createRewardSegment(type, sats, total) {
|
||||||
|
const segment = document.createElement("span");
|
||||||
|
|
||||||
|
segment.dataset.rewardSegment = type;
|
||||||
|
segment.dataset.rewardKey = type;
|
||||||
|
segment.style.setProperty("--share", `${(sats / total) * 100}%`);
|
||||||
|
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {RewardType} type
|
||||||
|
* @param {string} label
|
||||||
|
* @param {number} sats
|
||||||
|
* @param {number} total
|
||||||
|
* @param {number} price
|
||||||
|
*/
|
||||||
|
function createRewardPart(type, label, sats, total, price) {
|
||||||
|
const { button: part, value } = createLegendItem({
|
||||||
|
label,
|
||||||
|
color: REWARD_COLORS[type],
|
||||||
|
ariaLabel: `Highlight ${label}`,
|
||||||
|
detail: createRewardDetail(sats, total, price),
|
||||||
|
});
|
||||||
|
const amount = createBtcAmount("span", sats);
|
||||||
|
|
||||||
|
part.dataset.rewardPart = type;
|
||||||
|
part.dataset.rewardKey = type;
|
||||||
|
value.replaceChildren(amount);
|
||||||
|
|
||||||
|
return part;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} label
|
||||||
|
* @param {number} sats
|
||||||
|
* @param {number} 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 = "";
|
||||||
|
name.textContent = label;
|
||||||
|
total.append(name, amount, usd);
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {EventTarget | null} target */
|
||||||
|
function getRewardKey(target) {
|
||||||
|
if (!(target instanceof HTMLElement)) return null;
|
||||||
|
|
||||||
|
return target.closest("[data-reward-key]")?.getAttribute("data-reward-key") ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} rewards
|
||||||
|
* @param {string | null} activeKey
|
||||||
|
*/
|
||||||
|
function setRewardPreview(rewards, activeKey) {
|
||||||
|
for (const element of rewards.querySelectorAll("[data-reward-key]")) {
|
||||||
|
if (!(element instanceof HTMLElement)) continue;
|
||||||
|
|
||||||
|
if (element.dataset.rewardKey === activeKey) {
|
||||||
|
element.dataset.preview = "";
|
||||||
|
delete element.dataset.muted;
|
||||||
|
} else if (activeKey) {
|
||||||
|
element.dataset.muted = "";
|
||||||
|
delete element.dataset.preview;
|
||||||
|
} else {
|
||||||
|
delete element.dataset.muted;
|
||||||
|
delete element.dataset.preview;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {BlockExtras} extras */
|
||||||
|
export function createRewardsPane(extras) {
|
||||||
|
const subsidy = extras.reward - extras.totalFees;
|
||||||
|
const rewards = document.createElement("div");
|
||||||
|
const bar = document.createElement("div");
|
||||||
|
const split = createLegendList({ fill: true });
|
||||||
|
|
||||||
|
rewards.dataset.rewardsPane = "";
|
||||||
|
appendLegendListItem(
|
||||||
|
split,
|
||||||
|
createRewardPart("subsidy", "Subsidy", subsidy, extras.reward, extras.price),
|
||||||
|
);
|
||||||
|
appendLegendListItem(
|
||||||
|
split,
|
||||||
|
createRewardPart("fees", "Fees", extras.totalFees, extras.reward, extras.price),
|
||||||
|
);
|
||||||
|
bar.dataset.rewardBar = "";
|
||||||
|
bar.append(
|
||||||
|
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));
|
||||||
|
}, true);
|
||||||
|
rewards.addEventListener("pointerleave", () => setRewardPreview(rewards, null));
|
||||||
|
rewards.addEventListener("pointerdown", (event) => {
|
||||||
|
setRewardPreview(rewards, getRewardKey(event.target));
|
||||||
|
});
|
||||||
|
rewards.addEventListener("pointerup", () => setRewardPreview(rewards, null));
|
||||||
|
rewards.addEventListener("pointercancel", () => setRewardPreview(rewards, null));
|
||||||
|
|
||||||
|
return rewards;
|
||||||
|
}
|
||||||
@@ -10,11 +10,11 @@
|
|||||||
line-height: var(--line-height-sm);
|
line-height: var(--line-height-sm);
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
||||||
.dim {
|
[data-dim] {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(h1, h2, p, dl, dd) {
|
:is(h1, p) {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-label {
|
[data-title-label] {
|
||||||
font-family: var(--font-serif);
|
font-family: var(--font-serif);
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
@@ -50,7 +50,7 @@
|
|||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title-height {
|
[data-title-height] {
|
||||||
color: var(--gray);
|
color: var(--gray);
|
||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
line-height: var(--line-height-lg);
|
line-height: var(--line-height-lg);
|
||||||
@@ -86,10 +86,6 @@
|
|||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
&[data-group="overview"] {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-group="mining"] {
|
&[data-group="mining"] {
|
||||||
--section-color: var(--orange);
|
--section-color: var(--orange);
|
||||||
|
|
||||||
@@ -160,10 +156,11 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:is([data-group="mining"], [data-group="difficulty"]) {
|
&[data-group="difficulty"] {
|
||||||
|
--section-color: var(--orange);
|
||||||
|
|
||||||
[data-metric-list] {
|
[data-metric-list] {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -247,31 +244,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-group="difficulty"] {
|
|
||||||
--section-color: var(--orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-group="block"] {
|
&[data-group="block"] {
|
||||||
--section-color: var(--cyan);
|
--section-color: var(--cyan);
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: var(--section-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-group="rewards"] {
|
|
||||||
--section-color: var(--orange);
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-group="rewards"] {
|
|
||||||
[data-stat-box] {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.5rem;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-group="block"] {
|
|
||||||
[data-block-box] {
|
[data-block-box] {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -365,6 +340,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-group="rewards"] {
|
&[data-group="rewards"] {
|
||||||
|
--section-color: var(--orange);
|
||||||
|
|
||||||
|
[data-rewards-pane] {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
[data-reward-total] {
|
[data-reward-total] {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.125rem;
|
gap: 0.125rem;
|
||||||
@@ -417,62 +400,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[data-group="fees"] {
|
&[data-group="fees"] {
|
||||||
h2 {
|
|
||||||
color: var(--green);
|
|
||||||
}
|
|
||||||
|
|
||||||
figure[data-fee-chart] {
|
figure[data-fee-chart] {
|
||||||
--chart-xy-height: 7.5rem;
|
--chart-xy-height: 7.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: var(--gray);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
font-weight: 450;
|
|
||||||
line-height: var(--line-height-xs);
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
display: grid;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(6rem, 0.36fr) minmax(0, 1fr);
|
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.35rem 0;
|
|
||||||
border-bottom: 1px solid
|
|
||||||
color-mix(in oklch, var(--gray) 12%, transparent);
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
padding-bottom: 0;
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dt {
|
|
||||||
color: var(--gray);
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
min-width: 0;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--font-size-xs);
|
|
||||||
line-height: var(--line-height-xs);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 48rem) {
|
@media (max-width: 48rem) {
|
||||||
@@ -484,18 +416,5 @@
|
|||||||
> div {
|
> div {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
|
|
||||||
section[data-group="overview"] {
|
|
||||||
grid-column: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl > div {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
gap: 0.15rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
dd {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
/** @typedef {import("../../modules/brk-client/index.js").BlockInfoV1} Block */
|
||||||
|
|
||||||
|
const MAX_BLOCK_WEIGHT = 4_000_000;
|
||||||
|
|
||||||
|
/** @param {number} bytes */
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
return bytes >= 1_000_000
|
||||||
|
? `${(bytes / 1_000_000).toFixed(2)} MB`
|
||||||
|
: `${bytes.toLocaleString()} B`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} label
|
||||||
|
* @param {(string | Node)[]} values
|
||||||
|
*/
|
||||||
|
function createInlineRow(label, values) {
|
||||||
|
const row = document.createElement("div");
|
||||||
|
const name = document.createElement("span");
|
||||||
|
const data = document.createElement("strong");
|
||||||
|
|
||||||
|
row.dataset.inlineRow = "";
|
||||||
|
name.textContent = label;
|
||||||
|
data.append(...values);
|
||||||
|
row.append(name, data);
|
||||||
|
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} label
|
||||||
|
* @param {string | Node} value
|
||||||
|
* @param {string} [type]
|
||||||
|
*/
|
||||||
|
function createInlineBox(label, value, type = "inline") {
|
||||||
|
const box = document.createElement("div");
|
||||||
|
|
||||||
|
box.dataset.blockBox = type;
|
||||||
|
box.append(createInlineRow(label, [value]));
|
||||||
|
|
||||||
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Block} block */
|
||||||
|
function formatBlockFill(block) {
|
||||||
|
return `${((block.weight / MAX_BLOCK_WEIGHT) * 100).toFixed(1)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {Block} block */
|
||||||
|
export function createTransactionPane(block) {
|
||||||
|
const { extras } = block;
|
||||||
|
const box = document.createElement("div");
|
||||||
|
const transactions = document.createElement("div");
|
||||||
|
const io = document.createElement("div");
|
||||||
|
|
||||||
|
box.dataset.blockBox = "";
|
||||||
|
transactions.dataset.blockBox = "tx";
|
||||||
|
io.dataset.blockIo = "";
|
||||||
|
io.append(
|
||||||
|
createInlineBox("Input", extras.totalInputs.toLocaleString(), "input"),
|
||||||
|
createInlineBox("Output", extras.totalOutputs.toLocaleString(), "output"),
|
||||||
|
);
|
||||||
|
transactions.append(
|
||||||
|
createInlineRow("Tx", [block.txCount.toLocaleString()]),
|
||||||
|
io,
|
||||||
|
);
|
||||||
|
box.append(
|
||||||
|
createInlineRow("Block", [`${formatBytes(block.size)} · ${formatBlockFill(block)}`]),
|
||||||
|
transactions,
|
||||||
|
);
|
||||||
|
|
||||||
|
return box;
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
import { createPoolLogo, getPoolDisplayName } from "../../pools/index.js";
|
||||||
|
import { formatFeeRate } from "../../utils/fee-rate.js";
|
||||||
|
import { createCubeButton, createCubeDiv } from "./cube/index.js";
|
||||||
|
import { onPlainClick } from "./events.js";
|
||||||
|
import {
|
||||||
|
createHeightElement,
|
||||||
|
dim,
|
||||||
|
formatHHMM,
|
||||||
|
formatShortDate,
|
||||||
|
} from "./format.js";
|
||||||
|
|
||||||
|
/** @typedef {import("../../modules/brk-client/index.js").BlockInfoV1} Block */
|
||||||
|
/** @typedef {import("../../modules/brk-client/index.js").MempoolBlock} MempoolBlock */
|
||||||
|
|
||||||
|
export function createPlaceholderCube() {
|
||||||
|
const cube = document.createElement("div");
|
||||||
|
|
||||||
|
cube.dataset.cube = "";
|
||||||
|
cube.dataset.placeholder = "";
|
||||||
|
|
||||||
|
return cube;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Block} block
|
||||||
|
* @param {(cube: HTMLButtonElement) => void} onSelect
|
||||||
|
*/
|
||||||
|
export function createEnteringConfirmedCube(block, onSelect) {
|
||||||
|
const cube = createConfirmedCube(block, onSelect);
|
||||||
|
|
||||||
|
markCubeEntering(cube);
|
||||||
|
|
||||||
|
return cube;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Block} block
|
||||||
|
* @param {(cube: HTMLButtonElement) => void} onSelect
|
||||||
|
*/
|
||||||
|
function createConfirmedCube(block, onSelect) {
|
||||||
|
const { pool, medianFee, feeRange, virtualSize } = block.extras;
|
||||||
|
const cube = createCubeButton(Math.min(1, virtualSize / 1_000_000));
|
||||||
|
|
||||||
|
cube.element.dataset.hash = block.id;
|
||||||
|
cube.element.dataset.height = String(block.height);
|
||||||
|
cube.element.dataset.timestamp = String(block.timestamp);
|
||||||
|
cube.element.title = `Block ${block.height.toLocaleString()}`;
|
||||||
|
onPlainClick(cube.element, () => onSelect(cube.element));
|
||||||
|
|
||||||
|
const date = document.createElement("p");
|
||||||
|
const time = document.createElement("p");
|
||||||
|
const [hh, mm] = formatHHMM(block.timestamp);
|
||||||
|
date.textContent = formatShortDate(block.timestamp);
|
||||||
|
time.append(hh, dim(":"), mm);
|
||||||
|
cube.topFace.append(date, time);
|
||||||
|
|
||||||
|
const height = document.createElement("p");
|
||||||
|
height.dataset.cubeHeight = "";
|
||||||
|
height.append(createHeightElement(block.height));
|
||||||
|
|
||||||
|
const poolElement = document.createElement("div");
|
||||||
|
const logo = createPoolLogo(pool);
|
||||||
|
const name = document.createElement("span");
|
||||||
|
poolElement.dataset.cubePool = "";
|
||||||
|
name.textContent = getPoolDisplayName(pool.name);
|
||||||
|
poolElement.append(logo, name);
|
||||||
|
cube.rightFace.append(height, poolElement);
|
||||||
|
|
||||||
|
const fees = document.createElement("div");
|
||||||
|
const median = document.createElement("p");
|
||||||
|
const range = document.createElement("p");
|
||||||
|
const unit = document.createElement("p");
|
||||||
|
fees.dataset.cubeFees = "";
|
||||||
|
median.append(dim("~"), formatFeeRate(medianFee));
|
||||||
|
range.append(
|
||||||
|
formatFeeRate(feeRange[0]),
|
||||||
|
dim("-"),
|
||||||
|
formatFeeRate(feeRange[6]),
|
||||||
|
);
|
||||||
|
unit.dataset.dim = "";
|
||||||
|
unit.textContent = "sat/vB";
|
||||||
|
fees.append(median, range, unit);
|
||||||
|
cube.leftFace.append(fees);
|
||||||
|
|
||||||
|
return cube.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {HTMLElement} cube */
|
||||||
|
function markCubeEntering(cube) {
|
||||||
|
cube.dataset.enter = "";
|
||||||
|
cube.addEventListener(
|
||||||
|
"animationend",
|
||||||
|
() => {
|
||||||
|
cube.removeAttribute("data-enter");
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createProjectedCube() {
|
||||||
|
const cube = createCubeDiv();
|
||||||
|
const date = document.createTextNode("");
|
||||||
|
const hh = document.createTextNode("");
|
||||||
|
const mm = document.createTextNode("");
|
||||||
|
const txs = document.createTextNode("");
|
||||||
|
const txsUnit = document.createTextNode("");
|
||||||
|
const median = document.createTextNode("");
|
||||||
|
const rangeLo = document.createTextNode("");
|
||||||
|
const rangeHi = document.createTextNode("");
|
||||||
|
|
||||||
|
const dateElement = document.createElement("p");
|
||||||
|
const timeElement = document.createElement("p");
|
||||||
|
const txsElement = document.createElement("p");
|
||||||
|
const txsUnitElement = document.createElement("p");
|
||||||
|
const medianElement = document.createElement("p");
|
||||||
|
const rangeElement = document.createElement("p");
|
||||||
|
const unitElement = document.createElement("p");
|
||||||
|
|
||||||
|
cube.element.dataset.projected = "";
|
||||||
|
dateElement.append(date);
|
||||||
|
timeElement.append(hh, dim(":"), mm);
|
||||||
|
cube.topFace.append(dateElement, timeElement);
|
||||||
|
|
||||||
|
txsElement.append(txs);
|
||||||
|
txsUnitElement.dataset.dim = "";
|
||||||
|
txsUnitElement.append(txsUnit);
|
||||||
|
cube.rightFace.append(txsElement, txsUnitElement);
|
||||||
|
|
||||||
|
medianElement.append(dim("~"), median);
|
||||||
|
rangeElement.append(rangeLo, dim("-"), rangeHi);
|
||||||
|
unitElement.dataset.dim = "";
|
||||||
|
unitElement.textContent = "sat/vB";
|
||||||
|
cube.leftFace.append(medianElement, rangeElement, unitElement);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...cube,
|
||||||
|
parts: { date, hh, mm, txs, txsUnit, median, rangeLo, rangeHi },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ReturnType<typeof createProjectedCube>} cube @param {MempoolBlock} block */
|
||||||
|
export function updateProjectedCube(cube, block) {
|
||||||
|
cube.element.style.setProperty(
|
||||||
|
"--fill",
|
||||||
|
String(Math.min(1, block.blockVSize / 1_000_000)),
|
||||||
|
);
|
||||||
|
|
||||||
|
cube.parts.txs.nodeValue = block.nTx.toLocaleString();
|
||||||
|
cube.parts.txsUnit.nodeValue = block.nTx === 1 ? "tx" : "txs";
|
||||||
|
cube.parts.median.nodeValue = formatFeeRate(block.medianFee);
|
||||||
|
cube.parts.rangeLo.nodeValue = formatFeeRate(block.feeRange[0]);
|
||||||
|
cube.parts.rangeHi.nodeValue = formatFeeRate(block.feeRange[6]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {ReturnType<typeof createProjectedCube>} cube @param {number} timestamp */
|
||||||
|
export function updateProjectedTime(cube, timestamp) {
|
||||||
|
const [hh, mm] = formatHHMM(timestamp);
|
||||||
|
|
||||||
|
cube.parts.date.nodeValue = formatShortDate(timestamp);
|
||||||
|
cube.parts.hh.nodeValue = hh;
|
||||||
|
cube.parts.mm.nodeValue = mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {HTMLElement} cube */
|
||||||
|
export function setConfirmedInterval(cube) {
|
||||||
|
const prev = /** @type {HTMLElement | null} */ (cube.previousElementSibling);
|
||||||
|
if (!prev?.dataset.timestamp) return;
|
||||||
|
|
||||||
|
cube.style.setProperty(
|
||||||
|
"--block-interval",
|
||||||
|
String(
|
||||||
|
Math.max(
|
||||||
|
0,
|
||||||
|
Number(cube.dataset.timestamp) - Number(prev.dataset.timestamp),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@ function populateCube(element, fill) {
|
|||||||
const rightFace = createFace("face-text", "right");
|
const rightFace = createFace("face-text", "right");
|
||||||
const leftFace = createFace("face-text", "left");
|
const leftFace = createFace("face-text", "left");
|
||||||
|
|
||||||
element.classList.add("cube");
|
element.dataset.cube = "";
|
||||||
element.style.setProperty("--fill", String(fill));
|
element.style.setProperty("--fill", String(fill));
|
||||||
element.append(
|
element.append(
|
||||||
createFace("glass", "bottom"),
|
createFace("glass", "bottom"),
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
--cube-empty-alpha: 0.4;
|
--cube-empty-alpha: 0.4;
|
||||||
--face-step: 0.033;
|
--face-step: 0.033;
|
||||||
|
|
||||||
&:not(.loading) .cube[data-enter] {
|
&:not([data-loading]) [data-cube][data-enter] {
|
||||||
animation: confirmed-cube-enter 180ms ease-out both;
|
animation: confirmed-cube-enter 180ms ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cube {
|
[data-cube] {
|
||||||
--cube-width: calc(var(--cube-size) * 2 * var(--iso-scale));
|
--cube-width: calc(var(--cube-size) * 2 * var(--iso-scale));
|
||||||
--cube-height: calc(var(--cube-size) * 2);
|
--cube-height: calc(var(--cube-size) * 2);
|
||||||
|
|
||||||
@@ -69,26 +69,26 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:is(button):active,
|
&:is(button):active,
|
||||||
&.selected {
|
&[data-selected] {
|
||||||
color: var(--black);
|
color: var(--black);
|
||||||
--face-color-base: var(--orange);
|
--face-color-base: var(--orange);
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-press]:not(.selected) {
|
&[data-press]:not([data-selected]) {
|
||||||
color: var(--background-color);
|
color: var(--background-color);
|
||||||
--face-color-base: var(--inv-border-color);
|
--face-color-base: var(--inv-border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:is(button):active,
|
&:is(button):active,
|
||||||
&.selected,
|
&[data-selected],
|
||||||
&[data-press]:not(.selected) {
|
&[data-press]:not([data-selected]) {
|
||||||
--face-top: var(--state-face-top);
|
--face-top: var(--state-face-top);
|
||||||
--face-right: var(--state-face-right);
|
--face-right: var(--state-face-right);
|
||||||
--face-left: var(--state-face-left);
|
--face-left: var(--state-face-left);
|
||||||
--face-bottom: var(--state-face-bottom);
|
--face-bottom: var(--state-face-bottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.projected {
|
&[data-projected] {
|
||||||
animation: projected-cube-pulse 4s ease-in-out infinite;
|
animation: projected-cube-pulse 4s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.skeleton .face-text {
|
&[data-skeleton] .face-text {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { onPlainClick } from "./events.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {"tip"} name
|
||||||
|
* @param {string} label
|
||||||
|
* @param {string} mobileLabel
|
||||||
|
* @param {string} title
|
||||||
|
* @param {() => void} handler
|
||||||
|
*/
|
||||||
|
export function createEdgeButton(name, label, mobileLabel, title, handler) {
|
||||||
|
const button = document.createElement("button");
|
||||||
|
|
||||||
|
button.type = "button";
|
||||||
|
button.title = title;
|
||||||
|
button.ariaLabel = title;
|
||||||
|
button.dataset.edge = name;
|
||||||
|
button.dataset.mobileLabel = mobileLabel;
|
||||||
|
button.textContent = label;
|
||||||
|
onPlainClick(button, handler);
|
||||||
|
|
||||||
|
return button;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { isPlainLeftClick } from "../../utils/event.js";
|
||||||
|
|
||||||
|
/** @param {HTMLElement} element @param {() => void} handler */
|
||||||
|
export function onPlainClick(element, handler) {
|
||||||
|
element.addEventListener("click", (event) => {
|
||||||
|
if (!(event instanceof MouseEvent) || !isPlainLeftClick(event)) return;
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
handler();
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
const MONTHS = /** @type {const} */ ([
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
]);
|
||||||
|
|
||||||
|
/** @param {string} text */
|
||||||
|
export function dim(text) {
|
||||||
|
const element = document.createElement("span");
|
||||||
|
|
||||||
|
element.dataset.dim = "";
|
||||||
|
element.textContent = text;
|
||||||
|
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} height */
|
||||||
|
export function createHeightElement(height) {
|
||||||
|
const container = document.createElement("span");
|
||||||
|
const prefix = document.createElement("span");
|
||||||
|
const value = document.createElement("span");
|
||||||
|
|
||||||
|
prefix.dataset.dim = "";
|
||||||
|
prefix.textContent = `#${"0".repeat(Math.max(0, 7 - String(height).length))}`;
|
||||||
|
value.textContent = String(height);
|
||||||
|
container.append(prefix, value);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} unixSeconds */
|
||||||
|
export function formatShortDate(unixSeconds) {
|
||||||
|
const date = new Date(unixSeconds * 1_000);
|
||||||
|
|
||||||
|
return `${MONTHS[date.getMonth()]} ${date.getDate()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {number} unixSeconds */
|
||||||
|
export function formatHHMM(unixSeconds) {
|
||||||
|
const date = new Date(unixSeconds * 1_000);
|
||||||
|
|
||||||
|
return [
|
||||||
|
String(date.getHours()).padStart(2, "0"),
|
||||||
|
String(date.getMinutes()).padStart(2, "0"),
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,7 +1,24 @@
|
|||||||
import { createPoolLogo, getPoolDisplayName } from "../../pools/index.js";
|
|
||||||
import { brk } from "../../utils/client.js";
|
import { brk } from "../../utils/client.js";
|
||||||
import { isPlainLeftClick } from "../../utils/event.js";
|
import {
|
||||||
import { createCubeButton, createCubeDiv } from "./cube/index.js";
|
createEnteringConfirmedCube,
|
||||||
|
createPlaceholderCube,
|
||||||
|
createProjectedCube,
|
||||||
|
setConfirmedInterval,
|
||||||
|
updateProjectedCube,
|
||||||
|
updateProjectedTime,
|
||||||
|
} from "./block-cube.js";
|
||||||
|
import { createEdgeButton } from "./edge.js";
|
||||||
|
import {
|
||||||
|
distanceFromViewport,
|
||||||
|
findVisibleConfirmedHeight,
|
||||||
|
isHorizontalLayout,
|
||||||
|
olderRemaining,
|
||||||
|
olderRunway,
|
||||||
|
olderWheelDelta,
|
||||||
|
preserveScrollPosition,
|
||||||
|
scrollToElement,
|
||||||
|
} from "./scroll.js";
|
||||||
|
import { transitionMs } from "./transition.js";
|
||||||
|
|
||||||
const BLOCK_BATCH_SIZE = 15;
|
const BLOCK_BATCH_SIZE = 15;
|
||||||
const EDGE_LOAD_DISTANCE = 50;
|
const EDGE_LOAD_DISTANCE = 50;
|
||||||
@@ -10,107 +27,11 @@ const POLL_INTERVAL = 1_000;
|
|||||||
const PROJECTED_LIMIT = 8;
|
const PROJECTED_LIMIT = 8;
|
||||||
const TARGET_BLOCK_SECONDS = 600;
|
const TARGET_BLOCK_SECONDS = 600;
|
||||||
const TIP_BLOCK_THRESHOLD = 10;
|
const TIP_BLOCK_THRESHOLD = 10;
|
||||||
const MONTHS = /** @type {const} */ ([
|
|
||||||
"Jan",
|
|
||||||
"Feb",
|
|
||||||
"Mar",
|
|
||||||
"Apr",
|
|
||||||
"May",
|
|
||||||
"Jun",
|
|
||||||
"Jul",
|
|
||||||
"Aug",
|
|
||||||
"Sep",
|
|
||||||
"Oct",
|
|
||||||
"Nov",
|
|
||||||
"Dec",
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** @typedef {Awaited<ReturnType<typeof brk.getBlocksV1>>[number]} Block */
|
/** @typedef {Awaited<ReturnType<typeof brk.getBlocksV1>>[number]} Block */
|
||||||
/** @typedef {Awaited<ReturnType<typeof brk.getMempoolBlocks>>[number]} MempoolBlock */
|
/** @typedef {Awaited<ReturnType<typeof brk.getMempoolBlocks>>[number]} MempoolBlock */
|
||||||
/** @typedef {{ generation: number, startHeight: number, placeholders: HTMLElement[] }} OlderBatch */
|
/** @typedef {{ generation: number, startHeight: number, placeholders: HTMLElement[] }} OlderBatch */
|
||||||
|
|
||||||
/** @param {number} rate */
|
|
||||||
function formatFeeRate(rate) {
|
|
||||||
if (rate >= 1_000_000) return `${(rate / 1_000_000).toFixed(1)}M`;
|
|
||||||
if (rate >= 100_000) return `${Math.round(rate / 1_000)}k`;
|
|
||||||
if (rate >= 1_000) return `${(rate / 1_000).toFixed(1)}k`;
|
|
||||||
if (rate >= 100) return Math.round(rate).toLocaleString();
|
|
||||||
if (rate >= 10) return rate.toFixed(1);
|
|
||||||
return rate.toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} height */
|
|
||||||
function createHeightElement(height) {
|
|
||||||
const container = document.createElement("span");
|
|
||||||
const prefix = document.createElement("span");
|
|
||||||
const value = document.createElement("span");
|
|
||||||
|
|
||||||
prefix.classList.add("dim");
|
|
||||||
prefix.textContent = `#${"0".repeat(Math.max(0, 7 - String(height).length))}`;
|
|
||||||
value.textContent = String(height);
|
|
||||||
container.append(prefix, value);
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {HTMLElement} element @param {() => void} handler */
|
|
||||||
function onPlainClick(element, handler) {
|
|
||||||
element.addEventListener("click", (event) => {
|
|
||||||
if (!(event instanceof MouseEvent) || !isPlainLeftClick(event)) return;
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
handler();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} text @param {string} [className] */
|
|
||||||
function span(text, className) {
|
|
||||||
const element = document.createElement("span");
|
|
||||||
|
|
||||||
if (className) element.classList.add(className);
|
|
||||||
element.textContent = text;
|
|
||||||
|
|
||||||
return element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} unixSeconds */
|
|
||||||
function formatShortDate(unixSeconds) {
|
|
||||||
const date = new Date(unixSeconds * 1_000);
|
|
||||||
|
|
||||||
return `${MONTHS[date.getMonth()]} ${date.getDate()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {number} unixSeconds */
|
|
||||||
function formatHHMM(unixSeconds) {
|
|
||||||
const date = new Date(unixSeconds * 1_000);
|
|
||||||
|
|
||||||
return [
|
|
||||||
String(date.getHours()).padStart(2, "0"),
|
|
||||||
String(date.getMinutes()).padStart(2, "0"),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {"tip"} className
|
|
||||||
* @param {string} label
|
|
||||||
* @param {string} mobileLabel
|
|
||||||
* @param {string} title
|
|
||||||
* @param {() => void} handler
|
|
||||||
*/
|
|
||||||
function createEdgeButton(className, label, mobileLabel, title, handler) {
|
|
||||||
const button = document.createElement("button");
|
|
||||||
|
|
||||||
button.classList.add("edge", className);
|
|
||||||
button.type = "button";
|
|
||||||
button.title = title;
|
|
||||||
button.ariaLabel = title;
|
|
||||||
button.dataset.mobileLabel = mobileLabel;
|
|
||||||
button.textContent = label;
|
|
||||||
onPlainClick(button, handler);
|
|
||||||
|
|
||||||
return button;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ onSelect?: (block: Block) => void }} [options]
|
* @param {{ onSelect?: (block: Block) => void }} [options]
|
||||||
*/
|
*/
|
||||||
@@ -124,8 +45,8 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
|
|
||||||
element.id = "chain";
|
element.id = "chain";
|
||||||
setTipVisible(false);
|
setTipVisible(false);
|
||||||
scrollElement.classList.add("scroll");
|
scrollElement.dataset.chainScroll = "";
|
||||||
blocksElement.classList.add("blocks");
|
blocksElement.dataset.chainBlocks = "";
|
||||||
scrollElement.append(blocksElement);
|
scrollElement.append(blocksElement);
|
||||||
element.append(tipButton, scrollElement);
|
element.append(tipButton, scrollElement);
|
||||||
|
|
||||||
@@ -192,7 +113,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deselectCube() {
|
function deselectCube() {
|
||||||
if (selectedCube) selectedCube.classList.remove("selected");
|
if (selectedCube) delete selectedCube.dataset.selected;
|
||||||
selectedCube = null;
|
selectedCube = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,7 +128,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
|
|
||||||
jumping = true;
|
jumping = true;
|
||||||
|
|
||||||
element.classList.add("jumping");
|
element.dataset.jumping = "";
|
||||||
element.addEventListener("transitionend", finishJumpToTip);
|
element.addEventListener("transitionend", finishJumpToTip);
|
||||||
jumpTimeout = window.setTimeout(
|
jumpTimeout = window.setTimeout(
|
||||||
finishJumpToTip,
|
finishJumpToTip,
|
||||||
@@ -236,43 +157,10 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
element.removeEventListener("transitionend", finishJumpToTip);
|
element.removeEventListener("transitionend", finishJumpToTip);
|
||||||
element.classList.remove("jumping");
|
delete element.dataset.jumping;
|
||||||
jumping = false;
|
jumping = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Element} element
|
|
||||||
* @param {string} property
|
|
||||||
*/
|
|
||||||
function transitionMs(element, property) {
|
|
||||||
const style = getComputedStyle(element);
|
|
||||||
const properties = style.transitionProperty.split(",").map((part) => {
|
|
||||||
return part.trim();
|
|
||||||
});
|
|
||||||
const durations = parseCssTimes(style.transitionDuration);
|
|
||||||
const delays = parseCssTimes(style.transitionDelay);
|
|
||||||
const index = properties.findIndex((part) => {
|
|
||||||
return part === property || part === "all";
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index < 0) return 0;
|
|
||||||
|
|
||||||
const duration = durations[index] ?? durations.at(-1) ?? 0;
|
|
||||||
const delay = delays[index] ?? delays.at(-1) ?? 0;
|
|
||||||
|
|
||||||
return duration + delay;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {string} value */
|
|
||||||
function parseCssTimes(value) {
|
|
||||||
return value.split(",").map((part) => {
|
|
||||||
const time = part.trim();
|
|
||||||
const amount = Number.parseFloat(time);
|
|
||||||
|
|
||||||
return time.endsWith("ms") ? amount : amount * 1_000;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {HTMLButtonElement} cube
|
* @param {HTMLButtonElement} cube
|
||||||
* @param {{ scroll?: "smooth" | "instant" }} [options]
|
* @param {{ scroll?: "smooth" | "instant" }} [options]
|
||||||
@@ -281,7 +169,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
if (cube !== selectedCube) {
|
if (cube !== selectedCube) {
|
||||||
deselectCube();
|
deselectCube();
|
||||||
selectedCube = cube;
|
selectedCube = cube;
|
||||||
cube.classList.add("selected");
|
cube.dataset.selected = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const hash = cube.dataset.hash;
|
const hash = cube.dataset.hash;
|
||||||
@@ -294,52 +182,8 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Element} target
|
|
||||||
* @param {"smooth" | "instant"} behavior
|
|
||||||
*/
|
|
||||||
function scrollToElement(target, behavior) {
|
|
||||||
target.scrollIntoView({
|
|
||||||
behavior,
|
|
||||||
block: "center",
|
|
||||||
inline: "center",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {Element | null | undefined} anchor
|
|
||||||
* @param {DOMRect | undefined} anchorRect
|
|
||||||
*/
|
|
||||||
function preserveScrollPosition(anchor, anchorRect) {
|
|
||||||
if (!anchor || !anchorRect) return;
|
|
||||||
|
|
||||||
const rect = anchor.getBoundingClientRect();
|
|
||||||
|
|
||||||
scrollElement.scrollTop += rect.top - anchorRect.top;
|
|
||||||
scrollElement.scrollLeft += rect.left - anchorRect.left;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isHorizontal() {
|
function isHorizontal() {
|
||||||
return getComputedStyle(blocksElement).flexDirection.startsWith("row");
|
return isHorizontalLayout(blocksElement);
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {boolean} horizontal */
|
|
||||||
function olderRemaining(horizontal) {
|
|
||||||
return horizontal
|
|
||||||
? scrollElement.scrollWidth -
|
|
||||||
scrollElement.clientWidth -
|
|
||||||
scrollElement.scrollLeft
|
|
||||||
: scrollElement.scrollHeight -
|
|
||||||
scrollElement.clientHeight -
|
|
||||||
scrollElement.scrollTop;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {boolean} horizontal */
|
|
||||||
function olderRunway(horizontal) {
|
|
||||||
return (
|
|
||||||
(horizontal ? scrollElement.clientWidth : scrollElement.clientHeight) *
|
|
||||||
OLDER_RESERVE_VIEWPORTS
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {number} [delta] */
|
/** @param {number} [delta] */
|
||||||
@@ -347,12 +191,13 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
if (!active || oldestReservedHeight <= 0) return;
|
if (!active || oldestReservedHeight <= 0) return;
|
||||||
|
|
||||||
const horizontal = isHorizontal();
|
const horizontal = isHorizontal();
|
||||||
const runway = olderRunway(horizontal) + delta;
|
const runway =
|
||||||
let remaining = olderRemaining(horizontal);
|
olderRunway(scrollElement, horizontal, OLDER_RESERVE_VIEWPORTS) + delta;
|
||||||
|
let remaining = olderRemaining(scrollElement, horizontal);
|
||||||
|
|
||||||
while (remaining < runway) {
|
while (remaining < runway) {
|
||||||
if (!reserveOlderBatch()) return;
|
if (!reserveOlderBatch()) return;
|
||||||
remaining = olderRemaining(horizontal);
|
remaining = olderRemaining(scrollElement, horizontal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,10 +228,8 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
const placeholders = /** @type {HTMLElement[]} */ ([]);
|
const placeholders = /** @type {HTMLElement[]} */ ([]);
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const cube = document.createElement("div");
|
const cube = createPlaceholderCube();
|
||||||
|
|
||||||
cube.classList.add("cube");
|
|
||||||
cube.dataset.placeholder = "";
|
|
||||||
placeholders.push(cube);
|
placeholders.push(cube);
|
||||||
fragment.append(cube);
|
fragment.append(cube);
|
||||||
}
|
}
|
||||||
@@ -413,6 +256,13 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @param {Block} block */
|
||||||
|
function createKnownEnteringConfirmedCube(block) {
|
||||||
|
blocksByHash.set(block.id, block);
|
||||||
|
|
||||||
|
return createEnteringConfirmedCube(block, selectCube);
|
||||||
|
}
|
||||||
|
|
||||||
/** @param {Block[]} blocks */
|
/** @param {Block[]} blocks */
|
||||||
function appendNewerBlocks(blocks) {
|
function appendNewerBlocks(blocks) {
|
||||||
if (!blocks.length) return false;
|
if (!blocks.length) return false;
|
||||||
@@ -424,7 +274,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
const block = blocks[i];
|
const block = blocks[i];
|
||||||
|
|
||||||
if (block.height > newestHeight) {
|
if (block.height > newestHeight) {
|
||||||
appendConfirmed(createEnteringConfirmedCube(block));
|
appendConfirmed(createKnownEnteringConfirmedCube(block));
|
||||||
} else {
|
} else {
|
||||||
blocksByHash.set(block.id, block);
|
blocksByHash.set(block.id, block);
|
||||||
}
|
}
|
||||||
@@ -435,7 +285,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
updateTipCube();
|
updateTipCube();
|
||||||
refreshProjected();
|
refreshProjected();
|
||||||
|
|
||||||
preserveScrollPosition(anchor, anchorRect);
|
preserveScrollPosition(scrollElement, anchor, anchorRect);
|
||||||
|
|
||||||
syncTipVisibility();
|
syncTipVisibility();
|
||||||
|
|
||||||
@@ -452,7 +302,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
clear();
|
clear();
|
||||||
|
|
||||||
for (const block of blocks) {
|
for (const block of blocks) {
|
||||||
prependConfirmed(createEnteringConfirmedCube(block));
|
prependConfirmed(createKnownEnteringConfirmedCube(block));
|
||||||
}
|
}
|
||||||
|
|
||||||
newestHeight = blocks[0].height;
|
newestHeight = blocks[0].height;
|
||||||
@@ -504,10 +354,12 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const cube of blocksElement.children) {
|
for (const cube of blocksElement.children) {
|
||||||
if (!cube.classList.contains("projected")) cube.classList.add("skeleton");
|
if (!cube.hasAttribute("data-projected")) {
|
||||||
|
cube.setAttribute("data-skeleton", "");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
element.classList.add("loading");
|
element.dataset.loading = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const height = await resolveHeight(hashOrHeight);
|
const height = await resolveHeight(hashOrHeight);
|
||||||
@@ -519,7 +371,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
console.error("explore chain load:", error);
|
console.error("explore chain load:", error);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
element.classList.remove("loading");
|
delete element.dataset.loading;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,7 +432,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cubes = [...blocks].reverse().map(createEnteringConfirmedCube);
|
const cubes = [...blocks].reverse().map(createKnownEnteringConfirmedCube);
|
||||||
|
|
||||||
for (let i = 0; i < batch.placeholders.length; i++) {
|
for (let i = 0; i < batch.placeholders.length; i++) {
|
||||||
const cube = cubes[i];
|
const cube = cubes[i];
|
||||||
@@ -633,93 +485,6 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {HTMLElement} cube */
|
|
||||||
function markCubeEntering(cube) {
|
|
||||||
cube.dataset.enter = "";
|
|
||||||
cube.addEventListener(
|
|
||||||
"animationend",
|
|
||||||
() => {
|
|
||||||
cube.removeAttribute("data-enter");
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {Block} block */
|
|
||||||
function createEnteringConfirmedCube(block) {
|
|
||||||
const cube = createConfirmedCube(block);
|
|
||||||
|
|
||||||
markCubeEntering(cube);
|
|
||||||
|
|
||||||
return cube;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {Block} block */
|
|
||||||
function createConfirmedCube(block) {
|
|
||||||
const { pool, medianFee, feeRange, virtualSize } = block.extras;
|
|
||||||
const cube = createCubeButton(Math.min(1, virtualSize / 1_000_000));
|
|
||||||
|
|
||||||
cube.element.dataset.hash = block.id;
|
|
||||||
cube.element.dataset.height = String(block.height);
|
|
||||||
cube.element.dataset.timestamp = String(block.timestamp);
|
|
||||||
cube.element.title = `Block ${block.height.toLocaleString()}`;
|
|
||||||
blocksByHash.set(block.id, block);
|
|
||||||
onPlainClick(cube.element, () => selectCube(cube.element));
|
|
||||||
|
|
||||||
const date = document.createElement("p");
|
|
||||||
const time = document.createElement("p");
|
|
||||||
const [hh, mm] = formatHHMM(block.timestamp);
|
|
||||||
date.textContent = formatShortDate(block.timestamp);
|
|
||||||
time.append(hh, span(":", "dim"), mm);
|
|
||||||
cube.topFace.append(date, time);
|
|
||||||
|
|
||||||
const height = document.createElement("p");
|
|
||||||
height.classList.add("height");
|
|
||||||
height.append(createHeightElement(block.height));
|
|
||||||
|
|
||||||
const poolElement = document.createElement("div");
|
|
||||||
const logo = createPoolLogo(pool);
|
|
||||||
const name = document.createElement("span");
|
|
||||||
poolElement.classList.add("pool");
|
|
||||||
name.textContent = getPoolDisplayName(pool.name);
|
|
||||||
poolElement.append(logo, name);
|
|
||||||
cube.rightFace.append(height, poolElement);
|
|
||||||
|
|
||||||
const fees = document.createElement("div");
|
|
||||||
const median = document.createElement("p");
|
|
||||||
const range = document.createElement("p");
|
|
||||||
const unit = document.createElement("p");
|
|
||||||
fees.classList.add("fees");
|
|
||||||
median.append(span("~", "dim"), formatFeeRate(medianFee));
|
|
||||||
range.append(
|
|
||||||
formatFeeRate(feeRange[0]),
|
|
||||||
span("-", "dim"),
|
|
||||||
formatFeeRate(feeRange[6]),
|
|
||||||
);
|
|
||||||
unit.classList.add("dim");
|
|
||||||
unit.textContent = "sat/vB";
|
|
||||||
fees.append(median, range, unit);
|
|
||||||
cube.leftFace.append(fees);
|
|
||||||
|
|
||||||
return cube.element;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {HTMLElement} cube */
|
|
||||||
function setConfirmedInterval(cube) {
|
|
||||||
const prev = /** @type {HTMLElement | null} */ (cube.previousElementSibling);
|
|
||||||
if (!prev?.dataset.timestamp) return;
|
|
||||||
|
|
||||||
cube.style.setProperty(
|
|
||||||
"--block-interval",
|
|
||||||
String(
|
|
||||||
Math.max(
|
|
||||||
0,
|
|
||||||
Number(cube.dataset.timestamp) - Number(prev.dataset.timestamp),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {HTMLButtonElement} cube */
|
/** @param {HTMLButtonElement} cube */
|
||||||
function prependConfirmed(cube) {
|
function prependConfirmed(cube) {
|
||||||
const oldFirst = /** @type {HTMLElement | null} */ (
|
const oldFirst = /** @type {HTMLElement | null} */ (
|
||||||
@@ -758,61 +523,6 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
refreshProjected();
|
refreshProjected();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createProjectedCube() {
|
|
||||||
const cube = createCubeDiv();
|
|
||||||
const date = document.createTextNode("");
|
|
||||||
const hh = document.createTextNode("");
|
|
||||||
const mm = document.createTextNode("");
|
|
||||||
const txs = document.createTextNode("");
|
|
||||||
const txsUnit = document.createTextNode("");
|
|
||||||
const median = document.createTextNode("");
|
|
||||||
const rangeLo = document.createTextNode("");
|
|
||||||
const rangeHi = document.createTextNode("");
|
|
||||||
|
|
||||||
const dateElement = document.createElement("p");
|
|
||||||
const timeElement = document.createElement("p");
|
|
||||||
const txsElement = document.createElement("p");
|
|
||||||
const txsUnitElement = document.createElement("p");
|
|
||||||
const medianElement = document.createElement("p");
|
|
||||||
const rangeElement = document.createElement("p");
|
|
||||||
const unitElement = document.createElement("p");
|
|
||||||
|
|
||||||
cube.element.classList.add("projected");
|
|
||||||
dateElement.append(date);
|
|
||||||
timeElement.append(hh, span(":", "dim"), mm);
|
|
||||||
cube.topFace.append(dateElement, timeElement);
|
|
||||||
|
|
||||||
txsElement.append(txs);
|
|
||||||
txsUnitElement.classList.add("dim");
|
|
||||||
txsUnitElement.append(txsUnit);
|
|
||||||
cube.rightFace.append(txsElement, txsUnitElement);
|
|
||||||
|
|
||||||
medianElement.append(span("~", "dim"), median);
|
|
||||||
rangeElement.append(rangeLo, span("-", "dim"), rangeHi);
|
|
||||||
unitElement.classList.add("dim");
|
|
||||||
unitElement.textContent = "sat/vB";
|
|
||||||
cube.leftFace.append(medianElement, rangeElement, unitElement);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...cube,
|
|
||||||
parts: { date, hh, mm, txs, txsUnit, median, rangeLo, rangeHi },
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {ReturnType<typeof createProjectedCube>} cube @param {MempoolBlock} block */
|
|
||||||
function updateProjectedCube(cube, block) {
|
|
||||||
cube.element.style.setProperty(
|
|
||||||
"--fill",
|
|
||||||
String(Math.min(1, block.blockVSize / 1_000_000)),
|
|
||||||
);
|
|
||||||
|
|
||||||
cube.parts.txs.nodeValue = block.nTx.toLocaleString();
|
|
||||||
cube.parts.txsUnit.nodeValue = block.nTx === 1 ? "tx" : "txs";
|
|
||||||
cube.parts.median.nodeValue = formatFeeRate(block.medianFee);
|
|
||||||
cube.parts.rangeLo.nodeValue = formatFeeRate(block.feeRange[0]);
|
|
||||||
cube.parts.rangeHi.nodeValue = formatFeeRate(block.feeRange[6]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshProjected() {
|
function refreshProjected() {
|
||||||
if (!projectedCubes.length || !newestTimestamp) return;
|
if (!projectedCubes.length || !newestTimestamp) return;
|
||||||
|
|
||||||
@@ -824,15 +534,12 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
const cube = projectedCubes[i];
|
const cube = projectedCubes[i];
|
||||||
const interval = i === 0 ? elapsed : TARGET_BLOCK_SECONDS;
|
const interval = i === 0 ? elapsed : TARGET_BLOCK_SECONDS;
|
||||||
const timestamp = now + i * TARGET_BLOCK_SECONDS;
|
const timestamp = now + i * TARGET_BLOCK_SECONDS;
|
||||||
const [hh, mm] = formatHHMM(timestamp);
|
|
||||||
|
|
||||||
if (updateLayout) {
|
if (updateLayout) {
|
||||||
cube.element.style.setProperty("--block-interval", String(interval));
|
cube.element.style.setProperty("--block-interval", String(interval));
|
||||||
}
|
}
|
||||||
|
|
||||||
cube.parts.date.nodeValue = formatShortDate(timestamp);
|
updateProjectedTime(cube, timestamp);
|
||||||
cube.parts.hh.nodeValue = hh;
|
|
||||||
cube.parts.mm.nodeValue = mm;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,7 +565,7 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const visibleHeight = findVisibleConfirmedHeight();
|
const visibleHeight = findVisibleConfirmedHeight(scrollElement, blocksElement);
|
||||||
if (projectedCubes.some(({ element }) => isElementVisible(element))) {
|
if (projectedCubes.some(({ element }) => isElementVisible(element))) {
|
||||||
setTipVisible(false);
|
setTipVisible(false);
|
||||||
return;
|
return;
|
||||||
@@ -872,65 +579,25 @@ export function createChain({ onSelect = () => {} } = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @param {Element} element */
|
/** @param {Element} element */
|
||||||
function distanceFromViewport(element) {
|
function cubeDistanceFromViewport(element) {
|
||||||
const viewport = scrollElement.getBoundingClientRect();
|
return distanceFromViewport(scrollElement, element, isHorizontal());
|
||||||
const rect = element.getBoundingClientRect();
|
|
||||||
const horizontal = isHorizontal();
|
|
||||||
|
|
||||||
if (horizontal) {
|
|
||||||
if (rect.left > viewport.right) return rect.left - viewport.right;
|
|
||||||
if (rect.right < viewport.left) return viewport.left - rect.right;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rect.top > viewport.bottom) return rect.top - viewport.bottom;
|
|
||||||
if (rect.bottom < viewport.top) return viewport.top - rect.bottom;
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {Element} element */
|
/** @param {Element} element */
|
||||||
function isElementVisible(element) {
|
function isElementVisible(element) {
|
||||||
return distanceFromViewport(element) === 0;
|
return cubeDistanceFromViewport(element) === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldLoadNewer() {
|
function shouldLoadNewer() {
|
||||||
const cube = newestConfirmedCube();
|
const cube = newestConfirmedCube();
|
||||||
|
|
||||||
return cube != null && distanceFromViewport(cube) <= EDGE_LOAD_DISTANCE;
|
return cube != null && cubeDistanceFromViewport(cube) <= EDGE_LOAD_DISTANCE;
|
||||||
}
|
|
||||||
|
|
||||||
function findVisibleConfirmedHeight() {
|
|
||||||
const viewport = scrollElement.getBoundingClientRect();
|
|
||||||
const x = (viewport.left + viewport.right) / 2;
|
|
||||||
const y = (viewport.top + viewport.bottom) / 2;
|
|
||||||
|
|
||||||
for (const element of document.elementsFromPoint(x, y)) {
|
|
||||||
const cube = element.closest(".cube[data-height]");
|
|
||||||
|
|
||||||
if (
|
|
||||||
cube instanceof HTMLElement &&
|
|
||||||
blocksElement.contains(cube) &&
|
|
||||||
!cube.classList.contains("projected")
|
|
||||||
) {
|
|
||||||
return Number(cube.dataset.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @param {WheelEvent} event */
|
|
||||||
function olderWheelDelta(event) {
|
|
||||||
return Math.max(
|
|
||||||
0,
|
|
||||||
isHorizontal() ? Math.max(event.deltaX, event.deltaY) : event.deltaY,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollElement.addEventListener(
|
scrollElement.addEventListener(
|
||||||
"wheel",
|
"wheel",
|
||||||
(event) => {
|
(event) => {
|
||||||
reserveOlderRunway(olderWheelDelta(event));
|
reserveOlderRunway(olderWheelDelta(event, isHorizontal()));
|
||||||
},
|
},
|
||||||
{ passive: true },
|
{ passive: true },
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/**
|
||||||
|
* @param {Element} target
|
||||||
|
* @param {"smooth" | "instant"} behavior
|
||||||
|
*/
|
||||||
|
export function scrollToElement(target, behavior) {
|
||||||
|
target.scrollIntoView({
|
||||||
|
behavior,
|
||||||
|
block: "center",
|
||||||
|
inline: "center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} scrollElement
|
||||||
|
* @param {Element | null | undefined} anchor
|
||||||
|
* @param {DOMRect | undefined} anchorRect
|
||||||
|
*/
|
||||||
|
export function preserveScrollPosition(scrollElement, anchor, anchorRect) {
|
||||||
|
if (!anchor || !anchorRect) return;
|
||||||
|
|
||||||
|
const rect = anchor.getBoundingClientRect();
|
||||||
|
|
||||||
|
scrollElement.scrollTop += rect.top - anchorRect.top;
|
||||||
|
scrollElement.scrollLeft += rect.left - anchorRect.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {HTMLElement} blocksElement */
|
||||||
|
export function isHorizontalLayout(blocksElement) {
|
||||||
|
return getComputedStyle(blocksElement).flexDirection.startsWith("row");
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {HTMLElement} scrollElement @param {boolean} horizontal */
|
||||||
|
export function olderRemaining(scrollElement, horizontal) {
|
||||||
|
return horizontal
|
||||||
|
? scrollElement.scrollWidth -
|
||||||
|
scrollElement.clientWidth -
|
||||||
|
scrollElement.scrollLeft
|
||||||
|
: scrollElement.scrollHeight -
|
||||||
|
scrollElement.clientHeight -
|
||||||
|
scrollElement.scrollTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} scrollElement
|
||||||
|
* @param {boolean} horizontal
|
||||||
|
* @param {number} viewports
|
||||||
|
*/
|
||||||
|
export function olderRunway(scrollElement, horizontal, viewports) {
|
||||||
|
return (
|
||||||
|
(horizontal ? scrollElement.clientWidth : scrollElement.clientHeight) *
|
||||||
|
viewports
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {WheelEvent} event @param {boolean} horizontal */
|
||||||
|
export function olderWheelDelta(event, horizontal) {
|
||||||
|
return Math.max(
|
||||||
|
0,
|
||||||
|
horizontal ? Math.max(event.deltaX, event.deltaY) : event.deltaY,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} scrollElement
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {boolean} horizontal
|
||||||
|
*/
|
||||||
|
export function distanceFromViewport(scrollElement, element, horizontal) {
|
||||||
|
const viewport = scrollElement.getBoundingClientRect();
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (horizontal) {
|
||||||
|
if (rect.left > viewport.right) return rect.left - viewport.right;
|
||||||
|
if (rect.right < viewport.left) return viewport.left - rect.right;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rect.top > viewport.bottom) return rect.top - viewport.bottom;
|
||||||
|
if (rect.bottom < viewport.top) return viewport.top - rect.bottom;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {HTMLElement} scrollElement
|
||||||
|
* @param {HTMLElement} blocksElement
|
||||||
|
*/
|
||||||
|
export function findVisibleConfirmedHeight(scrollElement, blocksElement) {
|
||||||
|
const viewport = scrollElement.getBoundingClientRect();
|
||||||
|
const x = (viewport.left + viewport.right) / 2;
|
||||||
|
const y = (viewport.top + viewport.bottom) / 2;
|
||||||
|
|
||||||
|
for (const element of document.elementsFromPoint(x, y)) {
|
||||||
|
const cube = element.closest("[data-cube][data-height]");
|
||||||
|
|
||||||
|
if (
|
||||||
|
cube instanceof HTMLElement &&
|
||||||
|
blocksElement.contains(cube) &&
|
||||||
|
!cube.hasAttribute("data-projected")
|
||||||
|
) {
|
||||||
|
return Number(cube.dataset.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -18,17 +18,17 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 200ms ease;
|
transition: opacity 200ms ease;
|
||||||
|
|
||||||
&.loading,
|
&[data-loading],
|
||||||
&.jumping {
|
&[data-jumping] {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dim {
|
[data-dim] {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll {
|
[data-chain-scroll] {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -36,13 +36,13 @@
|
|||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocks {
|
[data-chain-blocks] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cube {
|
[data-cube] {
|
||||||
--cube-fall-off: pow(
|
--cube-fall-off: pow(
|
||||||
clamp(
|
clamp(
|
||||||
0,
|
0,
|
||||||
@@ -61,12 +61,12 @@
|
|||||||
margin-bottom: var(--block-gap);
|
margin-bottom: var(--block-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.face-text .height {
|
.face-text [data-cube-height] {
|
||||||
font-size: var(--font-size-sm);
|
font-size: var(--font-size-sm);
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.face-text .fees {
|
.face-text [data-cube-fees] {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.face-text .pool {
|
.face-text [data-cube-pool] {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edge {
|
[data-edge] {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(var(--main-padding) + 2.5rem);
|
top: calc(var(--main-padding) + 2.5rem);
|
||||||
left: calc(var(--cube-size) * var(--iso-scale));
|
left: calc(var(--cube-size) * var(--iso-scale));
|
||||||
@@ -134,7 +134,7 @@
|
|||||||
height: 14rem;
|
height: 14rem;
|
||||||
min-height: 14rem;
|
min-height: 14rem;
|
||||||
|
|
||||||
.scroll {
|
[data-chain-scroll] {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
@@ -146,21 +146,21 @@
|
|||||||
touch-action: pan-x;
|
touch-action: pan-x;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocks {
|
[data-chain-blocks] {
|
||||||
flex-direction: row-reverse;
|
flex-direction: row-reverse;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cube {
|
[data-cube] {
|
||||||
& + & {
|
& + & {
|
||||||
margin-right: var(--block-gap);
|
margin-right: var(--block-gap);
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edge {
|
[data-edge] {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 0.75rem;
|
left: 0.75rem;
|
||||||
translate: 0 -50%;
|
translate: 0 -50%;
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* @param {Element} element
|
||||||
|
* @param {string} property
|
||||||
|
*/
|
||||||
|
export function transitionMs(element, property) {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
const properties = style.transitionProperty.split(",").map((part) => {
|
||||||
|
return part.trim();
|
||||||
|
});
|
||||||
|
const durations = parseCssTimes(style.transitionDuration);
|
||||||
|
const delays = parseCssTimes(style.transitionDelay);
|
||||||
|
const index = properties.findIndex((part) => {
|
||||||
|
return part === property || part === "all";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index < 0) return 0;
|
||||||
|
|
||||||
|
const duration = durations[index] ?? durations.at(-1) ?? 0;
|
||||||
|
const delay = delays[index] ?? delays.at(-1) ?? 0;
|
||||||
|
|
||||||
|
return duration + delay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string} value */
|
||||||
|
function parseCssTimes(value) {
|
||||||
|
return value.split(",").map((part) => {
|
||||||
|
const time = part.trim();
|
||||||
|
const amount = Number.parseFloat(time);
|
||||||
|
|
||||||
|
return time.endsWith("ms") ? amount : amount * 1_000;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** @param {number} rate */
|
||||||
|
export function formatFeeRate(rate) {
|
||||||
|
if (rate >= 1_000_000) return `${(rate / 1_000_000).toFixed(1)}M`;
|
||||||
|
if (rate >= 100_000) return `${Math.round(rate / 1_000)}k`;
|
||||||
|
if (rate >= 1_000) return `${(rate / 1_000).toFixed(1)}k`;
|
||||||
|
if (rate >= 100) return Math.round(rate).toLocaleString();
|
||||||
|
if (rate >= 10) return rate.toFixed(1);
|
||||||
|
return rate.toFixed(2);
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ const amounts = /** @type {BtcAmountRecord[]} */ ([]);
|
|||||||
* @param {BtcAmount} amount
|
* @param {BtcAmount} amount
|
||||||
*/
|
*/
|
||||||
function renderBtcAmount(element, amount) {
|
function renderBtcAmount(element, amount) {
|
||||||
|
element.dataset.btcAmount = "";
|
||||||
|
|
||||||
if (redaction.isHidden()) {
|
if (redaction.isHidden()) {
|
||||||
element.textContent = FIXED_PRIVATE_TEXT;
|
element.textContent = FIXED_PRIVATE_TEXT;
|
||||||
return;
|
return;
|
||||||
@@ -43,7 +45,6 @@ export function createBtcAmount(tag, sats, options = {}) {
|
|||||||
signed: options.signed === true,
|
signed: options.signed === true,
|
||||||
};
|
};
|
||||||
|
|
||||||
element.classList.add("amount");
|
|
||||||
amounts.push({ element, amount });
|
amounts.push({ element, amount });
|
||||||
renderBtcAmount(element, amount);
|
renderBtcAmount(element, amount);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user