Files
brk/website/scripts/explorer/render.js
2026-04-08 12:09:35 +02:00

215 lines
6.3 KiB
JavaScript

export const TX_PAGE_SIZE = 25;
/** @param {HTMLElement} el */
export function showPanel(el) {
el.hidden = false;
el.scrollTop = 0;
}
/** @param {HTMLElement} el */
export function hidePanel(el) {
el.hidden = true;
}
/** @param {number} sats */
export function formatBtc(sats) {
return (sats / 1e8).toFixed(8);
}
/** @param {number} rate */
export function formatFeeRate(rate) {
if (rate >= 100) return Math.round(rate).toLocaleString();
if (rate >= 10) return rate.toFixed(1);
return rate.toFixed(2);
}
/** @param {string} text @param {HTMLElement} el */
export function setAddrContent(text, el) {
el.textContent = "";
if (text.length <= 6) {
el.textContent = text;
return;
}
const head = document.createElement("span");
head.classList.add("addr-head");
head.textContent = text.slice(0, -6);
const tail = document.createElement("span");
tail.classList.add("addr-tail");
tail.textContent = text.slice(-6);
el.append(head, tail);
}
/** @param {number} height */
export function createHeightElement(height) {
const container = document.createElement("span");
const str = height.toString();
const prefix = document.createElement("span");
prefix.classList.add("dim");
prefix.style.userSelect = "none";
prefix.textContent = "#" + "0".repeat(7 - str.length);
const num = document.createElement("span");
num.textContent = str;
container.append(prefix, num);
return container;
}
/**
* @param {[string, string, (string | null)?][]} rows
* @param {HTMLElement} parent
*/
export function renderRows(rows, parent) {
for (const [label, value, href] of rows) {
const row = document.createElement("div");
row.classList.add("row");
const labelEl = document.createElement("span");
labelEl.classList.add("label");
labelEl.textContent = label;
const valueEl = document.createElement(href ? "a" : "span");
valueEl.classList.add("value");
valueEl.textContent = value;
if (href) /** @type {HTMLAnchorElement} */ (valueEl).href = href;
row.append(labelEl, valueEl);
parent.append(row);
}
}
/**
* @param {TxIn} vin
* @param {string} [coinbaseAscii]
*/
function renderInput(vin, coinbaseAscii) {
const row = document.createElement("div");
row.classList.add("tx-io");
const addr = document.createElement("span");
addr.classList.add("addr");
if (vin.isCoinbase) {
addr.textContent = "Coinbase";
addr.classList.add("coinbase");
if (coinbaseAscii) {
const sig = document.createElement("div");
sig.classList.add("coinbase-sig");
sig.textContent = coinbaseAscii;
row.append(sig);
}
} else {
const addrStr = /** @type {string | undefined} */ (
/** @type {any} */ (vin.prevout)?.scriptpubkey_address
);
if (addrStr) {
const link = document.createElement("a");
link.href = `/address/${addrStr}`;
setAddrContent(addrStr, link);
addr.append(link);
} else {
addr.textContent = "Unknown";
}
}
const amt = document.createElement("span");
amt.classList.add("amount");
amt.textContent = vin.prevout ? `${formatBtc(vin.prevout.value)} BTC` : "";
row.append(addr, amt);
return row;
}
/** @param {TxOut} vout */
function renderOutput(vout) {
const row = document.createElement("div");
row.classList.add("tx-io");
const addr = document.createElement("span");
addr.classList.add("addr");
const type = /** @type {string | undefined} */ (
/** @type {any} */ (vout).scriptpubkey_type
);
const a = /** @type {string | undefined} */ (
/** @type {any} */ (vout).scriptpubkey_address
);
if (type === "op_return") {
addr.textContent = "OP_RETURN";
addr.classList.add("op-return");
} else if (a) {
const link = document.createElement("a");
link.href = `/address/${a}`;
setAddrContent(a, link);
addr.append(link);
} else {
setAddrContent(vout.scriptpubkey, addr);
}
const amt = document.createElement("span");
amt.classList.add("amount");
amt.textContent = `${formatBtc(vout.value)} BTC`;
row.append(addr, amt);
return row;
}
/**
* @template T
* @param {T[]} items
* @param {(item: T) => HTMLElement} render
* @param {HTMLElement} container
*/
function renderCapped(items, render, container, max = 10) {
const limit = Math.min(items.length, max);
for (let i = 0; i < limit; i++) container.append(render(items[i]));
if (items.length > max) {
const btn = document.createElement("button");
btn.classList.add("show-more");
btn.textContent = `Show ${items.length - max} more`;
btn.addEventListener("click", () => {
btn.remove();
for (let i = max; i < items.length; i++) container.append(render(items[i]));
});
container.append(btn);
}
}
/** @param {Transaction} tx @param {string} [coinbaseAscii] */
export function renderTx(tx, coinbaseAscii) {
const el = document.createElement("div");
el.classList.add("tx");
const head = document.createElement("div");
head.classList.add("tx-head");
const txidEl = document.createElement("a");
txidEl.classList.add("txid");
txidEl.textContent = tx.txid;
txidEl.href = `/tx/${tx.txid}`;
head.append(txidEl);
if (tx.status?.blockTime) {
const time = document.createElement("span");
time.classList.add("tx-time");
time.textContent = new Date(tx.status.blockTime * 1000).toLocaleString();
head.append(time);
}
el.append(head);
const body = document.createElement("div");
body.classList.add("tx-body");
const inputs = document.createElement("div");
inputs.classList.add("tx-inputs");
renderCapped(tx.vin, (vin) => renderInput(vin, coinbaseAscii), inputs);
const outputs = document.createElement("div");
outputs.classList.add("tx-outputs");
renderCapped(tx.vout, renderOutput, outputs);
const totalOut = tx.vout.reduce((s, v) => s + v.value, 0);
body.append(inputs, outputs);
el.append(body);
const foot = document.createElement("div");
foot.classList.add("tx-foot");
const feeInfo = document.createElement("span");
const vsize = Math.ceil(tx.weight / 4);
const feeRate = vsize > 0 ? tx.fee / vsize : 0;
feeInfo.textContent = `${formatFeeRate(feeRate)} sat/vB \u2013 ${tx.fee.toLocaleString()} sats`;
const total = document.createElement("span");
total.classList.add("amount", "total");
total.textContent = `${formatBtc(totalOut)} BTC`;
foot.append(feeInfo, total);
el.append(foot);
return el;
}