mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-19 15:08:12 -07:00
global: opreturn part 3
This commit is contained in:
@@ -1,29 +1,4 @@
|
||||
import { brk } from "../../../utils/client.js";
|
||||
import {
|
||||
FILTERS,
|
||||
TYPE_BITS,
|
||||
TYPE_FILTER_MASK,
|
||||
getFilterBit,
|
||||
} from "./filters/model.js";
|
||||
|
||||
const VERSION_BITS = [
|
||||
0,
|
||||
getFilterBit("version:1"),
|
||||
getFilterBit("version:2"),
|
||||
getFilterBit("version:3"),
|
||||
];
|
||||
|
||||
const RBF_YES_BIT = getFilterBit("rbf:yes");
|
||||
const RBF_NO_BIT = getFilterBit("rbf:no");
|
||||
const INPUT_ONE_BIT = getFilterBit("input:one");
|
||||
const INPUT_MULTI_BIT = getFilterBit("input:multi");
|
||||
const OUTPUT_ONE_BIT = getFilterBit("output:one");
|
||||
const OUTPUT_MULTI_BIT = getFilterBit("output:multi");
|
||||
const FILTER_INDEX_BY_BIT = FILTERS.reduce((indexes, { bit, index }) => {
|
||||
indexes[bit] = index;
|
||||
|
||||
return indexes;
|
||||
}, /** @type {Record<number, number>} */ ({}));
|
||||
|
||||
/**
|
||||
* @template T
|
||||
@@ -76,13 +51,14 @@ export async function loadBlockPreview(block, signal) {
|
||||
const { start, end } = await loadBlockPreviewRange(block, signal);
|
||||
const tx = brk.series.transactions;
|
||||
const [weights, feeRates] = await Promise.all([
|
||||
fetchSeriesSlice(tx.size.weight.txIndex.by.tx_index, start, end, signal),
|
||||
fetchSeriesSlice(tx.raw.weight.by.tx_index, start, end, signal),
|
||||
fetchSeriesSlice(tx.fees.feeRate.by.tx_index, start, end, signal),
|
||||
]);
|
||||
|
||||
signal.throwIfAborted();
|
||||
|
||||
return {
|
||||
blockWeight: block.weight,
|
||||
range: { start, end },
|
||||
feeRates: /** @type {number[]} */ (feeRates.data),
|
||||
weights: /** @type {number[]} */ (weights.data),
|
||||
@@ -105,130 +81,6 @@ export async function loadBlockPreviewTxid(txIndex, signal) {
|
||||
return /** @type {string} */ (txid);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number[]} starts
|
||||
* @param {number[]} counts
|
||||
*/
|
||||
function rangeEnd(starts, counts) {
|
||||
const last = starts.length - 1;
|
||||
|
||||
return starts[last] + counts[last];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Uint32Array} counts
|
||||
* @param {number} mask
|
||||
*/
|
||||
function countMask(counts, mask) {
|
||||
while (mask) {
|
||||
const bit = mask & -mask;
|
||||
|
||||
counts[FILTER_INDEX_BY_BIT[bit]] += 1;
|
||||
mask ^= bit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {readonly string[]} types
|
||||
* @param {number} start
|
||||
* @param {number} end
|
||||
*/
|
||||
function getTypesMask(types, start, end) {
|
||||
let mask = 0;
|
||||
|
||||
for (let index = start; index < end; index += 1) {
|
||||
mask |= TYPE_BITS[/** @type {keyof typeof TYPE_BITS} */ (types[index])] ?? 0;
|
||||
if (mask === TYPE_FILTER_MASK) return mask;
|
||||
}
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewRange} range
|
||||
* @param {AbortSignal} signal
|
||||
*/
|
||||
export async function loadBlockPreviewFilters(range, signal) {
|
||||
const { start, end } = range;
|
||||
const tx = brk.series.transactions;
|
||||
const indexes = brk.series.indexes.txIndex;
|
||||
const [
|
||||
versions,
|
||||
rbfs,
|
||||
inputCounts,
|
||||
outputCounts,
|
||||
firstInputs,
|
||||
firstOutputs,
|
||||
] = await Promise.all([
|
||||
fetchSeriesSlice(tx.raw.txVersion.by.tx_index, start, end, signal),
|
||||
fetchSeriesSlice(tx.raw.isExplicitlyRbf.by.tx_index, start, end, signal),
|
||||
fetchSeriesSlice(indexes.inputCount.by.tx_index, start, end, signal),
|
||||
fetchSeriesSlice(indexes.outputCount.by.tx_index, start, end, signal),
|
||||
fetchSeriesSlice(tx.raw.firstTxinIndex.by.tx_index, start, end, signal),
|
||||
fetchSeriesSlice(tx.raw.firstTxoutIndex.by.tx_index, start, end, signal),
|
||||
]);
|
||||
|
||||
signal.throwIfAborted();
|
||||
|
||||
const inputStart = /** @type {number} */ (firstInputs.data[0]);
|
||||
const outputStart = /** @type {number} */ (firstOutputs.data[0]);
|
||||
const [inputTypes, outputTypes] = await Promise.all([
|
||||
fetchSeriesSlice(
|
||||
brk.series.inputs.raw.outputType.by.txin_index,
|
||||
inputStart,
|
||||
rangeEnd(
|
||||
/** @type {number[]} */ (firstInputs.data),
|
||||
/** @type {number[]} */ (inputCounts.data),
|
||||
),
|
||||
signal,
|
||||
),
|
||||
fetchSeriesSlice(
|
||||
brk.series.outputs.raw.outputType.by.txout_index,
|
||||
outputStart,
|
||||
rangeEnd(
|
||||
/** @type {number[]} */ (firstOutputs.data),
|
||||
/** @type {number[]} */ (outputCounts.data),
|
||||
),
|
||||
signal,
|
||||
),
|
||||
]);
|
||||
|
||||
signal.throwIfAborted();
|
||||
|
||||
const masks = new Uint32Array(end - start);
|
||||
const counts = new Uint32Array(FILTERS.length);
|
||||
|
||||
for (let index = 0; index < versions.data.length; index += 1) {
|
||||
const version = /** @type {number} */ (versions.data[index]);
|
||||
const inputCount = /** @type {number} */ (inputCounts.data[index]);
|
||||
const outputCount = /** @type {number} */ (outputCounts.data[index]);
|
||||
const inputTypeStart = /** @type {number} */ (firstInputs.data[index]);
|
||||
const inputTypeEnd = inputTypeStart + inputCount;
|
||||
const outputTypeStart = /** @type {number} */ (firstOutputs.data[index]);
|
||||
const outputTypeEnd = outputTypeStart + outputCount;
|
||||
const mask =
|
||||
(VERSION_BITS[version] ?? 0) |
|
||||
(rbfs.data[index] ? RBF_YES_BIT : RBF_NO_BIT) |
|
||||
(inputCount === 1 ? INPUT_ONE_BIT : INPUT_MULTI_BIT) |
|
||||
(outputCount === 1 ? OUTPUT_ONE_BIT : OUTPUT_MULTI_BIT) |
|
||||
getTypesMask(
|
||||
/** @type {string[]} */ (inputTypes.data),
|
||||
inputTypeStart - inputStart,
|
||||
inputTypeEnd - inputStart,
|
||||
) |
|
||||
getTypesMask(
|
||||
/** @type {string[]} */ (outputTypes.data),
|
||||
outputTypeStart - outputStart,
|
||||
outputTypeEnd - outputStart,
|
||||
);
|
||||
|
||||
masks[index] = mask;
|
||||
countMask(counts, mask);
|
||||
}
|
||||
|
||||
return { counts, masks, start };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreviewRange
|
||||
* @property {number} start
|
||||
@@ -237,6 +89,7 @@ export async function loadBlockPreviewFilters(range, signal) {
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreviewData
|
||||
* @property {number} blockWeight
|
||||
* @property {BlockPreviewRange} range
|
||||
* @property {number[]} weights
|
||||
* @property {number[]} feeRates
|
||||
@@ -248,10 +101,3 @@ export async function loadBlockPreviewFilters(range, signal) {
|
||||
* @property {number} weight
|
||||
* @property {number} feeRate
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreviewFilterState
|
||||
* @property {number} start
|
||||
* @property {Uint32Array} masks
|
||||
* @property {Uint32Array} counts
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import { brk } from "../../../../utils/client.js";
|
||||
import { FILTERS } from "./model.js";
|
||||
import {
|
||||
createOpReturnFilterData,
|
||||
OP_RETURN_COUNT_SOURCES,
|
||||
} from "./op-return/data.js";
|
||||
|
||||
const tx = brk.series.transactions;
|
||||
const txIndexes = brk.series.indexes.txIndex;
|
||||
|
||||
/** @param {unknown} expected */
|
||||
const equals = (expected) => (/** @type {unknown} */ value) => value === expected;
|
||||
|
||||
/** @param {unknown} expected */
|
||||
const differs = (expected) => (/** @type {unknown} */ value) => value !== expected;
|
||||
|
||||
/** @param {unknown} value */
|
||||
const isOtherVersion = (value) => value !== 1 && value !== 2 && value !== 3;
|
||||
|
||||
const COUNT_SOURCES = [
|
||||
{ key: "version:1", series: tx.features.count.v1 },
|
||||
{ key: "version:2", series: tx.features.count.v2 },
|
||||
{ key: "version:3", series: tx.features.count.v3 },
|
||||
{ key: "version:other", series: tx.features.count.otherVersion },
|
||||
{ key: "rbf:yes", series: tx.features.count.explicitlyRbf },
|
||||
{ key: "input:one", series: tx.features.count.oneInput },
|
||||
{ key: "output:one", series: tx.features.count.oneOutput },
|
||||
{ key: "type:p2pk", series: tx.features.count.p2pk },
|
||||
{ key: "type:p2pkh", series: tx.features.count.p2pkh },
|
||||
{ key: "type:p2sh", series: tx.features.count.p2sh },
|
||||
{ key: "type:p2wpkh", series: tx.features.count.p2wpkh },
|
||||
{ key: "type:p2wsh", series: tx.features.count.p2wsh },
|
||||
{ key: "type:taproot", series: tx.features.count.p2tr },
|
||||
{ key: "type:p2a", series: tx.features.count.p2a },
|
||||
{ key: "type:multisig", series: tx.features.count.p2ms },
|
||||
{ key: "type:op_return", series: tx.features.count.opReturn },
|
||||
{ key: "type:empty", series: tx.features.count.empty },
|
||||
{ key: "type:unknown", series: tx.features.count.unknown },
|
||||
{ key: "behavior:cpfp_parent", series: tx.fees.count.cpfpParent },
|
||||
{ key: "behavior:cpfp_child", series: tx.fees.count.cpfpChild },
|
||||
{ key: "behavior:coinjoin", series: tx.patterns.count.coinjoin },
|
||||
{ key: "behavior:consolidation", series: tx.patterns.count.consolidation },
|
||||
{ key: "behavior:batch", series: tx.patterns.count.batchPayout },
|
||||
{ key: "data:fake_pubkey", series: tx.features.count.fakePubkey },
|
||||
{ key: "data:fake_scripthash", series: tx.features.count.fakeScripthash },
|
||||
{ key: "data:inscription", series: tx.features.count.inscription },
|
||||
{ key: "data:annex", series: tx.features.count.annex },
|
||||
{ key: "data:dust", series: tx.features.count.dustOutput },
|
||||
{ key: "sighash:all", series: tx.features.count.sighashAll },
|
||||
{ key: "sighash:none", series: tx.features.count.sighashNone },
|
||||
{ key: "sighash:single", series: tx.features.count.sighashSingle },
|
||||
{ key: "sighash:default", series: tx.features.count.sighashDefault },
|
||||
{
|
||||
key: "sighash:anyone_can_pay",
|
||||
series: tx.features.count.sighashAnyoneCanPay,
|
||||
},
|
||||
{ key: "policy:nonstandard", series: tx.policy.count },
|
||||
...OP_RETURN_COUNT_SOURCES,
|
||||
];
|
||||
|
||||
const MEMBERSHIP_SOURCES = /** @type {Map<string, BlockPreviewFilterSource>} */ (new Map([
|
||||
["version:1", { matches: equals(1), series: tx.raw.txVersion }],
|
||||
["version:2", { matches: equals(2), series: tx.raw.txVersion }],
|
||||
["version:3", { matches: equals(3), series: tx.raw.txVersion }],
|
||||
["version:other", { matches: isOtherVersion, series: tx.raw.txVersion }],
|
||||
["rbf:yes", { matches: Boolean, series: tx.raw.isExplicitlyRbf }],
|
||||
["rbf:no", { matches: equals(false), series: tx.raw.isExplicitlyRbf }],
|
||||
["input:one", { matches: equals(1), series: txIndexes.inputCount }],
|
||||
["input:multi", { matches: differs(1), series: txIndexes.inputCount }],
|
||||
["output:one", { matches: equals(1), series: txIndexes.outputCount }],
|
||||
["output:multi", { matches: differs(1), series: txIndexes.outputCount }],
|
||||
["type:p2pk", { matches: Boolean, series: tx.features.hasP2pk }],
|
||||
["type:p2pkh", { matches: Boolean, series: tx.features.hasP2pkh }],
|
||||
["type:p2sh", { matches: Boolean, series: tx.features.hasP2sh }],
|
||||
["type:p2wpkh", { matches: Boolean, series: tx.features.hasP2wpkh }],
|
||||
["type:p2wsh", { matches: Boolean, series: tx.features.hasP2wsh }],
|
||||
["type:taproot", { matches: Boolean, series: tx.features.hasP2tr }],
|
||||
["type:p2a", { matches: Boolean, series: tx.features.hasP2a }],
|
||||
["type:multisig", { matches: Boolean, series: tx.features.hasP2ms }],
|
||||
["type:op_return", { matches: Boolean, series: tx.features.hasOpReturn }],
|
||||
["type:empty", { matches: Boolean, series: tx.features.hasEmpty }],
|
||||
["type:unknown", { matches: Boolean, series: tx.features.hasUnknown }],
|
||||
[
|
||||
"behavior:cpfp_parent",
|
||||
{ matches: Boolean, series: tx.fees.isCpfpParent },
|
||||
],
|
||||
[
|
||||
"behavior:cpfp_child",
|
||||
{ matches: Boolean, series: tx.fees.isCpfpChild },
|
||||
],
|
||||
["behavior:coinjoin", { matches: Boolean, series: tx.patterns.isCoinjoin }],
|
||||
[
|
||||
"behavior:consolidation",
|
||||
{ matches: Boolean, series: tx.patterns.isConsolidation },
|
||||
],
|
||||
["behavior:batch", { matches: Boolean, series: tx.patterns.isBatchPayout }],
|
||||
["data:fake_pubkey", { matches: Boolean, series: tx.features.hasFakePubkey }],
|
||||
[
|
||||
"data:fake_scripthash",
|
||||
{ matches: Boolean, series: tx.features.hasFakeScripthash },
|
||||
],
|
||||
["data:inscription", { matches: Boolean, series: tx.features.hasInscription }],
|
||||
["data:annex", { matches: Boolean, series: tx.features.hasAnnex }],
|
||||
["data:dust", { matches: Boolean, series: tx.features.hasDustOutput }],
|
||||
["sighash:all", { matches: Boolean, series: tx.features.hasSighashAll }],
|
||||
["sighash:none", { matches: Boolean, series: tx.features.hasSighashNone }],
|
||||
["sighash:single", { matches: Boolean, series: tx.features.hasSighashSingle }],
|
||||
["sighash:default", { matches: Boolean, series: tx.features.hasSighashDefault }],
|
||||
[
|
||||
"sighash:anyone_can_pay",
|
||||
{ matches: Boolean, series: tx.features.hasSighashAnyoneCanPay },
|
||||
],
|
||||
["policy:nonstandard", { matches: Boolean, series: tx.policy.isNonstandard }],
|
||||
]));
|
||||
|
||||
const FILTERS_BY_KEY = /** @type {Map<string, BlockPreviewFilter>} */ (
|
||||
new Map(FILTERS.map((filter) => [filter.key, filter]))
|
||||
);
|
||||
const INSPECT_SERIES = [...new Map(
|
||||
[...MEMBERSHIP_SOURCES.values()].map(({ series }) => [series.name, series]),
|
||||
).values()];
|
||||
|
||||
/** @param {string} key */
|
||||
function filterIndex(key) {
|
||||
return /** @type {BlockPreviewFilter} */ (FILTERS_BY_KEY.get(key)).index;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} height
|
||||
* @param {BlockPreviewRange} range
|
||||
* @param {AbortSignal} signal
|
||||
*/
|
||||
export function createBlockPreviewFilterData(height, range, signal) {
|
||||
const opReturn = createOpReturnFilterData(height, range, signal);
|
||||
const sourcePromises = new Map();
|
||||
const membershipPromises = new Map();
|
||||
let countsPromise = /** @type {Promise<Uint32Array> | null} */ (null);
|
||||
|
||||
async function fetchCounts() {
|
||||
const response = await Promise.all(COUNT_SOURCES.map(({ series }) => {
|
||||
return series.by.height.get(height).fetch({ signal });
|
||||
}));
|
||||
const counts = new Uint32Array(FILTERS.length);
|
||||
|
||||
for (let index = 0; index < COUNT_SOURCES.length; index += 1) {
|
||||
counts[filterIndex(COUNT_SOURCES[index].key)] = Number(response[index].data[0]);
|
||||
}
|
||||
|
||||
const txCount = range.end - range.start;
|
||||
counts[filterIndex("rbf:no")] = txCount - counts[filterIndex("rbf:yes")];
|
||||
counts[filterIndex("input:multi")] =
|
||||
txCount - counts[filterIndex("input:one")];
|
||||
counts[filterIndex("output:multi")] =
|
||||
txCount - counts[filterIndex("output:one")];
|
||||
|
||||
signal.throwIfAborted();
|
||||
|
||||
return counts;
|
||||
}
|
||||
|
||||
function loadCounts() {
|
||||
countsPromise ??= fetchCounts();
|
||||
|
||||
return countsPromise;
|
||||
}
|
||||
|
||||
/** @param {BlockPreviewFilterSource["series"]} series */
|
||||
function loadSource(series) {
|
||||
let promise = sourcePromises.get(series.name);
|
||||
|
||||
if (promise === undefined) {
|
||||
promise = series.by.tx_index
|
||||
.skip(range.start)
|
||||
.take(range.end - range.start)
|
||||
.fetch({ signal, memCache: false })
|
||||
.then(({ data }) => data);
|
||||
sourcePromises.set(series.name, promise);
|
||||
}
|
||||
|
||||
return /** @type {Promise<unknown[]>} */ (promise);
|
||||
}
|
||||
|
||||
/** @param {BlockPreviewFilter} filter */
|
||||
function loadMembership(filter) {
|
||||
let promise = membershipPromises.get(filter.key);
|
||||
|
||||
if (promise === undefined) {
|
||||
if (filter.group === "op_return") {
|
||||
promise = opReturn.loadMembership(filter.key);
|
||||
} else {
|
||||
const source = /** @type {BlockPreviewFilterSource} */ (
|
||||
MEMBERSHIP_SOURCES.get(filter.key)
|
||||
);
|
||||
promise = loadSource(source.series).then((values) => {
|
||||
const membership = new Uint8Array(values.length);
|
||||
|
||||
for (let index = 0; index < values.length; index += 1) {
|
||||
membership[index] = Number(source.matches(values[index]));
|
||||
}
|
||||
|
||||
return membership;
|
||||
});
|
||||
}
|
||||
|
||||
membershipPromises.set(filter.key, promise);
|
||||
}
|
||||
|
||||
return /** @type {Promise<Uint8Array>} */ (promise);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} txIndex
|
||||
* @param {AbortSignal} requestSignal
|
||||
*/
|
||||
async function loadTransactionFilters(txIndex, requestSignal) {
|
||||
const response = await Promise.all(INSPECT_SERIES.map((series) => {
|
||||
return series.by.tx_index.get(txIndex).fetch({ signal: requestSignal });
|
||||
}));
|
||||
const values = new Map(INSPECT_SERIES.map((series, index) => {
|
||||
return [series.name, response[index].data[0]];
|
||||
}));
|
||||
const keys = /** @type {string[]} */ ([]);
|
||||
|
||||
for (const [key, source] of MEMBERSHIP_SOURCES) {
|
||||
if (source.matches(values.get(source.series.name))) keys.push(key);
|
||||
}
|
||||
|
||||
if (keys.includes("type:op_return")) {
|
||||
keys.push(...await opReturn.loadTransactionKeys(txIndex));
|
||||
}
|
||||
|
||||
requestSignal.throwIfAborted();
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
return /** @type {const} */ ({
|
||||
loadCounts,
|
||||
loadMembership,
|
||||
loadTransactionFilters,
|
||||
});
|
||||
}
|
||||
|
||||
/** @typedef {import("../data.js").BlockPreviewRange} BlockPreviewRange */
|
||||
/** @typedef {import("./model.js").BlockPreviewFilter} BlockPreviewFilter */
|
||||
/** @typedef {ReturnType<typeof createBlockPreviewFilterData>} BlockPreviewFilterData */
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreviewFilterSource
|
||||
* @property {(value: any) => boolean} matches
|
||||
* @property {{
|
||||
* name: string,
|
||||
* by: {
|
||||
* tx_index: {
|
||||
* get: (index: number) => {
|
||||
* fetch: (options: { signal: AbortSignal }) => Promise<{ data: unknown[] }>,
|
||||
* },
|
||||
* skip: (start: number) => {
|
||||
* take: (count: number) => {
|
||||
* fetch: (options: { signal: AbortSignal, memCache: false }) => Promise<{ data: unknown[] }>,
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* }} series
|
||||
*/
|
||||
@@ -14,15 +14,6 @@ function setActive(button, active) {
|
||||
button.toggleAttribute("data-muted", !active);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLButtonElement} button
|
||||
*/
|
||||
function setPending(button) {
|
||||
button.disabled = true;
|
||||
button.setAttribute("aria-pressed", "false");
|
||||
button.removeAttribute("data-muted");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} labels
|
||||
*/
|
||||
@@ -35,12 +26,12 @@ function formatSummaryValue(labels) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} disabledMask
|
||||
* @param {Set<string>} hiddenKeys
|
||||
* @param {HTMLElement} value
|
||||
*/
|
||||
function updateSummaryValue(disabledMask, value) {
|
||||
function updateSummaryValue(hiddenKeys, value) {
|
||||
const labels = FILTERS
|
||||
.filter(({ bit }) => disabledMask & bit)
|
||||
.filter(({ key }) => hiddenKeys.has(key))
|
||||
.map(({ group, label }) => {
|
||||
return `${FILTER_GROUP_LABELS.get(group)} ${label}`;
|
||||
});
|
||||
@@ -94,109 +85,133 @@ export function createPendingPreviewFilters() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {() => Promise<BlockPreviewFilterState>} loadFilters
|
||||
* @param {BlockPreviewFilterData} data
|
||||
* @param {BlockPreviewHeatmap} heatmap
|
||||
*/
|
||||
export function createPreviewFilters(loadFilters, heatmap) {
|
||||
export function createPreviewFilters(data, heatmap) {
|
||||
const { panel, summary, summaryValue } = createFilterPanel();
|
||||
let disabledMask = 0;
|
||||
const appliedKeys = new Set();
|
||||
const hiddenKeys = new Set();
|
||||
let live = true;
|
||||
let loading = false;
|
||||
let state = /** @type {BlockPreviewFilterState | null} */ (null);
|
||||
let counts = /** @type {Uint32Array | null} */ (null);
|
||||
let previewButton = /** @type {HTMLButtonElement | null} */ (null);
|
||||
|
||||
updateSummaryValue(disabledMask, summaryValue);
|
||||
updateSummaryValue(hiddenKeys, summaryValue);
|
||||
|
||||
function resetPreview() {
|
||||
previewButton?.removeAttribute("data-preview");
|
||||
previewButton = null;
|
||||
heatmap.setPreviewMask(null);
|
||||
heatmap.setPreviewMembership(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLButtonElement} button
|
||||
* @param {number} nextMask
|
||||
* @param {BlockPreviewFilter} filter
|
||||
*/
|
||||
function preview(button, nextMask) {
|
||||
function preview(button, filter) {
|
||||
resetPreview();
|
||||
previewButton = button;
|
||||
button.dataset.preview = "";
|
||||
heatmap.setPreviewMask(nextMask);
|
||||
button.setAttribute("aria-busy", "true");
|
||||
|
||||
void data.loadMembership(filter)
|
||||
.then((membership) => {
|
||||
button.removeAttribute("aria-busy");
|
||||
if (!live || previewButton !== button) return;
|
||||
|
||||
button.dataset.preview = "";
|
||||
heatmap.setPreviewMembership(membership);
|
||||
})
|
||||
.catch((error) => {
|
||||
button.removeAttribute("aria-busy");
|
||||
if (!live) return;
|
||||
|
||||
resetPreview();
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewFilterState | null} filterState
|
||||
* @param {BlockPreviewFilter} filter
|
||||
* @param {Uint8Array} membership
|
||||
*/
|
||||
function renderFilters(filterState) {
|
||||
function syncHidden(filter, membership) {
|
||||
const hidden = hiddenKeys.has(filter.key);
|
||||
const applied = appliedKeys.has(filter.key);
|
||||
|
||||
if (hidden === applied) return;
|
||||
|
||||
heatmap.setFilterHidden(membership, hidden);
|
||||
if (hidden) appliedKeys.add(filter.key);
|
||||
else appliedKeys.delete(filter.key);
|
||||
}
|
||||
|
||||
/** @param {Uint32Array} filterCounts */
|
||||
function renderFilters(filterCounts) {
|
||||
clearGroups(panel, summary);
|
||||
const canPreview = CAN_HOVER.matches;
|
||||
|
||||
for (const { label, filters } of FILTER_GROUP_FILTERS) {
|
||||
const group = createFilterGroup(label);
|
||||
let group = /** @type {HTMLDivElement | null} */ (null);
|
||||
|
||||
for (const filter of filters) {
|
||||
const count = filterCounts[filter.index];
|
||||
if (count === 0) continue;
|
||||
|
||||
group ??= createFilterGroup(label);
|
||||
const { button, value } = createLegendItem({
|
||||
ariaLabel: filter.label,
|
||||
color: filter.color,
|
||||
label: filter.label,
|
||||
});
|
||||
|
||||
value.textContent = filterState === null
|
||||
? "..."
|
||||
: formatNumber(filterState.counts[filter.index]);
|
||||
value.textContent = formatNumber(count);
|
||||
button.addEventListener("click", () => {
|
||||
resetPreview();
|
||||
if (hiddenKeys.has(filter.key)) hiddenKeys.delete(filter.key);
|
||||
else hiddenKeys.add(filter.key);
|
||||
|
||||
if (filterState === null) {
|
||||
setPending(button);
|
||||
} else {
|
||||
const canPreview = CAN_HOVER.matches;
|
||||
setActive(button, !hiddenKeys.has(filter.key));
|
||||
updateSummaryValue(hiddenKeys, summaryValue);
|
||||
void data.loadMembership(filter)
|
||||
.then((membership) => {
|
||||
if (live) syncHidden(filter, membership);
|
||||
})
|
||||
.catch(console.error);
|
||||
});
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
const active = (disabledMask & filter.bit) === 0;
|
||||
|
||||
resetPreview();
|
||||
disabledMask = active
|
||||
? disabledMask | filter.bit
|
||||
: disabledMask & ~filter.bit;
|
||||
setActive(button, !active);
|
||||
updateSummaryValue(disabledMask, summaryValue);
|
||||
heatmap.setDisabledMask(disabledMask);
|
||||
if (canPreview) {
|
||||
button.addEventListener("pointerenter", () => {
|
||||
preview(button, filter);
|
||||
});
|
||||
|
||||
if (canPreview) {
|
||||
button.addEventListener("pointerenter", () => {
|
||||
preview(button, filter.bit);
|
||||
});
|
||||
button.addEventListener("pointerleave", resetPreview);
|
||||
}
|
||||
|
||||
button.addEventListener("focus", () => {
|
||||
preview(button, filter.bit);
|
||||
});
|
||||
button.addEventListener("blur", resetPreview);
|
||||
setActive(button, (disabledMask & filter.bit) === 0);
|
||||
button.addEventListener("pointerleave", resetPreview);
|
||||
}
|
||||
|
||||
button.addEventListener("focus", () => {
|
||||
preview(button, filter);
|
||||
});
|
||||
button.addEventListener("blur", resetPreview);
|
||||
setActive(button, !hiddenKeys.has(filter.key));
|
||||
group.append(button);
|
||||
}
|
||||
|
||||
panel.append(group);
|
||||
if (group !== null) panel.append(group);
|
||||
}
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (loading || state !== null) return;
|
||||
if (loading || counts !== null) return;
|
||||
|
||||
loading = true;
|
||||
summaryValue.textContent = "loading";
|
||||
renderFilters(null);
|
||||
void loadFilters()
|
||||
.then((nextState) => {
|
||||
void data.loadCounts()
|
||||
.then((nextCounts) => {
|
||||
if (!live) return;
|
||||
|
||||
loading = false;
|
||||
state = nextState;
|
||||
heatmap.setFilterState(nextState);
|
||||
updateSummaryValue(disabledMask, summaryValue);
|
||||
renderFilters(nextState);
|
||||
counts = nextCounts;
|
||||
updateSummaryValue(hiddenKeys, summaryValue);
|
||||
renderFilters(nextCounts);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!live) return;
|
||||
@@ -210,6 +225,7 @@ export function createPreviewFilters(loadFilters, heatmap) {
|
||||
|
||||
panel.addEventListener("toggle", () => {
|
||||
if (panel.open) load();
|
||||
else resetPreview();
|
||||
});
|
||||
|
||||
return /** @type {const} */ ({
|
||||
@@ -221,11 +237,11 @@ export function createPreviewFilters(loadFilters, heatmap) {
|
||||
});
|
||||
}
|
||||
|
||||
/** @typedef {import("../data.js").BlockPreviewFilterState} BlockPreviewFilterState */
|
||||
/** @typedef {import("./data.js").BlockPreviewFilterData} BlockPreviewFilterData */
|
||||
/** @typedef {import("./model.js").BlockPreviewFilter} BlockPreviewFilter */
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreviewHeatmap
|
||||
* @property {(mask: number | null) => void} setPreviewMask
|
||||
* @property {(mask: number) => void} setDisabledMask
|
||||
* @property {(state: BlockPreviewFilterState) => void} setFilterState
|
||||
* @property {(membership: Uint8Array | null) => void} setPreviewMembership
|
||||
* @property {(membership: Uint8Array, hidden: boolean) => void} setFilterHidden
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { txColors } from "../../../../utils/colors.js";
|
||||
import {
|
||||
OP_RETURN_KIND_FILTERS,
|
||||
OP_RETURN_POLICY_FILTERS,
|
||||
} from "./op-return/model.js";
|
||||
|
||||
export const FILTER_GROUPS = /** @type {const} */ ([
|
||||
{ key: "version", label: "version" },
|
||||
@@ -6,12 +10,18 @@ export const FILTER_GROUPS = /** @type {const} */ ([
|
||||
{ key: "input", label: "input" },
|
||||
{ key: "output", label: "output" },
|
||||
{ key: "type", label: "type" },
|
||||
{ key: "behavior", label: "behavior" },
|
||||
{ key: "data", label: "data" },
|
||||
{ key: "sighash", label: "sighash" },
|
||||
{ key: "policy", label: "policy" },
|
||||
{ key: "op_return", label: "op return" },
|
||||
]);
|
||||
|
||||
const FILTER_DEFS = /** @type {const} */ ([
|
||||
["version", "v1", "version:1", txColors.v1],
|
||||
["version", "v2", "version:2", txColors.v2],
|
||||
["version", "v3", "version:3", txColors.v3],
|
||||
["version", "other", "version:other", txColors.otherVersion],
|
||||
["rbf", "yes", "rbf:yes", txColors.rbf],
|
||||
["rbf", "no", "rbf:no", txColors.noRbf],
|
||||
["input", "1", "input:one", txColors.oneInput],
|
||||
@@ -29,10 +39,67 @@ const FILTER_DEFS = /** @type {const} */ ([
|
||||
["type", "op ret", "type:op_return", txColors.opReturn],
|
||||
["type", "empty", "type:empty", txColors.empty],
|
||||
["type", "unknown", "type:unknown", txColors.unknown],
|
||||
[
|
||||
"behavior",
|
||||
"paid by child",
|
||||
"behavior:cpfp_parent",
|
||||
txColors.behavior.cpfpParent,
|
||||
],
|
||||
[
|
||||
"behavior",
|
||||
"pays parent",
|
||||
"behavior:cpfp_child",
|
||||
txColors.behavior.cpfpChild,
|
||||
],
|
||||
["behavior", "coinjoin", "behavior:coinjoin", txColors.behavior.coinjoin],
|
||||
[
|
||||
"behavior",
|
||||
"consolidation",
|
||||
"behavior:consolidation",
|
||||
txColors.behavior.consolidation,
|
||||
],
|
||||
["behavior", "batch", "behavior:batch", txColors.behavior.batchPayout],
|
||||
["data", "fake pubkey", "data:fake_pubkey", txColors.data.fakePubkey],
|
||||
[
|
||||
"data",
|
||||
"fake scripthash",
|
||||
"data:fake_scripthash",
|
||||
txColors.data.fakeScripthash,
|
||||
],
|
||||
["data", "inscription", "data:inscription", txColors.data.inscription],
|
||||
["data", "annex", "data:annex", txColors.data.annex],
|
||||
["data", "dust", "data:dust", txColors.data.dust],
|
||||
["sighash", "all", "sighash:all", txColors.sighash.all],
|
||||
["sighash", "none", "sighash:none", txColors.sighash.none],
|
||||
["sighash", "single", "sighash:single", txColors.sighash.single],
|
||||
["sighash", "default", "sighash:default", txColors.sighash.default],
|
||||
[
|
||||
"sighash",
|
||||
"anyonecanpay",
|
||||
"sighash:anyone_can_pay",
|
||||
txColors.sighash.anyoneCanPay,
|
||||
],
|
||||
[
|
||||
"policy",
|
||||
"nonstandard",
|
||||
"policy:nonstandard",
|
||||
txColors.policy.nonstandard,
|
||||
],
|
||||
...OP_RETURN_KIND_FILTERS.map(([label, kind, , color]) => {
|
||||
return /** @type {const} */ (["op_return", label, `op_return:${kind}`, color]);
|
||||
}),
|
||||
...OP_RETURN_POLICY_FILTERS.map(([label, policy, , color]) => {
|
||||
return /** @type {const} */ ([
|
||||
"op_return",
|
||||
label,
|
||||
`op_return_policy:${policy}`,
|
||||
color,
|
||||
]);
|
||||
}),
|
||||
]);
|
||||
|
||||
export const FILTERS = FILTER_DEFS.map(([group, label, key, color], index) => {
|
||||
return /** @type {const} */ ({ bit: 1 << index, color, group, index, key, label });
|
||||
return /** @type {const} */ ({ color, group, index, key, label });
|
||||
});
|
||||
|
||||
export const FILTER_GROUP_FILTERS = FILTER_GROUPS.map((group) => {
|
||||
@@ -46,31 +113,4 @@ export const FILTER_GROUP_LABELS = new Map(FILTER_GROUPS.map(({ key, label }) =>
|
||||
return [key, label];
|
||||
}));
|
||||
|
||||
const FILTER_BITS = /** @type {Map<string, number>} */ (
|
||||
new Map(FILTERS.map(({ bit, key }) => [key, bit]))
|
||||
);
|
||||
|
||||
export const TYPE_FILTER_MASK = FILTERS
|
||||
.filter(({ group }) => group === "type")
|
||||
.reduce((mask, { bit }) => mask | bit, 0);
|
||||
|
||||
export const TYPE_BITS = /** @type {const} */ ({
|
||||
empty: getFilterBit("type:empty"),
|
||||
multisig: getFilterBit("type:multisig"),
|
||||
op_return: getFilterBit("type:op_return"),
|
||||
p2a: getFilterBit("type:p2a"),
|
||||
p2pk: getFilterBit("type:p2pk"),
|
||||
p2pkh: getFilterBit("type:p2pkh"),
|
||||
p2sh: getFilterBit("type:p2sh"),
|
||||
unknown: getFilterBit("type:unknown"),
|
||||
v0_p2wpkh: getFilterBit("type:p2wpkh"),
|
||||
v0_p2wsh: getFilterBit("type:p2wsh"),
|
||||
v1_p2tr: getFilterBit("type:taproot"),
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
*/
|
||||
export function getFilterBit(key) {
|
||||
return FILTER_BITS.get(key) ?? 0;
|
||||
}
|
||||
/** @typedef {(typeof FILTERS)[number]} BlockPreviewFilter */
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { brk } from "../../../../../utils/client.js";
|
||||
import {
|
||||
OP_RETURN_KIND_FILTERS,
|
||||
OP_RETURN_POLICY_FILTERS,
|
||||
} from "./model.js";
|
||||
|
||||
const MAX_STANDARD_BYTES = 82;
|
||||
const byKind = brk.series.opReturn.byKind;
|
||||
const policy = brk.series.opReturn.policy;
|
||||
|
||||
export const OP_RETURN_COUNT_SOURCES = [
|
||||
...OP_RETURN_KIND_FILTERS.map(([, kind, clientKey]) => {
|
||||
return {
|
||||
key: `op_return:${kind}`,
|
||||
series: byKind[clientKey].carrierTxCount.block,
|
||||
};
|
||||
}),
|
||||
...OP_RETURN_POLICY_FILTERS.map(([, filter, clientKey]) => {
|
||||
return {
|
||||
key: `op_return_policy:${filter}`,
|
||||
series: policy[clientKey].carrierTxCount.block,
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
/**
|
||||
* @param {string} policy
|
||||
* @param {number} count
|
||||
* @param {boolean} oversized
|
||||
*/
|
||||
function matchesPolicy(policy, count, oversized) {
|
||||
if (policy === "standard") return count === 1 && !oversized;
|
||||
if (policy === "oversized") return oversized;
|
||||
if (policy === "multiple") return count > 1;
|
||||
|
||||
return oversized || count > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} height
|
||||
* @param {BlockPreviewRange} range
|
||||
* @param {AbortSignal} signal
|
||||
*/
|
||||
export function createOpReturnFilterData(height, range, signal) {
|
||||
const raw = brk.series.opReturn.raw;
|
||||
let rawPromise = /** @type {Promise<OpReturnRows> | null} */ (null);
|
||||
|
||||
async function fetchRows() {
|
||||
const indexes = await raw.firstIndex.by.height
|
||||
.skip(height)
|
||||
.take(2)
|
||||
.fetch({ signal });
|
||||
const start = /** @type {number} */ (indexes.data[0]);
|
||||
const end = indexes.data.length === 2
|
||||
? /** @type {number} */ (indexes.data[1])
|
||||
: await raw.toTxIndex.by.op_return_index.len();
|
||||
|
||||
signal.throwIfAborted();
|
||||
if (start === end) return { bytes: [], kinds: [], txIndexes: [] };
|
||||
|
||||
const count = end - start;
|
||||
const [txIndexes, kinds, bytes] = await Promise.all([
|
||||
raw.toTxIndex.by.op_return_index
|
||||
.skip(start)
|
||||
.take(count)
|
||||
.fetch({ signal, memCache: false }),
|
||||
raw.kind.by.op_return_index
|
||||
.skip(start)
|
||||
.take(count)
|
||||
.fetch({ signal, memCache: false }),
|
||||
raw.postOpReturnBytes.by.op_return_index
|
||||
.skip(start)
|
||||
.take(count)
|
||||
.fetch({ signal, memCache: false }),
|
||||
]);
|
||||
|
||||
signal.throwIfAborted();
|
||||
|
||||
return {
|
||||
bytes: bytes.data,
|
||||
kinds: kinds.data,
|
||||
txIndexes: txIndexes.data,
|
||||
};
|
||||
}
|
||||
|
||||
function loadRows() {
|
||||
rawPromise ??= fetchRows();
|
||||
|
||||
return rawPromise;
|
||||
}
|
||||
|
||||
/** @param {string} key */
|
||||
async function loadMembership(key) {
|
||||
const rows = await loadRows();
|
||||
const membership = new Uint8Array(range.end - range.start);
|
||||
const separator = key.indexOf(":");
|
||||
const group = key.slice(0, separator);
|
||||
const value = key.slice(separator + 1);
|
||||
|
||||
if (group === "op_return") {
|
||||
for (let index = 0; index < rows.txIndexes.length; index += 1) {
|
||||
if (rows.kinds[index] === value) {
|
||||
membership[rows.txIndexes[index] - range.start] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
for (let index = 0; index < rows.txIndexes.length;) {
|
||||
const txIndex = rows.txIndexes[index];
|
||||
let count = 0;
|
||||
let oversized = false;
|
||||
|
||||
while (index < rows.txIndexes.length && rows.txIndexes[index] === txIndex) {
|
||||
oversized ||= rows.bytes[index] > MAX_STANDARD_BYTES;
|
||||
count += 1;
|
||||
index += 1;
|
||||
}
|
||||
|
||||
if (matchesPolicy(value, count, oversized)) {
|
||||
membership[txIndex - range.start] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
/** @param {number} txIndex */
|
||||
async function loadTransactionKeys(txIndex) {
|
||||
const rows = await loadRows();
|
||||
const keys = /** @type {string[]} */ ([]);
|
||||
const kinds = new Set();
|
||||
let count = 0;
|
||||
let oversized = false;
|
||||
|
||||
for (let index = 0; index < rows.txIndexes.length; index += 1) {
|
||||
const rowTxIndex = rows.txIndexes[index];
|
||||
|
||||
if (rowTxIndex < txIndex) continue;
|
||||
if (rowTxIndex > txIndex) break;
|
||||
|
||||
kinds.add(rows.kinds[index]);
|
||||
oversized ||= rows.bytes[index] > MAX_STANDARD_BYTES;
|
||||
count += 1;
|
||||
}
|
||||
|
||||
for (const kind of kinds) keys.push(`op_return:${kind}`);
|
||||
for (const [, filter] of OP_RETURN_POLICY_FILTERS) {
|
||||
if (matchesPolicy(filter, count, oversized)) {
|
||||
keys.push(`op_return_policy:${filter}`);
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
return /** @type {const} */ ({ loadMembership, loadTransactionKeys });
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} OpReturnRows
|
||||
* @property {number[]} bytes
|
||||
* @property {string[]} kinds
|
||||
* @property {number[]} txIndexes
|
||||
*/
|
||||
|
||||
/** @typedef {import("../../data.js").BlockPreviewRange} BlockPreviewRange */
|
||||
@@ -0,0 +1,49 @@
|
||||
import { txColors } from "../../../../../utils/colors.js";
|
||||
|
||||
export const OP_RETURN_KIND_FILTERS = /** @type {const} */ ([
|
||||
["runes", "runes", "runes", txColors.opReturnKind.runes],
|
||||
["veri block", "veri_block", "veriBlock", txColors.opReturnKind.veriBlock],
|
||||
["omni", "omni", "omni", txColors.opReturnKind.omni],
|
||||
["stacks", "stacks", "stacks", txColors.opReturnKind.stacks],
|
||||
["blockstack", "blockstack", "blockstack", txColors.opReturnKind.blockstack],
|
||||
["colu", "colu", "colu", txColors.opReturnKind.colu],
|
||||
["open assets", "open_assets", "openAssets", txColors.opReturnKind.openAssets],
|
||||
["komodo", "komodo", "komodo", txColors.opReturnKind.komodo],
|
||||
["coin spark", "coin_spark", "coinSpark", txColors.opReturnKind.coinSpark],
|
||||
["poet", "poet", "poet", txColors.opReturnKind.poet],
|
||||
["docproof", "docproof", "docproof", txColors.opReturnKind.docproof],
|
||||
[
|
||||
"open timestamps",
|
||||
"open_timestamps",
|
||||
"openTimestamps",
|
||||
txColors.opReturnKind.openTimestamps,
|
||||
],
|
||||
["factom", "factom", "factom", txColors.opReturnKind.factom],
|
||||
[
|
||||
"eternity wall",
|
||||
"eternity_wall",
|
||||
"eternityWall",
|
||||
txColors.opReturnKind.eternityWall,
|
||||
],
|
||||
["memo", "memo", "memo", txColors.opReturnKind.memo],
|
||||
["bitproof", "bitproof", "bitproof", txColors.opReturnKind.bitproof],
|
||||
["ascribe", "ascribe", "ascribe", txColors.opReturnKind.ascribe],
|
||||
["stampery", "stampery", "stampery", txColors.opReturnKind.stampery],
|
||||
["epobc", "epobc", "epobc", txColors.opReturnKind.epobc],
|
||||
["bare hash", "bare_hash", "bareHash", txColors.opReturnKind.bareHash],
|
||||
["text", "text", "text", txColors.opReturnKind.text],
|
||||
["empty", "empty", "empty", txColors.opReturnKind.empty],
|
||||
["unknown", "unknown", "unknown", txColors.opReturnKind.unknown],
|
||||
]);
|
||||
|
||||
export const OP_RETURN_POLICY_FILTERS = /** @type {const} */ ([
|
||||
["standard", "standard", "standard", txColors.opReturnPolicy.standard],
|
||||
["oversized", "oversized", "oversized", txColors.opReturnPolicy.oversized],
|
||||
["multiple", "multiple", "multiple", txColors.opReturnPolicy.multiple],
|
||||
[
|
||||
"pre-v30 nonstandard",
|
||||
"pre_v30_nonstandard",
|
||||
"preV30Nonstandard",
|
||||
txColors.opReturnPolicy.preV30Nonstandard,
|
||||
],
|
||||
]);
|
||||
@@ -1,73 +1,95 @@
|
||||
import { packCells } from "./pack.js";
|
||||
const CALIBRATION_STEPS = 24;
|
||||
|
||||
/**
|
||||
* @param {number} weight
|
||||
* @param {number} capacity
|
||||
* @param {number} columns
|
||||
* @param {Float64Array} spans
|
||||
* @param {number} scale
|
||||
*/
|
||||
function weightToSpan(weight, capacity, columns) {
|
||||
const cellWeight = capacity / (columns * columns);
|
||||
function scaledArea(spans, scale) {
|
||||
let area = 0;
|
||||
|
||||
return Math.max(1, Math.round(Math.sqrt(weight / cellWeight)));
|
||||
}
|
||||
for (const span of spans) {
|
||||
const resolved = Math.max(1, Math.round(span * scale));
|
||||
|
||||
/**
|
||||
* @template {CapacityCell} Cell
|
||||
* @param {readonly Cell[]} cells
|
||||
* @param {number} capacity
|
||||
* @param {number} columns
|
||||
* @returns {ResolvedCapacityCell<Cell>[]}
|
||||
*/
|
||||
function resolveCapacityCells(cells, capacity, columns) {
|
||||
const resolved = /** @type {ResolvedCapacityCell<Cell>[]} */ (cells);
|
||||
|
||||
for (const cell of resolved) {
|
||||
cell.span = weightToSpan(cell.weight, capacity, columns);
|
||||
area += resolved * resolved;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
return area;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {CapacityCell} Cell
|
||||
* @param {readonly Cell[]} cells
|
||||
* @param {number} blockWeight
|
||||
* @param {number} capacity
|
||||
* @param {number} columns
|
||||
*/
|
||||
function fitCapacityCells(cells, capacity, columns) {
|
||||
const resolvedCells = resolveCapacityCells(cells, capacity, columns);
|
||||
let layouts = packCells(resolvedCells, columns, columns);
|
||||
export function resolveCapacityCells(cells, blockWeight, capacity, columns) {
|
||||
const gridArea = columns * columns;
|
||||
const targetArea = Math.max(
|
||||
cells.length,
|
||||
Math.floor((gridArea * blockWeight) / capacity),
|
||||
);
|
||||
const idealSpans = new Float64Array(cells.length);
|
||||
const resolved = /** @type {ResolvedCapacityCell<Cell>[]} */ (cells);
|
||||
|
||||
while (layouts === null) {
|
||||
let largest = 0;
|
||||
for (let index = 0; index < cells.length; index += 1) {
|
||||
idealSpans[index] = Math.sqrt((cells[index].weight * gridArea) / capacity);
|
||||
}
|
||||
|
||||
for (const { span } of resolvedCells) largest = Math.max(largest, span);
|
||||
let lowerScale = 0;
|
||||
let upperScale = 1;
|
||||
|
||||
if (largest <= 1) break;
|
||||
while (scaledArea(idealSpans, upperScale) <= targetArea) {
|
||||
upperScale *= 2;
|
||||
}
|
||||
|
||||
for (const cell of resolvedCells) {
|
||||
if (cell.span === largest) cell.span -= 1;
|
||||
for (let step = 0; step < CALIBRATION_STEPS; step += 1) {
|
||||
const scale = (lowerScale + upperScale) / 2;
|
||||
|
||||
if (scaledArea(idealSpans, scale) <= targetArea) {
|
||||
lowerScale = scale;
|
||||
} else {
|
||||
upperScale = scale;
|
||||
}
|
||||
}
|
||||
|
||||
layouts = packCells(resolvedCells, columns, columns);
|
||||
let area = 0;
|
||||
let largestSpan = 1;
|
||||
|
||||
for (let index = 0; index < resolved.length; index += 1) {
|
||||
const idealSpan = idealSpans[index];
|
||||
const span = Math.max(1, Math.round(idealSpan * lowerScale));
|
||||
|
||||
resolved[index].span = span;
|
||||
idealSpans[index] = (span + 0.5) / idealSpan;
|
||||
largestSpan = Math.max(largestSpan, span);
|
||||
area += span * span;
|
||||
}
|
||||
|
||||
const candidates = Uint32Array.from(resolved, (_, index) => index);
|
||||
|
||||
candidates.sort((a, b) => {
|
||||
return idealSpans[a] - idealSpans[b] || a - b;
|
||||
});
|
||||
|
||||
for (const index of candidates) {
|
||||
const cell = resolved[index];
|
||||
const growth = 2 * cell.span + 1;
|
||||
|
||||
if (area + growth > targetArea) continue;
|
||||
|
||||
cell.span += 1;
|
||||
largestSpan = Math.max(largestSpan, cell.span);
|
||||
area += growth;
|
||||
if (targetArea - area < 3) break;
|
||||
}
|
||||
|
||||
return {
|
||||
layouts: /** @type {NonNullable<typeof layouts>} */ (layouts),
|
||||
resolvedCells,
|
||||
resolvedCells: resolved,
|
||||
rows: Math.max(largestSpan, Math.ceil(targetArea / columns)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {CapacityCell} Cell
|
||||
* @param {readonly Cell[]} cells
|
||||
* @param {number} capacity
|
||||
* @param {number} columns
|
||||
*/
|
||||
export function createSquareLayout(cells, capacity, columns) {
|
||||
return { columns, ...fitCapacityCells(cells, capacity, columns) };
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} CapacityCell
|
||||
* @property {number} weight
|
||||
|
||||
@@ -48,22 +48,28 @@ function drawHover(context, rect) {
|
||||
* @param {DrawPreviewArgs} args
|
||||
*/
|
||||
export function drawPreview(args) {
|
||||
const { context, disabledMask, filterState, inspected, previewMask, rects } = args;
|
||||
const {
|
||||
context,
|
||||
hasHidden,
|
||||
hiddenCounts,
|
||||
inspected,
|
||||
previewMembership,
|
||||
rects,
|
||||
start,
|
||||
} = args;
|
||||
let inspectedRect = /** @type {PreviewRect | null} */ (null);
|
||||
|
||||
if (filterState === null || (disabledMask === 0 && previewMask === null)) {
|
||||
if (previewMembership === null && !hasHidden) {
|
||||
for (const rect of rects) {
|
||||
drawRect(context, 1, rect.color, rect);
|
||||
if (rect.transaction === inspected) inspectedRect = rect;
|
||||
}
|
||||
} else {
|
||||
const activeMask = previewMask ?? disabledMask;
|
||||
|
||||
for (const rect of rects) {
|
||||
const mask = filterState.masks[rect.transaction.txIndex - filterState.start];
|
||||
const alpha = previewMask === null
|
||||
? (mask & activeMask ? MUTED_ALPHA : 1)
|
||||
: (mask & activeMask ? 1 : MUTED_ALPHA);
|
||||
const index = rect.transaction.txIndex - start;
|
||||
const alpha = previewMembership === null
|
||||
? (hiddenCounts[index] ? MUTED_ALPHA : 1)
|
||||
: (previewMembership[index] ? 1 : MUTED_ALPHA);
|
||||
|
||||
drawRect(context, alpha, rect.color, rect);
|
||||
if (rect.transaction === inspected) inspectedRect = rect;
|
||||
@@ -77,13 +83,13 @@ export function drawPreview(args) {
|
||||
/**
|
||||
* @typedef {Object} DrawPreviewArgs
|
||||
* @property {CanvasRenderingContext2D} context
|
||||
* @property {number} disabledMask
|
||||
* @property {BlockPreviewFilterState | null} filterState
|
||||
* @property {boolean} hasHidden
|
||||
* @property {Uint8Array} hiddenCounts
|
||||
* @property {BlockPreviewTransaction | null} inspected
|
||||
* @property {number | null} previewMask
|
||||
* @property {Uint8Array | null} previewMembership
|
||||
* @property {PreviewRect[]} rects
|
||||
* @property {number} start
|
||||
*/
|
||||
|
||||
/** @typedef {import("../data.js").BlockPreviewFilterState} BlockPreviewFilterState */
|
||||
/** @typedef {import("../data.js").BlockPreviewTransaction} BlockPreviewTransaction */
|
||||
/** @typedef {import("./geometry.js").PreviewRect} PreviewRect */
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FEE_RATE_PERCENTILES } from "../../fee-rates.js";
|
||||
import { packCells } from "./pack.js";
|
||||
|
||||
/**
|
||||
* @param {number[]} feeRates
|
||||
@@ -25,3 +26,20 @@ export function orderTransactions(weights, feeRates) {
|
||||
|
||||
return order.sort((a, b) => feeRates[b] - feeRates[a] || weights[b] - weights[a]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {{ span: number }} Cell
|
||||
* @param {readonly Cell[]} cells
|
||||
* @param {number} columns
|
||||
* @param {number} rows
|
||||
*/
|
||||
export function packTransactions(cells, columns, rows) {
|
||||
let layouts = packCells(cells, columns, rows);
|
||||
|
||||
while (layouts === null) {
|
||||
rows += 1;
|
||||
layouts = packCells(cells, columns, rows);
|
||||
}
|
||||
|
||||
return { layouts, resolvedCells: cells };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { MAX_BLOCK_WEIGHT } from "../../format.js";
|
||||
import { createPreviewFeeRange, orderTransactions } from "./fees.js";
|
||||
import { createSquareLayout } from "./capacity.js";
|
||||
import { createSquareLayout } from "./layout.js";
|
||||
import { getCanvasFeeRateColor } from "./color.js";
|
||||
import { createPreviewGeometry, hitTest } from "./geometry.js";
|
||||
import { drawPreview } from "./draw.js";
|
||||
|
||||
const COLUMNS = 84;
|
||||
const COLUMNS = 86;
|
||||
const VISIBLE_CELLS = COLUMNS * COLUMNS;
|
||||
|
||||
/**
|
||||
@@ -56,15 +56,20 @@ export function createBlockPreviewHeatmap(data, options = {}) {
|
||||
const order = orderTransactions(data.weights, data.feeRates);
|
||||
const ranges = createPreviewFeeRange(data.feeRates, order);
|
||||
const cells = createCapacityCells(data, order, ranges);
|
||||
const square = createSquareLayout(cells, MAX_BLOCK_WEIGHT, COLUMNS);
|
||||
let disabledMask = 0;
|
||||
let filterState = /** @type {BlockPreviewFilterState | null} */ (null);
|
||||
const square = createSquareLayout(
|
||||
cells,
|
||||
data.blockWeight,
|
||||
MAX_BLOCK_WEIGHT,
|
||||
COLUMNS,
|
||||
);
|
||||
const hiddenCounts = new Uint8Array(data.range.end - data.range.start);
|
||||
let hiddenFilterCount = 0;
|
||||
let frame = 0;
|
||||
let inspected = /** @type {BlockPreviewTransaction | null} */ (null);
|
||||
let inspectFrame = 0;
|
||||
let inspectPoint = /** @type {BlockPreviewPointer | null} */ (null);
|
||||
let bounds = /** @type {DOMRectReadOnly | null} */ (null);
|
||||
let previewMask = /** @type {number | null} */ (null);
|
||||
let previewMembership = /** @type {Uint8Array | null} */ (null);
|
||||
let geometry = /** @type {PreviewGeometry | null} */ (null);
|
||||
let rectWidth = 0;
|
||||
let capturedPointer = /** @type {number | null} */ (null);
|
||||
@@ -99,11 +104,12 @@ export function createBlockPreviewHeatmap(data, options = {}) {
|
||||
context.clearRect(0, 0, width, width);
|
||||
drawPreview({
|
||||
context,
|
||||
disabledMask,
|
||||
filterState,
|
||||
hasHidden: hiddenFilterCount > 0,
|
||||
hiddenCounts,
|
||||
inspected,
|
||||
previewMask,
|
||||
previewMembership,
|
||||
rects: geometry?.rects ?? [],
|
||||
start: data.range.start,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -242,23 +248,25 @@ export function createBlockPreviewHeatmap(data, options = {}) {
|
||||
window.removeEventListener("blur", clearInspect);
|
||||
observer.disconnect();
|
||||
},
|
||||
/** @param {number | null} mask */
|
||||
setPreviewMask(mask) {
|
||||
if (previewMask === mask) return;
|
||||
/** @param {Uint8Array | null} membership */
|
||||
setPreviewMembership(membership) {
|
||||
if (previewMembership === membership) return;
|
||||
|
||||
previewMask = mask;
|
||||
previewMembership = membership;
|
||||
scheduleDraw();
|
||||
},
|
||||
/** @param {number} mask */
|
||||
setDisabledMask(mask) {
|
||||
if (disabledMask === mask) return;
|
||||
/**
|
||||
* @param {Uint8Array} membership
|
||||
* @param {boolean} hidden
|
||||
*/
|
||||
setFilterHidden(membership, hidden) {
|
||||
const change = hidden ? 1 : -1;
|
||||
|
||||
hiddenFilterCount += change;
|
||||
for (let index = 0; index < hiddenCounts.length; index += 1) {
|
||||
if (membership[index]) hiddenCounts[index] += change;
|
||||
}
|
||||
|
||||
disabledMask = mask;
|
||||
scheduleDraw();
|
||||
},
|
||||
/** @param {BlockPreviewFilterState} state */
|
||||
setFilterState(state) {
|
||||
filterState = state;
|
||||
scheduleDraw();
|
||||
},
|
||||
});
|
||||
@@ -266,7 +274,6 @@ export function createBlockPreviewHeatmap(data, options = {}) {
|
||||
|
||||
/** @typedef {import("../data.js").BlockPreviewData} BlockPreviewData */
|
||||
/** @typedef {import("../data.js").BlockPreviewTransaction} BlockPreviewTransaction */
|
||||
/** @typedef {import("../data.js").BlockPreviewFilterState} BlockPreviewFilterState */
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreviewPointer
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { resolveCapacityCells } from "./capacity.js";
|
||||
import { packTransactions } from "./fees.js";
|
||||
|
||||
/**
|
||||
* @template {{ weight: number }} Cell
|
||||
* @param {readonly Cell[]} cells
|
||||
* @param {number} blockWeight
|
||||
* @param {number} capacity
|
||||
* @param {number} columns
|
||||
*/
|
||||
export function createSquareLayout(cells, blockWeight, capacity, columns) {
|
||||
const capacityLayout = resolveCapacityCells(
|
||||
cells,
|
||||
blockWeight,
|
||||
capacity,
|
||||
columns,
|
||||
);
|
||||
const packed = packTransactions(
|
||||
capacityLayout.resolvedCells,
|
||||
columns,
|
||||
capacityLayout.rows,
|
||||
);
|
||||
|
||||
return { columns, ...packed };
|
||||
}
|
||||
@@ -6,15 +6,23 @@
|
||||
*/
|
||||
export function packCells(cells, columns, rows) {
|
||||
const occupied = new Uint8Array(columns * rows);
|
||||
const cursors = new Uint32Array(columns + 1);
|
||||
const layouts = [];
|
||||
|
||||
for (const cell of cells) {
|
||||
const span = Math.min(cell.span, columns);
|
||||
const position = findPosition(occupied, columns, rows, span);
|
||||
const position = findPosition(
|
||||
occupied,
|
||||
columns,
|
||||
rows,
|
||||
span,
|
||||
cursors[span],
|
||||
);
|
||||
|
||||
if (position === null) return null;
|
||||
|
||||
fillCells(occupied, columns, position.x, position.y, span);
|
||||
cursors[span] = position.index + span;
|
||||
layouts.push({ x: position.x, y: position.y, span });
|
||||
}
|
||||
|
||||
@@ -26,14 +34,22 @@ export function packCells(cells, columns, rows) {
|
||||
* @param {number} columns
|
||||
* @param {number} rows
|
||||
* @param {number} span
|
||||
* @returns {{ x: number, y: number } | null}
|
||||
* @param {number} start
|
||||
* @returns {{ index: number, x: number, y: number } | null}
|
||||
*/
|
||||
function findPosition(occupied, columns, rows, span) {
|
||||
function findPosition(occupied, columns, rows, span, start) {
|
||||
const lastRow = rows - span;
|
||||
const lastColumn = columns - span;
|
||||
const startRow = Math.floor(start / columns);
|
||||
const startColumn = start % columns;
|
||||
|
||||
for (let y = 0; y <= lastRow; y += 1) {
|
||||
for (let x = 0; x <= columns - span; x += 1) {
|
||||
if (canPlace(occupied, columns, x, y, span)) return { x, y };
|
||||
for (let y = startRow; y <= lastRow; y += 1) {
|
||||
const firstColumn = y === startRow ? startColumn : 0;
|
||||
|
||||
for (let x = firstColumn; x <= lastColumn; x += 1) {
|
||||
if (canPlace(occupied, columns, x, y, span)) {
|
||||
return { index: y * columns + x, x, y };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import {
|
||||
loadBlockPreview,
|
||||
loadBlockPreviewFilters,
|
||||
} from "./data.js";
|
||||
import { loadBlockPreview } from "./data.js";
|
||||
import { createBlockPreviewFilterData } from "./filters/data.js";
|
||||
import {
|
||||
createPendingPreviewFilters,
|
||||
createPreviewFilters,
|
||||
@@ -29,35 +27,18 @@ function createFigure(body, filters, inspector) {
|
||||
return figure;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {() => Promise<T>} load
|
||||
*/
|
||||
function memoize(load) {
|
||||
let promise = /** @type {Promise<T> | null} */ (null);
|
||||
|
||||
return () => {
|
||||
promise ??= load().catch((error) => {
|
||||
promise = null;
|
||||
throw error;
|
||||
});
|
||||
|
||||
return promise;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewData} data
|
||||
* @param {() => Promise<BlockPreviewFilterState>} loadFilters
|
||||
* @param {number} height
|
||||
* @param {AbortSignal} signal
|
||||
*/
|
||||
function createPreview(data, loadFilters, signal) {
|
||||
const loadFilterState = memoize(loadFilters);
|
||||
const inspector = createBlockPreviewInspector(signal, loadFilterState);
|
||||
function createPreview(data, height, signal) {
|
||||
const filterData = createBlockPreviewFilterData(height, data.range, signal);
|
||||
const inspector = createBlockPreviewInspector(signal, filterData);
|
||||
const heatmap = createBlockPreviewHeatmap(data, {
|
||||
onInspect: inspector.inspect,
|
||||
});
|
||||
const filters = createPreviewFilters(loadFilterState, heatmap);
|
||||
const filters = createPreviewFilters(filterData, heatmap);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
@@ -96,11 +77,7 @@ export function createBlockPreviewPane(block) {
|
||||
void loadBlockPreview(block, controller.signal)
|
||||
.then((data) => {
|
||||
if (!live) return;
|
||||
const preview = createPreview(
|
||||
data,
|
||||
() => loadBlockPreviewFilters(data.range, controller.signal),
|
||||
controller.signal,
|
||||
);
|
||||
const preview = createPreview(data, block.height, controller.signal);
|
||||
|
||||
destroyHeatmap = preview.destroy;
|
||||
content.replaceChildren(preview.element);
|
||||
@@ -123,7 +100,6 @@ export function createBlockPreviewPane(block) {
|
||||
}
|
||||
|
||||
/** @typedef {import("./data.js").BlockPreviewData} BlockPreviewData */
|
||||
/** @typedef {import("./data.js").BlockPreviewFilterState} BlockPreviewFilterState */
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreview
|
||||
|
||||
@@ -36,9 +36,9 @@ function setField(element, value, loading = false) {
|
||||
|
||||
/**
|
||||
* @param {AbortSignal} parentSignal
|
||||
* @param {() => Promise<BlockPreviewFilterState>} loadFilters
|
||||
* @param {BlockPreviewFilterData} filterData
|
||||
*/
|
||||
export function createBlockPreviewInspector(parentSignal, loadFilters) {
|
||||
export function createBlockPreviewInspector(parentSignal, filterData) {
|
||||
const element = document.createElement("dl");
|
||||
const txid = appendField(element, "txid");
|
||||
const tx = appendField(element, "tx");
|
||||
@@ -51,8 +51,7 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
|
||||
let inspected = /** @type {BlockPreviewTransaction | null} */ (null);
|
||||
let point = /** @type {BlockPreviewPointer | null} */ (null);
|
||||
let readoutSize = /** @type {BlockPreviewReadoutSize | null} */ (null);
|
||||
let filterState = /** @type {BlockPreviewFilterState | null} */ (null);
|
||||
let filterPromise = /** @type {Promise<void> | null} */ (null);
|
||||
let traitsController = /** @type {AbortController | null} */ (null);
|
||||
let traitsTimer = 0;
|
||||
|
||||
element.dataset.blockPreviewTransaction = "";
|
||||
@@ -60,6 +59,8 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
|
||||
|
||||
function abortPending() {
|
||||
clearTimeout(traitsTimer);
|
||||
traitsController?.abort();
|
||||
traitsController = null;
|
||||
txidLoader.abort();
|
||||
}
|
||||
|
||||
@@ -87,13 +88,14 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {number} mask */
|
||||
function setTraits(mask) {
|
||||
/** @param {string[]} keys */
|
||||
function setTraits(keys) {
|
||||
invalidateReadoutSize();
|
||||
const activeKeys = new Set(keys);
|
||||
|
||||
for (const { filters, value } of traitFields) {
|
||||
const labels = filters
|
||||
.filter(({ bit }) => mask & bit)
|
||||
.filter(({ key }) => activeKeys.has(key))
|
||||
.map(({ label }) => label);
|
||||
const text = labels.join(" · ") || "none";
|
||||
|
||||
@@ -102,46 +104,33 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {BlockPreviewTransaction} transaction */
|
||||
function setTransactionTraits(transaction) {
|
||||
const state = /** @type {BlockPreviewFilterState} */ (filterState);
|
||||
const mask = state.masks[transaction.txIndex - state.start];
|
||||
|
||||
setTraits(mask);
|
||||
if (point !== null) place(point);
|
||||
}
|
||||
|
||||
function loadFilterState() {
|
||||
if (filterState !== null || filterPromise !== null) return;
|
||||
|
||||
filterPromise = loadFilters()
|
||||
.then((state) => {
|
||||
filterState = state;
|
||||
filterPromise = null;
|
||||
if (inspected !== null) setTransactionTraits(inspected);
|
||||
})
|
||||
.catch((error) => {
|
||||
filterPromise = null;
|
||||
if (parentSignal.aborted || inspected === null) return;
|
||||
|
||||
for (const { value } of traitFields) setField(value, "unavailable");
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {BlockPreviewTransaction} transaction
|
||||
* @param {boolean} eager
|
||||
*/
|
||||
function loadTraits(transaction, eager) {
|
||||
setTraitsLoading();
|
||||
if (filterState !== null) {
|
||||
setTransactionTraits(transaction);
|
||||
return;
|
||||
}
|
||||
|
||||
traitsTimer = setTimeout(() => {
|
||||
loadFilterState();
|
||||
const controller = new AbortController();
|
||||
const signal = AbortSignal.any([parentSignal, controller.signal]);
|
||||
|
||||
traitsController = controller;
|
||||
void filterData.loadTransactionFilters(transaction.txIndex, signal)
|
||||
.then((keys) => {
|
||||
if (inspected !== transaction) return;
|
||||
|
||||
setTraits(keys);
|
||||
if (point !== null) place(point);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (signal.aborted || inspected !== transaction) return;
|
||||
|
||||
for (const { value } of traitFields) setField(value, "unavailable");
|
||||
console.error(error);
|
||||
})
|
||||
.finally(() => {
|
||||
if (traitsController === controller) traitsController = null;
|
||||
});
|
||||
}, eager ? 0 : TRAITS_DWELL_MS);
|
||||
}
|
||||
|
||||
@@ -210,7 +199,7 @@ export function createBlockPreviewInspector(parentSignal, loadFilters) {
|
||||
}
|
||||
|
||||
/** @typedef {import("./data.js").BlockPreviewTransaction} BlockPreviewTransaction */
|
||||
/** @typedef {import("./data.js").BlockPreviewFilterState} BlockPreviewFilterState */
|
||||
/** @typedef {import("./filters/data.js").BlockPreviewFilterData} BlockPreviewFilterData */
|
||||
|
||||
/**
|
||||
* @typedef {Object} BlockPreviewPointer
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import { BrkClient } from "../modules/brk-client/index.js";
|
||||
|
||||
export const brk = new BrkClient("https://bitview.space");
|
||||
export const brk = new BrkClient("http://localhost:3110");
|
||||
|
||||
@@ -25,6 +25,7 @@ export const txColors = /** @type {const} */ ({
|
||||
v1: colors.blue(),
|
||||
v2: colors.violet(),
|
||||
v3: colors.fuchsia(),
|
||||
otherVersion: colors.gray(),
|
||||
rbf: colors.orange(),
|
||||
noRbf: colors.gray(),
|
||||
oneInput: colors.yellow(),
|
||||
@@ -42,4 +43,59 @@ export const txColors = /** @type {const} */ ({
|
||||
opReturn: colors.red(),
|
||||
empty: colors.gray(),
|
||||
unknown: colors.gray(),
|
||||
behavior: {
|
||||
cpfpParent: colors.sky(),
|
||||
cpfpChild: colors.blue(),
|
||||
coinjoin: colors.violet(),
|
||||
consolidation: colors.yellow(),
|
||||
batchPayout: colors.green(),
|
||||
},
|
||||
data: {
|
||||
fakePubkey: colors.yellow(),
|
||||
fakeScripthash: colors.amber(),
|
||||
inscription: colors.fuchsia(),
|
||||
annex: colors.violet(),
|
||||
dust: colors.red(),
|
||||
},
|
||||
sighash: {
|
||||
all: colors.blue(),
|
||||
none: colors.red(),
|
||||
single: colors.orange(),
|
||||
default: colors.violet(),
|
||||
anyoneCanPay: colors.fuchsia(),
|
||||
},
|
||||
policy: {
|
||||
nonstandard: colors.red(),
|
||||
},
|
||||
opReturnKind: {
|
||||
runes: colors.orange(),
|
||||
veriBlock: colors.blue(),
|
||||
omni: colors.violet(),
|
||||
stacks: colors.purple(),
|
||||
blockstack: colors.indigo(),
|
||||
colu: colors.cyan(),
|
||||
openAssets: colors.sky(),
|
||||
komodo: colors.amber(),
|
||||
coinSpark: colors.yellow(),
|
||||
poet: colors.fuchsia(),
|
||||
docproof: colors.emerald(),
|
||||
openTimestamps: colors.green(),
|
||||
factom: colors.lime(),
|
||||
eternityWall: colors.avocado(),
|
||||
memo: colors.pink(),
|
||||
bitproof: colors.rose(),
|
||||
ascribe: colors.teal(),
|
||||
stampery: colors.blue(),
|
||||
epobc: colors.violet(),
|
||||
bareHash: colors.white(),
|
||||
text: colors.cyan(),
|
||||
empty: colors.gray(),
|
||||
unknown: colors.gray(),
|
||||
},
|
||||
opReturnPolicy: {
|
||||
standard: colors.green(),
|
||||
oversized: colors.orange(),
|
||||
multiple: colors.yellow(),
|
||||
preV30Nonstandard: colors.red(),
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user