global: snap

This commit is contained in:
nym21
2026-04-07 13:49:02 +02:00
parent e91f1386b1
commit f022f62cce
24 changed files with 746 additions and 372 deletions

View File

@@ -2,7 +2,7 @@ import { createHeader } from "../utils/dom.js";
import { chartElement } from "../utils/elements.js";
import { INDEX_FROM_LABEL } from "../utils/serde.js";
import { Unit } from "../utils/units.js";
import { createChart } from "../chart/index.js";
import { createChart } from "../utils/chart/index.js";
import { colors } from "../utils/colors.js";
import { latestPrice, onPrice } from "../utils/price.js";
import { brk } from "../client.js";

View File

@@ -1,12 +1,14 @@
import { explorerElement } from "../utils/elements.js";
import { brk } from "../client.js";
import { createPersistedValue } from "../utils/persisted.js";
const LOOKAHEAD = 15;
const TX_PAGE_SIZE = 25;
/** @type {HTMLDivElement} */ let chain;
/** @type {HTMLDivElement} */ let blocksEl;
/** @type {HTMLDivElement} */ let details;
/** @type {HTMLDivElement} */ let blockDetails;
/** @type {HTMLDivElement} */ let txDetails;
/** @type {HTMLDivElement | null} */ let selectedCube = null;
/** @type {number | undefined} */ let pollInterval;
/** @type {IntersectionObserver} */ let olderObserver;
@@ -31,9 +33,20 @@ let reachedTip = false;
/** @typedef {{ first: HTMLButtonElement, prev: HTMLButtonElement, label: HTMLSpanElement, next: HTMLButtonElement, last: HTMLButtonElement }} TxNav */
/** @type {TxNav[]} */ let txNavs = [];
/** @type {BlockInfoV1 | null} */ let txBlock = null;
let txPage = -1;
let txTotalPages = 0;
let txLoading = false;
let txLoaded = false;
const txPageParam = createPersistedValue({
defaultValue: 0,
urlKey: "page",
serialize: (v) => String(v + 1),
deserialize: (s) => Math.max(0, Number(s) - 1),
});
/** @returns {string[]} */
function pathSegments() {
return window.location.pathname.split("/").filter((v) => v);
}
export function init() {
chain = document.createElement("div");
@@ -44,11 +57,17 @@ export function init() {
blocksEl.classList.add("blocks");
chain.append(blocksEl);
details = document.createElement("div");
details.id = "block-details";
explorerElement.append(details);
blockDetails = document.createElement("div");
blockDetails.id = "block-details";
explorerElement.append(blockDetails);
initDetails();
txDetails = document.createElement("div");
txDetails.id = "tx-details";
txDetails.hidden = true;
explorerElement.append(txDetails);
initBlockDetails();
initTxDetails();
olderObserver = new IntersectionObserver(
(entries) => {
@@ -80,6 +99,17 @@ export function init() {
if (!document.hidden && !explorerElement.hidden) loadLatest();
});
window.addEventListener("popstate", () => {
const [kind, value] = pathSegments();
if (kind === "block" && value) navigateToBlock(value, false);
else if (kind === "tx" && value) showTxDetail(value);
else if (kind === "address" && value) showAddrDetail(value);
else {
blockDetails.hidden = false;
txDetails.hidden = true;
}
});
loadLatest();
}
@@ -105,6 +135,8 @@ function observeOldestEdge() {
/** @param {BlockInfoV1[]} blocks */
function appendNewerBlocks(blocks) {
if (!blocks.length) return false;
const anchor = blocksEl.lastElementChild;
const anchorRect = anchor?.getBoundingClientRect();
for (const b of [...blocks].reverse()) {
if (b.height > newestHeight) {
blocksEl.append(createBlockCube(b));
@@ -113,16 +145,57 @@ function appendNewerBlocks(blocks) {
}
}
newestHeight = Math.max(newestHeight, blocks[0].height);
if (anchor && anchorRect) {
const r = anchor.getBoundingClientRect();
chain.scrollTop += r.top - anchorRect.top;
chain.scrollLeft += r.left - anchorRect.left;
}
return true;
}
/** @param {string} hash @param {boolean} [pushUrl] */
function navigateToBlock(hash, pushUrl = true) {
if (pushUrl) history.pushState(null, "", `/block/${hash}`);
const cube = /** @type {HTMLDivElement | null} */ (
blocksEl.querySelector(`[data-hash="${hash}"]`)
);
if (cube) {
selectCube(cube, { scroll: true });
} else {
resetExplorer();
}
}
function resetExplorer() {
newestHeight = -1;
oldestHeight = Infinity;
loadingLatest = false;
loadingOlder = false;
loadingNewer = false;
reachedTip = false;
selectedCube = null;
blocksEl.innerHTML = "";
olderObserver.disconnect();
loadLatest();
}
/** @returns {Promise<number | null>} */
/** @type {Transaction | null} */
let pendingTx = null;
async function getStartHeight() {
const path = window.location.pathname.split("/").filter((v) => v);
if (path[0] !== "block" || !path[1]) return null;
const value = path[1];
if (/^\d+$/.test(value)) return Number(value);
return (await brk.getBlockV1(value)).height;
if (pendingTx) return pendingTx.status?.blockHeight ?? null;
const [kind, value] = pathSegments();
if (!value) return null;
if (kind === "block") {
if (/^\d+$/.test(value)) return Number(value);
return (await brk.getBlockV1(value)).height;
}
if (kind === "tx") {
pendingTx = await brk.getTx(value);
return pendingTx.status?.blockHeight ?? null;
}
return null;
}
async function loadLatest() {
@@ -141,7 +214,21 @@ async function loadLatest() {
newestHeight = blocks[0].height;
oldestHeight = blocks[blocks.length - 1].height;
if (startHeight === null) reachedTip = true;
selectCube(/** @type {HTMLDivElement} */ (blocksEl.lastElementChild));
const [kind, value] = pathSegments();
if (pendingTx) {
const hash = pendingTx.status?.blockHash;
const cube = /** @type {HTMLDivElement | null} */ (
hash ? blocksEl.querySelector(`[data-hash="${hash}"]`) : null
);
if (cube) selectCube(cube);
showTxFromData(pendingTx);
pendingTx = null;
} else if (kind === "address" && value) {
selectCube(/** @type {HTMLDivElement} */ (blocksEl.lastElementChild));
showAddrDetail(value);
} else {
selectCube(/** @type {HTMLDivElement} */ (blocksEl.lastElementChild));
}
loadingLatest = false;
observeOldestEdge();
if (!reachedTip) await loadNewer();
@@ -176,17 +263,8 @@ async function loadNewer() {
if (loadingNewer || newestHeight === -1 || reachedTip) return;
loadingNewer = true;
try {
const anchor = blocksEl.lastElementChild;
const anchorRect = anchor?.getBoundingClientRect();
const blocks = await brk.getBlocksV1FromHeight(newestHeight + LOOKAHEAD);
if (appendNewerBlocks(blocks)) {
if (anchor && anchorRect) {
const r = anchor.getBoundingClientRect();
chain.scrollTop += r.top - anchorRect.top;
chain.scrollLeft += r.left - anchorRect.left;
}
} else {
if (!appendNewerBlocks(blocks)) {
reachedTip = true;
}
} catch (e) {
@@ -195,23 +273,32 @@ async function loadNewer() {
loadingNewer = false;
}
/** @param {HTMLDivElement} cube */
function selectCube(cube) {
/** @param {HTMLDivElement} cube @param {{ pushUrl?: boolean, scroll?: boolean }} [opts] */
function selectCube(cube, { pushUrl = false, scroll = false } = {}) {
if (cube === selectedCube) return;
if (selectedCube) selectedCube.classList.remove("selected");
selectedCube = cube;
if (cube) {
cube.classList.add("selected");
if (scroll) cube.scrollIntoView({ behavior: "smooth" });
const hash = cube.dataset.hash;
if (hash) updateDetails(blocksByHash.get(hash));
if (hash) {
updateDetails(blocksByHash.get(hash));
if (pushUrl) history.pushState(null, "", `/block/${hash}`);
}
}
}
/** @typedef {[string, (b: BlockInfoV1) => string | null]} RowDef */
/** @typedef {[string, (b: BlockInfoV1) => string | null, ((b: BlockInfoV1) => string | null)?]} RowDef */
/** @type {RowDef[]} */
const ROW_DEFS = [
["Hash", (b) => b.id],
["Previous Hash", (b) => b.previousblockhash],
["Hash", (b) => b.id, (b) => `/block/${b.id}`],
[
"Previous Hash",
(b) => b.previousblockhash,
(b) => `/block/${b.previousblockhash}`,
],
["Merkle Root", (b) => b.merkleRoot],
["Timestamp", (b) => new Date(b.timestamp * 1000).toUTCString()],
["Median Time", (b) => new Date(b.mediantime * 1000).toUTCString()],
@@ -227,25 +314,77 @@ const ROW_DEFS = [
["Pool ID", (b) => b.extras?.pool.id.toString() ?? null],
["Pool Slug", (b) => b.extras?.pool.slug ?? null],
["Miner Names", (b) => b.extras?.pool.minerNames?.join(", ") || null],
["Reward", (b) => (b.extras ? `${(b.extras.reward / 1e8).toFixed(8)} BTC` : null)],
["Total Fees", (b) => (b.extras ? `${(b.extras.totalFees / 1e8).toFixed(8)} BTC` : null)],
["Median Fee Rate", (b) => (b.extras ? `${formatFeeRate(b.extras.medianFee)} sat/vB` : null)],
["Avg Fee Rate", (b) => (b.extras ? `${formatFeeRate(b.extras.avgFeeRate)} sat/vB` : null)],
["Avg Fee", (b) => (b.extras ? `${b.extras.avgFee.toLocaleString()} sat` : null)],
["Median Fee", (b) => (b.extras ? `${b.extras.medianFeeAmt.toLocaleString()} sat` : null)],
["Fee Range", (b) => (b.extras ? b.extras.feeRange.map((f) => formatFeeRate(f)).join(", ") + " sat/vB" : null)],
["Fee Percentiles", (b) => (b.extras ? b.extras.feePercentiles.map((f) => f.toLocaleString()).join(", ") + " sat" : null)],
["Avg Tx Size", (b) => (b.extras ? `${b.extras.avgTxSize.toLocaleString()} B` : null)],
["Virtual Size", (b) => (b.extras ? `${b.extras.virtualSize.toLocaleString()} vB` : null)],
[
"Reward",
(b) => (b.extras ? `${(b.extras.reward / 1e8).toFixed(8)} BTC` : null),
],
[
"Total Fees",
(b) => (b.extras ? `${(b.extras.totalFees / 1e8).toFixed(8)} BTC` : null),
],
[
"Median Fee Rate",
(b) => (b.extras ? `${formatFeeRate(b.extras.medianFee)} sat/vB` : null),
],
[
"Avg Fee Rate",
(b) => (b.extras ? `${formatFeeRate(b.extras.avgFeeRate)} sat/vB` : null),
],
[
"Avg Fee",
(b) => (b.extras ? `${b.extras.avgFee.toLocaleString()} sat` : null),
],
[
"Median Fee",
(b) => (b.extras ? `${b.extras.medianFeeAmt.toLocaleString()} sat` : null),
],
[
"Fee Range",
(b) =>
b.extras
? b.extras.feeRange.map((f) => formatFeeRate(f)).join(", ") + " sat/vB"
: null,
],
[
"Fee Percentiles",
(b) =>
b.extras
? b.extras.feePercentiles.map((f) => f.toLocaleString()).join(", ") +
" sat"
: null,
],
[
"Avg Tx Size",
(b) => (b.extras ? `${b.extras.avgTxSize.toLocaleString()} B` : null),
],
[
"Virtual Size",
(b) => (b.extras ? `${b.extras.virtualSize.toLocaleString()} vB` : null),
],
["Inputs", (b) => b.extras?.totalInputs.toLocaleString() ?? null],
["Outputs", (b) => b.extras?.totalOutputs.toLocaleString() ?? null],
["Total Input Amount", (b) => (b.extras ? `${(b.extras.totalInputAmt / 1e8).toFixed(8)} BTC` : null)],
["Total Output Amount", (b) => (b.extras ? `${(b.extras.totalOutputAmt / 1e8).toFixed(8)} BTC` : null)],
[
"Total Input Amount",
(b) =>
b.extras ? `${(b.extras.totalInputAmt / 1e8).toFixed(8)} BTC` : null,
],
[
"Total Output Amount",
(b) =>
b.extras ? `${(b.extras.totalOutputAmt / 1e8).toFixed(8)} BTC` : null,
],
["UTXO Set Change", (b) => b.extras?.utxoSetChange.toLocaleString() ?? null],
["UTXO Set Size", (b) => b.extras?.utxoSetSize.toLocaleString() ?? null],
["SegWit Txs", (b) => b.extras?.segwitTotalTxs.toLocaleString() ?? null],
["SegWit Size", (b) => (b.extras ? `${b.extras.segwitTotalSize.toLocaleString()} B` : null)],
["SegWit Weight", (b) => (b.extras ? `${b.extras.segwitTotalWeight.toLocaleString()} WU` : null)],
[
"SegWit Size",
(b) => (b.extras ? `${b.extras.segwitTotalSize.toLocaleString()} B` : null),
],
[
"SegWit Weight",
(b) =>
b.extras ? `${b.extras.segwitTotalWeight.toLocaleString()} WU` : null,
],
["Coinbase Address", (b) => b.extras?.coinbaseAddress || null],
["Coinbase Addresses", (b) => b.extras?.coinbaseAddresses.join(", ") || null],
["Coinbase Raw", (b) => b.extras?.coinbaseRaw ?? null],
@@ -254,7 +393,27 @@ const ROW_DEFS = [
["Header", (b) => b.extras?.header ?? null],
];
function initDetails() {
/** @param {MouseEvent} e */
function handleLinkClick(e) {
const a = /** @type {HTMLAnchorElement | null} */ (
/** @type {HTMLElement} */ (e.target).closest("a[href]")
);
if (!a) return;
const m = a.pathname.match(/^\/(block|tx|address)\/(.+)/);
if (!m) return;
e.preventDefault();
if (m[1] === "block") {
navigateToBlock(m[2]);
} else if (m[1] === "tx") {
history.pushState(null, "", a.href);
showTxDetail(m[2]);
} else {
history.pushState(null, "", a.href);
showAddrDetail(m[2]);
}
}
function initBlockDetails() {
const title = document.createElement("h1");
title.textContent = "Block ";
const code = document.createElement("code");
@@ -266,24 +425,26 @@ function initDetails() {
container.append(heightPrefix, heightNum);
code.append(container);
title.append(code);
details.append(title);
blockDetails.append(title);
detailRows = ROW_DEFS.map(([label]) => {
blockDetails.addEventListener("click", handleLinkClick);
detailRows = ROW_DEFS.map(([label, , linkFn]) => {
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("span");
const valueEl = document.createElement(linkFn ? "a" : "span");
valueEl.classList.add("value");
row.append(labelEl, valueEl);
details.append(row);
blockDetails.append(row);
return { row, valueEl };
});
txSection = document.createElement("div");
txSection.classList.add("transactions");
details.append(txSection);
blockDetails.append(txSection);
const txHeader = document.createElement("div");
txHeader.classList.add("tx-header");
@@ -297,7 +458,9 @@ function initDetails() {
txSection.append(txList, createTxNav());
txObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && txPage === -1) loadTxPage(0);
if (entries[0].isIntersecting && !txLoaded) {
loadTxPage(txPageParam.value, false);
}
});
txObserver.observe(txSection);
}
@@ -317,8 +480,8 @@ function createTxNav() {
last.textContent = "\u00BB";
nav.append(first, prev, label, next, last);
first.addEventListener("click", () => loadTxPage(0));
prev.addEventListener("click", () => loadTxPage(txPage - 1));
next.addEventListener("click", () => loadTxPage(txPage + 1));
prev.addEventListener("click", () => loadTxPage(txPageParam.value - 1));
next.addEventListener("click", () => loadTxPage(txPageParam.value + 1));
last.addEventListener("click", () => loadTxPage(txTotalPages - 1));
txNavs.push({ first, prev, label, next, last });
return nav;
@@ -340,17 +503,21 @@ function updateTxNavs(page) {
/** @param {BlockInfoV1 | undefined} block */
function updateDetails(block) {
if (!block) return;
details.scrollTop = 0;
blockDetails.hidden = false;
txDetails.hidden = true;
blockDetails.scrollTop = 0;
const str = block.height.toString();
heightPrefix.textContent = "#" + "0".repeat(7 - str.length);
heightNum.textContent = str;
ROW_DEFS.forEach(([, getter], i) => {
ROW_DEFS.forEach(([, getter, linkFn], i) => {
const value = getter(block);
const { row, valueEl } = detailRows[i];
if (value !== null) {
valueEl.textContent = value;
if (linkFn)
/** @type {HTMLAnchorElement} */ (valueEl).href = linkFn(block) ?? "";
row.hidden = false;
} else {
row.hidden = true;
@@ -359,18 +526,209 @@ function updateDetails(block) {
txBlock = block;
txTotalPages = Math.ceil(block.txCount / TX_PAGE_SIZE);
txPage = -1;
updateTxNavs(0);
if (txLoaded) txPageParam.setImmediate(0);
txLoaded = false;
updateTxNavs(txPageParam.value);
txList.innerHTML = "";
txObserver.disconnect();
txObserver.observe(txSection);
}
/** @param {number} page */
async function loadTxPage(page) {
function initTxDetails() {
txDetails.addEventListener("click", handleLinkClick);
}
/** @param {string} txid */
async function showTxDetail(txid) {
try {
const tx = await brk.getTx(txid);
if (tx.status?.blockHash) {
const cube = /** @type {HTMLDivElement | null} */ (
blocksEl.querySelector(`[data-hash="${tx.status.blockHash}"]`)
);
if (cube) {
selectCube(cube, { scroll: true });
showTxFromData(tx);
return;
}
pendingTx = tx;
resetExplorer();
return;
}
showTxFromData(tx);
} catch (e) {
console.error("explorer tx:", e);
}
}
/** @param {Transaction} tx */
function showTxFromData(tx) {
blockDetails.hidden = true;
txDetails.hidden = false;
txDetails.scrollTop = 0;
txDetails.innerHTML = "";
const title = document.createElement("h1");
title.textContent = "Transaction";
txDetails.append(title);
const vsize = Math.ceil(tx.weight / 4);
const feeRate = vsize > 0 ? tx.fee / vsize : 0;
const totalIn = tx.vin.reduce((s, v) => s + (v.prevout?.value ?? 0), 0);
const totalOut = tx.vout.reduce((s, v) => s + v.value, 0);
/** @type {[string, string, (string | null)?][]} */
const rows = [
["TXID", tx.txid],
[
"Status",
tx.status?.confirmed
? `Confirmed (block ${tx.status.blockHeight?.toLocaleString()})`
: "Unconfirmed",
tx.status?.blockHash ? `/block/${tx.status.blockHash}` : null,
],
[
"Timestamp",
tx.status?.blockTime
? new Date(tx.status.blockTime * 1000).toUTCString()
: "Pending",
],
["Size", `${tx.size.toLocaleString()} B`],
["Virtual Size", `${vsize.toLocaleString()} vB`],
["Weight", `${tx.weight.toLocaleString()} WU`],
["Fee", `${tx.fee.toLocaleString()} sat`],
["Fee Rate", `${formatFeeRate(feeRate)} sat/vB`],
["Inputs", `${tx.vin.length}`],
["Outputs", `${tx.vout.length}`],
["Total Input", `${formatBtc(totalIn)} BTC`],
["Total Output", `${formatBtc(totalOut)} BTC`],
["Version", `${tx.version}`],
["Locktime", `${tx.locktime}`],
];
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);
txDetails.append(row);
}
const section = document.createElement("div");
section.classList.add("transactions");
const heading = document.createElement("h2");
heading.textContent = "Inputs & Outputs";
section.append(heading);
section.append(renderTx(tx));
txDetails.append(section);
}
/** @param {string} address */
async function showAddrDetail(address) {
blockDetails.hidden = true;
txDetails.hidden = false;
txDetails.scrollTop = 0;
txDetails.innerHTML = "";
try {
const stats = await brk.getAddress(address);
const chain = stats.chainStats;
const title = document.createElement("h1");
title.textContent = "Address";
txDetails.append(title);
const addrEl = document.createElement("div");
addrEl.classList.add("row");
const addrLabel = document.createElement("span");
addrLabel.classList.add("label");
addrLabel.textContent = "Address";
const addrValue = document.createElement("span");
addrValue.classList.add("value");
addrValue.textContent = address;
addrEl.append(addrLabel, addrValue);
txDetails.append(addrEl);
const balance = chain.fundedTxoSum - chain.spentTxoSum;
/** @type {[string, string][]} */
const rows = [
["Balance", `${formatBtc(balance)} BTC`],
["Total Received", `${formatBtc(chain.fundedTxoSum)} BTC`],
["Total Sent", `${formatBtc(chain.spentTxoSum)} BTC`],
["Tx Count", chain.txCount.toLocaleString()],
["Funded Outputs", chain.fundedTxoCount.toLocaleString()],
["Spent Outputs", chain.spentTxoCount.toLocaleString()],
];
for (const [label, value] 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("span");
valueEl.classList.add("value");
valueEl.textContent = value;
row.append(labelEl, valueEl);
txDetails.append(row);
}
const section = document.createElement("div");
section.classList.add("transactions");
const heading = document.createElement("h2");
heading.textContent = "Transactions";
section.append(heading);
txDetails.append(section);
let loadingAddr = false;
let addrTxCount = 0;
/** @type {string | undefined} */
let afterTxid;
const addrTxObserver = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && !loadingAddr && addrTxCount < chain.txCount)
loadMore();
});
async function loadMore() {
loadingAddr = true;
try {
const txs = await brk.getAddressTxs(address, afterTxid);
for (const tx of txs) section.append(renderTx(tx));
addrTxCount += txs.length;
if (txs.length) {
afterTxid = txs[txs.length - 1].txid;
addrTxObserver.disconnect();
const last = section.lastElementChild;
if (last) addrTxObserver.observe(last);
}
} catch (e) {
console.error("explorer addr txs:", e);
addrTxCount = chain.txCount;
}
loadingAddr = false;
}
await loadMore();
} catch (e) {
console.error("explorer addr:", e);
txDetails.textContent = "Address not found";
}
}
/** @param {number} page @param {boolean} [pushUrl] */
async function loadTxPage(page, pushUrl = true) {
if (txLoading || !txBlock || page < 0 || page >= txTotalPages) return;
txLoading = true;
txPage = page;
txLoaded = true;
if (pushUrl) txPageParam.setImmediate(page);
updateTxNavs(page);
try {
const txs = await brk.getBlockTxsFromIndex(txBlock.id, page * TX_PAGE_SIZE);
@@ -389,9 +747,10 @@ function renderTx(tx) {
const head = document.createElement("div");
head.classList.add("tx-head");
const txidEl = document.createElement("span");
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");
@@ -414,9 +773,25 @@ function renderTx(tx) {
if (vin.isCoinbase) {
addr.textContent = "Coinbase";
addr.classList.add("coinbase");
const ascii = txBlock?.extras?.coinbaseSignatureAscii;
if (ascii) {
const sig = document.createElement("span");
sig.classList.add("coinbase-sig");
sig.textContent = ascii;
row.append(sig);
}
} else {
const a = /** @type {string | undefined} */ (/** @type {any} */ (vin.prevout)?.scriptpubkey_address);
setAddrContent(a || "Unknown", addr);
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");
@@ -434,13 +809,22 @@ function renderTx(tx) {
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);
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(a || vout.scriptpubkey, addr);
setAddrContent(vout.scriptpubkey, addr);
}
const amt = document.createElement("span");
amt.classList.add("amount");
@@ -516,7 +900,9 @@ function createBlockCube(block) {
cubeElement.dataset.hash = block.id;
blocksByHash.set(block.id, block);
cubeElement.addEventListener("click", () => selectCube(cubeElement));
cubeElement.addEventListener("click", () =>
selectCube(cubeElement, { pushUrl: true }),
);
const heightEl = document.createElement("p");
heightEl.append(createHeightElement(block.height));