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; }