mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 22:59:58 -07:00
210 lines
6.2 KiB
JavaScript
210 lines
6.2 KiB
JavaScript
export const TX_PAGE_SIZE = 25;
|
|
|
|
/** @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.style.opacity = "0.5";
|
|
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 {Transaction} tx
|
|
* @param {string} [coinbaseAscii]
|
|
*/
|
|
const IO_LIMIT = 10;
|
|
|
|
/**
|
|
* @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) {
|
|
const limit = Math.min(items.length, IO_LIMIT);
|
|
for (let i = 0; i < limit; i++) container.append(render(items[i]));
|
|
if (items.length > IO_LIMIT) {
|
|
const btn = document.createElement("button");
|
|
btn.classList.add("show-more");
|
|
btn.textContent = `Show ${items.length - IO_LIMIT} more`;
|
|
btn.addEventListener("click", () => {
|
|
btn.remove();
|
|
for (let i = IO_LIMIT; 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;
|
|
}
|