mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-24 22:59:58 -07:00
1399 lines
40 KiB
JavaScript
1399 lines
40 KiB
JavaScript
/** Network section - On-chain activity and health */
|
|
|
|
import { colors } from "../utils/colors.js";
|
|
import { brk } from "../utils/client.js";
|
|
import { Unit } from "../utils/units.js";
|
|
import { entries } from "../utils/array.js";
|
|
import {
|
|
line,
|
|
baseline,
|
|
fromSupplyPattern,
|
|
chartsFromFull,
|
|
chartsFromFullPerBlock,
|
|
chartsFromCount,
|
|
chartsFromCountEntries,
|
|
chartsFromPercentCumulative,
|
|
chartsFromPercentCumulativeEntries,
|
|
chartsFromAggregatedPerBlock,
|
|
distributionWindowsTree,
|
|
averagesArray,
|
|
simpleDeltaTree,
|
|
ROLLING_WINDOWS,
|
|
chartsFromBlockAnd6b,
|
|
multiSeriesTree,
|
|
percentRatioDots,
|
|
} from "./series.js";
|
|
import {
|
|
satsBtcUsd,
|
|
satsBtcUsdFrom,
|
|
satsBtcUsdFullTree,
|
|
formatCohortTitle,
|
|
groupedWindowsCumulative,
|
|
} from "./shared.js";
|
|
|
|
/**
|
|
* Create Network section
|
|
* @returns {PartialOptionsGroup}
|
|
*/
|
|
export function createNetworkSection() {
|
|
const { blocks, transactions, inputs, outputs, supply, addrs, cohorts } =
|
|
brk.series;
|
|
|
|
const st = colors.scriptType;
|
|
|
|
// Addressable types - newest to oldest (for addresses/counts that only support addressable types)
|
|
const addressTypes = /** @type {const} */ ([
|
|
{ key: "p2a", name: "P2A", color: st.p2a, defaultActive: false },
|
|
{ key: "p2tr", name: "P2TR", color: st.p2tr, defaultActive: true },
|
|
{ key: "p2wsh", name: "P2WSH", color: st.p2wsh, defaultActive: true },
|
|
{ key: "p2wpkh", name: "P2WPKH", color: st.p2wpkh, defaultActive: true },
|
|
{ key: "p2sh", name: "P2SH", color: st.p2sh, defaultActive: true },
|
|
{ key: "p2pkh", name: "P2PKH", color: st.p2pkh, defaultActive: true },
|
|
{ key: "p2pk33", name: "P2PK33", color: st.p2pk33, defaultActive: false },
|
|
{ key: "p2pk65", name: "P2PK65", color: st.p2pk65, defaultActive: false },
|
|
]);
|
|
|
|
// Non-addressable script types, reverse creation with catch-alls at tail
|
|
const nonAddressableTypes = /** @type {const} */ ([
|
|
{
|
|
key: "opReturn",
|
|
name: "OP_RETURN",
|
|
color: st.opReturn,
|
|
defaultActive: true,
|
|
},
|
|
{ key: "p2ms", name: "P2MS", color: st.p2ms, defaultActive: false },
|
|
{
|
|
key: "empty",
|
|
name: "Empty",
|
|
color: st.empty,
|
|
defaultActive: false,
|
|
},
|
|
{
|
|
key: "unknown",
|
|
name: "Unknown",
|
|
color: st.unknown,
|
|
defaultActive: false,
|
|
},
|
|
]);
|
|
|
|
// All output types = addressable + non-addressable (12 total)
|
|
const outputTypes = [...addressTypes, ...nonAddressableTypes];
|
|
// Spendable input types: every output type can fund an input *except* OP_RETURN
|
|
const inputTypes = [
|
|
...addressTypes,
|
|
...nonAddressableTypes.filter((t) => t.key !== "opReturn"),
|
|
];
|
|
|
|
// Transacting types (transaction participation)
|
|
const activityTypes = /** @type {const} */ ([
|
|
{ key: "active", name: "Active" },
|
|
{ key: "sending", name: "Sending" },
|
|
{ key: "receiving", name: "Receiving" },
|
|
{ key: "bidirectional", name: "Bidirectional" },
|
|
{ key: "reactivated", name: "Reactivated" },
|
|
]);
|
|
|
|
const countMetrics = /** @type {const} */ ([
|
|
{ key: "funded", name: "Funded", color: undefined },
|
|
{ key: "empty", name: "Empty", color: colors.gray },
|
|
{ key: "total", name: "Total", color: colors.default },
|
|
]);
|
|
|
|
const reusedSetEntries =
|
|
/**
|
|
* @param {AddressableType | "all"} key
|
|
* @param {(name: string) => string} title
|
|
*/
|
|
(key, title) => [
|
|
{
|
|
name: "Compare",
|
|
title: title("Reused Address Count"),
|
|
bottom: [
|
|
line({
|
|
series: addrs.reused.count.funded[key],
|
|
name: "Funded",
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
series: addrs.reused.count.total[key],
|
|
name: "Total",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Funded",
|
|
title: title("Funded Reused Addresses"),
|
|
bottom: [
|
|
line({
|
|
series: addrs.reused.count.funded[key],
|
|
name: "Funded Reused",
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Total",
|
|
title: title("Total Reused Addresses"),
|
|
bottom: [
|
|
line({
|
|
series: addrs.reused.count.total[key],
|
|
name: "Total Reused",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
];
|
|
|
|
const reusedInputsSubtree =
|
|
/**
|
|
* @param {AddressableType | "all"} key
|
|
* @param {(name: string) => string} title
|
|
*/
|
|
(key, title) => ({
|
|
name: "Inputs",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: chartsFromCount({
|
|
pattern: addrs.reused.events.inputFromReusedAddrCount[key],
|
|
title,
|
|
metric: "Transaction Inputs from Reused Addresses",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Share",
|
|
tree: chartsFromPercentCumulative({
|
|
pattern: addrs.reused.events.inputFromReusedAddrShare[key],
|
|
title,
|
|
metric: "Share of Transaction Inputs from Reused Addresses",
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
const reusedOutputsSubtreeForAll =
|
|
/** @param {(name: string) => string} title */
|
|
(title) => ({
|
|
name: "Outputs",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: chartsFromCount({
|
|
pattern: addrs.reused.events.outputToReusedAddrCount.all,
|
|
title,
|
|
metric: "Transaction Outputs to Reused Addresses",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Share",
|
|
tree: chartsFromPercentCumulativeEntries({
|
|
entries: [
|
|
{
|
|
name: "All",
|
|
pattern: addrs.reused.events.outputToReusedAddrShare.all,
|
|
},
|
|
{
|
|
name: "Spendable",
|
|
pattern: addrs.reused.events.spendableOutputToReusedAddrShare,
|
|
color: colors.gray,
|
|
},
|
|
],
|
|
title,
|
|
metric: "Share of Transaction Outputs to Reused Addresses",
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
const reusedOutputsSubtreeForType =
|
|
/**
|
|
* @param {AddressableType} key
|
|
* @param {(name: string) => string} title
|
|
*/
|
|
(key, title) => ({
|
|
name: "Outputs",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: chartsFromCount({
|
|
pattern: addrs.reused.events.outputToReusedAddrCount[key],
|
|
title,
|
|
metric: "Transaction Outputs to Reused Addresses",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Share",
|
|
tree: chartsFromPercentCumulative({
|
|
pattern: addrs.reused.events.outputToReusedAddrShare[key],
|
|
title,
|
|
metric: "Share of Transaction Outputs to Reused Addresses",
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
const reusedActiveSubtreeForAll =
|
|
/** @param {(name: string) => string} title */
|
|
(title) => ({
|
|
name: "Active",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: averagesArray({
|
|
windows: addrs.reused.events.activeReusedAddrCount,
|
|
title,
|
|
metric: "Active Reused Addresses",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Share",
|
|
tree: averagesArray({
|
|
windows: addrs.reused.events.activeReusedAddrShare,
|
|
title,
|
|
metric: "Active Reused Address Share",
|
|
unit: Unit.percentage,
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
const reusedSubtreeForAll =
|
|
/** @param {(name: string) => string} title */
|
|
(title) => ({
|
|
name: "Reused",
|
|
tree: [
|
|
...reusedSetEntries("all", title),
|
|
reusedActiveSubtreeForAll(title),
|
|
reusedOutputsSubtreeForAll(title),
|
|
reusedInputsSubtree("all", title),
|
|
],
|
|
});
|
|
|
|
const reusedSubtreeForType =
|
|
/**
|
|
* @param {AddressableType} key
|
|
* @param {(name: string) => string} title
|
|
*/
|
|
(key, title) => ({
|
|
name: "Reused",
|
|
tree: [
|
|
...reusedSetEntries(key, title),
|
|
reusedOutputsSubtreeForType(key, title),
|
|
reusedInputsSubtree(key, title),
|
|
],
|
|
});
|
|
|
|
const countSubtree =
|
|
/**
|
|
* @param {AddressableType | "all"} key
|
|
* @param {(name: string) => string} title
|
|
*/
|
|
(key, title) => ({
|
|
name: "Count",
|
|
tree: [
|
|
{
|
|
name: "Compare",
|
|
title: title("Address Count"),
|
|
bottom: countMetrics.map((m) =>
|
|
line({
|
|
series: addrs[m.key][key],
|
|
name: m.name,
|
|
color: m.color,
|
|
unit: Unit.count,
|
|
}),
|
|
),
|
|
},
|
|
...countMetrics.map((m) => ({
|
|
name: m.name,
|
|
title: title(`${m.name} Addresses`),
|
|
bottom: [
|
|
line({
|
|
series: addrs[m.key][key],
|
|
name: m.name,
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
})),
|
|
],
|
|
});
|
|
|
|
const exposedSubtree =
|
|
/**
|
|
* @param {AddressableType | "all"} key
|
|
* @param {(name: string) => string} title
|
|
*/
|
|
(key, title) => ({
|
|
name: "Exposed",
|
|
tree: [
|
|
{
|
|
name: "Compare",
|
|
title: title("Exposed Address Count"),
|
|
bottom: [
|
|
line({
|
|
series: addrs.exposed.count.funded[key],
|
|
name: "Funded",
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
series: addrs.exposed.count.total[key],
|
|
name: "Total",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Funded",
|
|
title: title("Funded Exposed Address Count"),
|
|
bottom: [
|
|
line({
|
|
series: addrs.exposed.count.funded[key],
|
|
name: "Funded Exposed",
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Total",
|
|
title: title("Total Exposed Address Count"),
|
|
bottom: [
|
|
line({
|
|
series: addrs.exposed.count.total[key],
|
|
name: "Total Exposed",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Supply",
|
|
title: title("Supply in Exposed Addresses"),
|
|
bottom: satsBtcUsd({
|
|
pattern: addrs.exposed.supply[key],
|
|
name: "Supply",
|
|
}),
|
|
},
|
|
],
|
|
});
|
|
|
|
const activityPerTypeEntries =
|
|
/**
|
|
* @param {AddressableType | "all"} key
|
|
* @param {(name: string) => string} title
|
|
*/
|
|
(key, title) =>
|
|
activityTypes.map((t) => ({
|
|
name: t.name,
|
|
tree: averagesArray({
|
|
windows: addrs.activity[key][t.key],
|
|
title,
|
|
metric: `${t.name} Addresses`,
|
|
unit: Unit.count,
|
|
}),
|
|
}));
|
|
|
|
const activitySubtreeForAll =
|
|
/** @param {(name: string) => string} title */
|
|
(title) => ({
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Compare",
|
|
tree: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: title(`${w.title} Active Addresses`),
|
|
bottom: [
|
|
...activityTypes.map((t, i) =>
|
|
line({
|
|
series: addrs.activity.all[t.key][w.key],
|
|
name: t.name,
|
|
color: colors.at(i, activityTypes.length),
|
|
unit: Unit.count,
|
|
}),
|
|
),
|
|
line({
|
|
series: addrs.reused.events.activeReusedAddrShare[w.key],
|
|
name: "Reused Share",
|
|
unit: Unit.percentage,
|
|
}),
|
|
],
|
|
})),
|
|
},
|
|
...activityPerTypeEntries("all", title),
|
|
],
|
|
});
|
|
|
|
const activitySubtreeForType =
|
|
/**
|
|
* @param {AddressableType} key
|
|
* @param {(name: string) => string} title
|
|
*/
|
|
(key, title) => ({
|
|
name: "Activity",
|
|
tree: [
|
|
{
|
|
name: "Compare",
|
|
tree: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: title(`${w.title} Active Addresses`),
|
|
bottom: activityTypes.map((t, i) =>
|
|
line({
|
|
series: addrs.activity[key][t.key][w.key],
|
|
name: t.name,
|
|
color: colors.at(i, activityTypes.length),
|
|
unit: Unit.count,
|
|
}),
|
|
),
|
|
})),
|
|
},
|
|
...activityPerTypeEntries(key, title),
|
|
],
|
|
});
|
|
|
|
const createAddressSeriesTreeForAll = () => {
|
|
const title = formatCohortTitle();
|
|
return [
|
|
countSubtree("all", title),
|
|
{
|
|
name: "New",
|
|
tree: chartsFromCount({
|
|
pattern: addrs.new.all,
|
|
title,
|
|
metric: "New Addresses",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
...simpleDeltaTree({
|
|
delta: addrs.delta.all,
|
|
title,
|
|
metric: "Address Count",
|
|
unit: Unit.count,
|
|
}),
|
|
activitySubtreeForAll(title),
|
|
reusedSubtreeForAll(title),
|
|
exposedSubtree("all", title),
|
|
];
|
|
};
|
|
|
|
const createAddressSeriesTreeForType =
|
|
/**
|
|
* @param {AddressableType} addrType
|
|
* @param {string} typeName
|
|
*/
|
|
(addrType, typeName) => {
|
|
const title = formatCohortTitle(typeName);
|
|
return [
|
|
countSubtree(addrType, title),
|
|
{
|
|
name: "New",
|
|
tree: chartsFromCount({
|
|
pattern: addrs.new[addrType],
|
|
title,
|
|
metric: "New Addresses",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
...simpleDeltaTree({
|
|
delta: addrs.delta[addrType],
|
|
title,
|
|
metric: "Address Count",
|
|
unit: Unit.count,
|
|
}),
|
|
activitySubtreeForType(addrType, title),
|
|
reusedSubtreeForType(addrType, title),
|
|
exposedSubtree(addrType, title),
|
|
];
|
|
};
|
|
|
|
/**
|
|
* Mirror of the per-type singles tree, but every leaf is a cross-type
|
|
* comparison chart (same metric, same unit, one line per addr type).
|
|
* Structure parallels `createAddressSeriesTreeForType` section-by-section
|
|
* so users can compare anything they can view on a single type.
|
|
*/
|
|
const createAddressByTypeCompare = () => {
|
|
const typeLines =
|
|
/**
|
|
* @param {(t: (typeof addressTypes)[number]) => AnySeriesPattern} getSeries
|
|
* @param {Unit} [unit]
|
|
*/
|
|
(getSeries, unit = Unit.count) =>
|
|
addressTypes.map((t) =>
|
|
line({
|
|
series: getSeries(t),
|
|
name: t.name,
|
|
color: t.color,
|
|
unit,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
);
|
|
|
|
const typeBaselines =
|
|
/**
|
|
* @param {(t: (typeof addressTypes)[number]) => AnySeriesPattern} getSeries
|
|
* @param {Unit} [unit]
|
|
*/
|
|
(getSeries, unit = Unit.count) =>
|
|
addressTypes.map((t) =>
|
|
baseline({
|
|
series: getSeries(t),
|
|
name: t.name,
|
|
color: t.color,
|
|
unit,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
);
|
|
|
|
return {
|
|
name: "Compare",
|
|
tree: [
|
|
// Count (lifetime Funded/Empty/Total)
|
|
{
|
|
name: "Count",
|
|
tree: countMetrics.map((m) => ({
|
|
name: m.name,
|
|
title: `${m.name} Address Count by Type`,
|
|
bottom: typeLines((t) => addrs[m.key][t.key]),
|
|
})),
|
|
},
|
|
|
|
// New (rolling sums + cumulative)
|
|
{
|
|
name: "New",
|
|
tree: groupedWindowsCumulative({
|
|
list: addressTypes,
|
|
title: (s) => s,
|
|
metricTitle: "New Addresses by Type",
|
|
getWindowSeries: (t, key) => addrs.new[t.key].sum[key],
|
|
getCumulativeSeries: (t) => addrs.new[t.key].cumulative,
|
|
seriesFn: line,
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
|
|
// Change (rolling deltas, signed, baseline)
|
|
{
|
|
name: "Change",
|
|
tree: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Address Count Change by Type`,
|
|
bottom: typeBaselines(
|
|
(t) => addrs.delta[t.key].absolute[w.key],
|
|
),
|
|
})),
|
|
},
|
|
|
|
// Growth Rate (rolling percent rates)
|
|
{
|
|
name: "Growth Rate",
|
|
tree: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Address Growth Rate by Type`,
|
|
bottom: typeLines(
|
|
(t) => addrs.delta[t.key].rate[w.key].percent,
|
|
Unit.percentage,
|
|
),
|
|
})),
|
|
},
|
|
|
|
// Activity (per activity type, per window)
|
|
{
|
|
name: "Activity",
|
|
tree: activityTypes.map((a) => ({
|
|
name: a.name,
|
|
tree: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} ${a.name} Addresses by Type`,
|
|
bottom: typeLines(
|
|
(t) => addrs.activity[t.key][a.key][w.key],
|
|
),
|
|
})),
|
|
})),
|
|
},
|
|
|
|
// Reused
|
|
{
|
|
name: "Reused",
|
|
tree: [
|
|
{
|
|
name: "Funded",
|
|
title: "Funded Reused Address Count by Type",
|
|
bottom: typeLines((t) => addrs.reused.count.funded[t.key]),
|
|
},
|
|
{
|
|
name: "Total",
|
|
title: "Total Reused Address Count by Type",
|
|
bottom: typeLines((t) => addrs.reused.count.total[t.key]),
|
|
},
|
|
{
|
|
name: "Outputs",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: groupedWindowsCumulative({
|
|
list: addressTypes,
|
|
title: (s) => s,
|
|
metricTitle:
|
|
"Transaction Outputs to Reused Addresses by Type",
|
|
getWindowSeries: (t, key) =>
|
|
addrs.reused.events.outputToReusedAddrCount[t.key].sum[
|
|
key
|
|
],
|
|
getCumulativeSeries: (t) =>
|
|
addrs.reused.events.outputToReusedAddrCount[t.key]
|
|
.cumulative,
|
|
seriesFn: line,
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Share",
|
|
tree: [
|
|
...ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Share of Transaction Outputs to Reused Addresses by Type`,
|
|
bottom: typeLines(
|
|
(t) =>
|
|
addrs.reused.events.outputToReusedAddrShare[t.key][
|
|
w.key
|
|
].percent,
|
|
Unit.percentage,
|
|
),
|
|
})),
|
|
{
|
|
name: "Cumulative",
|
|
title:
|
|
"Cumulative Share of Transaction Outputs to Reused Addresses by Type",
|
|
bottom: typeLines(
|
|
(t) =>
|
|
addrs.reused.events.outputToReusedAddrShare[t.key]
|
|
.percent,
|
|
Unit.percentage,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Inputs",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: groupedWindowsCumulative({
|
|
list: addressTypes,
|
|
title: (s) => s,
|
|
metricTitle:
|
|
"Transaction Inputs from Reused Addresses by Type",
|
|
getWindowSeries: (t, key) =>
|
|
addrs.reused.events.inputFromReusedAddrCount[t.key].sum[
|
|
key
|
|
],
|
|
getCumulativeSeries: (t) =>
|
|
addrs.reused.events.inputFromReusedAddrCount[t.key]
|
|
.cumulative,
|
|
seriesFn: line,
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Share",
|
|
tree: [
|
|
...ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Share of Transaction Inputs from Reused Addresses by Type`,
|
|
bottom: typeLines(
|
|
(t) =>
|
|
addrs.reused.events.inputFromReusedAddrShare[t.key][
|
|
w.key
|
|
].percent,
|
|
Unit.percentage,
|
|
),
|
|
})),
|
|
{
|
|
name: "Cumulative",
|
|
title:
|
|
"Cumulative Share of Transaction Inputs from Reused Addresses by Type",
|
|
bottom: typeLines(
|
|
(t) =>
|
|
addrs.reused.events.inputFromReusedAddrShare[t.key]
|
|
.percent,
|
|
Unit.percentage,
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// Exposed
|
|
{
|
|
name: "Exposed",
|
|
tree: [
|
|
{
|
|
name: "Funded",
|
|
title: "Funded Exposed Address Count by Type",
|
|
bottom: typeLines((t) => addrs.exposed.count.funded[t.key]),
|
|
},
|
|
{
|
|
name: "Total",
|
|
title: "Total Exposed Address Count by Type",
|
|
bottom: typeLines((t) => addrs.exposed.count.total[t.key]),
|
|
},
|
|
{
|
|
name: "Supply",
|
|
title: "Supply in Exposed Addresses by Type",
|
|
bottom: addressTypes.flatMap((t) =>
|
|
satsBtcUsd({
|
|
pattern: addrs.exposed.supply[t.key],
|
|
name: t.name,
|
|
color: t.color,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Build a "By Type" subtree: Compare (count / tx count / tx %) plus a
|
|
* per-type drill-down with the same three metrics.
|
|
*
|
|
* @template {string} K
|
|
* @param {Object} args
|
|
* @param {string} args.label - Singular noun for count/tree labels ("Output" / "Prev-Out")
|
|
* @param {Readonly<Record<K, CountPattern<number>>>} args.count
|
|
* @param {Readonly<Record<K, CountPattern<number>>>} args.txCount
|
|
* @param {Readonly<Record<K, PercentRatioCumulativePattern>>} args.share
|
|
* @param {Readonly<Record<K, PercentRatioCumulativePattern>>} args.txShare
|
|
* @param {ReadonlyArray<{key: K, name: string, color: Color, defaultActive: boolean}>} args.types
|
|
* @returns {PartialOptionsTree}
|
|
*/
|
|
const createByTypeTree = ({
|
|
label,
|
|
count,
|
|
share,
|
|
txCount,
|
|
txShare,
|
|
types,
|
|
}) => {
|
|
const lowerLabel = label.toLowerCase();
|
|
return [
|
|
{
|
|
name: "Compare",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: [
|
|
...ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} ${label} Count by Type`,
|
|
bottom: types.map((t) =>
|
|
line({
|
|
series: count[t.key].sum[w.key],
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
),
|
|
})),
|
|
{
|
|
name: "Cumulative",
|
|
title: `Cumulative ${label} Count by Type`,
|
|
bottom: types.map((t) =>
|
|
line({
|
|
series: count[t.key].cumulative,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Share",
|
|
tree: groupedWindowsCumulative({
|
|
list: types,
|
|
title: (n) => n,
|
|
metricTitle: `Share of ${label}s by Type`,
|
|
getWindowSeries: (t, key) => share[t.key][key].percent,
|
|
getCumulativeSeries: (t) => share[t.key].percent,
|
|
seriesFn: line,
|
|
unit: Unit.percentage,
|
|
}),
|
|
},
|
|
{
|
|
name: "Transaction Count",
|
|
tree: [
|
|
...ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Transactions by ${label} Type`,
|
|
bottom: types.map((t) =>
|
|
line({
|
|
series: txCount[t.key].sum[w.key],
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
),
|
|
})),
|
|
{
|
|
name: "Cumulative",
|
|
title: `Cumulative Transactions by ${label} Type`,
|
|
bottom: types.map((t) =>
|
|
line({
|
|
series: txCount[t.key].cumulative,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.count,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Transaction Share",
|
|
tree: [
|
|
...ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Share of Transactions by ${label} Type`,
|
|
bottom: types.map((t) =>
|
|
line({
|
|
series: txShare[t.key][w.key].percent,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.percentage,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
),
|
|
})),
|
|
{
|
|
name: "Cumulative",
|
|
title: `Cumulative Share of Transactions by ${label} Type`,
|
|
bottom: types.map((t) =>
|
|
line({
|
|
series: txShare[t.key].percent,
|
|
name: t.name,
|
|
color: t.color,
|
|
unit: Unit.percentage,
|
|
defaultActive: t.defaultActive,
|
|
}),
|
|
),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
...types.map((t) => ({
|
|
name: t.name,
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: chartsFromCount({
|
|
pattern: count[t.key],
|
|
metric: `${t.name} ${label} Count`,
|
|
unit: Unit.count,
|
|
color: t.color,
|
|
}),
|
|
},
|
|
{
|
|
name: "Share",
|
|
tree: chartsFromPercentCumulative({
|
|
pattern: share[t.key],
|
|
metric: `Share of ${label}s that are ${t.name}`,
|
|
color: t.color,
|
|
}),
|
|
},
|
|
{
|
|
name: "Transaction Count",
|
|
tree: chartsFromCount({
|
|
pattern: txCount[t.key],
|
|
metric: `Transactions with ${t.name} ${lowerLabel}`,
|
|
unit: Unit.count,
|
|
color: t.color,
|
|
}),
|
|
},
|
|
{
|
|
name: "Transaction Share",
|
|
tree: chartsFromPercentCumulative({
|
|
pattern: txShare[t.key],
|
|
metric: `Share of Transactions with ${t.name} ${lowerLabel}`,
|
|
color: t.color,
|
|
}),
|
|
},
|
|
],
|
|
})),
|
|
];
|
|
};
|
|
|
|
return {
|
|
name: "Network",
|
|
tree: [
|
|
// Supply
|
|
{
|
|
name: "Supply",
|
|
tree: [
|
|
{
|
|
name: "Circulating",
|
|
title: "Circulating Supply",
|
|
bottom: fromSupplyPattern({
|
|
pattern: supply.circulating,
|
|
title: "Supply",
|
|
}),
|
|
},
|
|
{
|
|
name: "Inflation",
|
|
title: "Inflation Rate",
|
|
bottom: percentRatioDots({
|
|
pattern: supply.inflationRate,
|
|
name: "Rate",
|
|
}),
|
|
},
|
|
{
|
|
name: "Hodled or Lost",
|
|
title: "Hodled or Lost Supply",
|
|
bottom: satsBtcUsd({
|
|
pattern: supply.hodledOrLost,
|
|
name: "Supply",
|
|
}),
|
|
},
|
|
{
|
|
name: "Unspendable",
|
|
tree: [
|
|
{
|
|
name: "All",
|
|
title: "Unspendable Supply",
|
|
bottom: satsBtcUsdFrom({
|
|
source: supply.burned,
|
|
key: "cumulative",
|
|
name: "All Time",
|
|
}),
|
|
},
|
|
{
|
|
name: "OP_RETURN",
|
|
title: "OP_RETURN Burned",
|
|
bottom: satsBtcUsd({
|
|
pattern: outputs.value.opReturn.cumulative,
|
|
name: "All Time",
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// Blocks
|
|
{
|
|
name: "Blocks",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: [
|
|
{
|
|
name: "Compare",
|
|
title: "Block Count",
|
|
bottom: ROLLING_WINDOWS.map((w) =>
|
|
line({
|
|
series: blocks.count.total.sum[w.key],
|
|
name: w.name,
|
|
color: w.color,
|
|
unit: Unit.count,
|
|
}),
|
|
),
|
|
},
|
|
...ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Block Count`,
|
|
bottom: [
|
|
line({
|
|
series: blocks.count.total.sum[w.key],
|
|
name: "Actual",
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
series: blocks.count.target[w.key],
|
|
name: "Target",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
options: { lineStyle: 4 },
|
|
}),
|
|
],
|
|
})),
|
|
{
|
|
name: "Cumulative",
|
|
title: "Cumulative Block Count",
|
|
bottom: [
|
|
{
|
|
series: blocks.count.total.cumulative,
|
|
title: "All Time",
|
|
unit: Unit.count,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Interval",
|
|
tree: averagesArray({
|
|
windows: blocks.interval,
|
|
metric: "Block Interval",
|
|
unit: Unit.secs,
|
|
}),
|
|
},
|
|
{
|
|
name: "Size",
|
|
tree: chartsFromFull({
|
|
pattern: blocks.size,
|
|
metric: "Block Size",
|
|
unit: Unit.bytes,
|
|
}),
|
|
},
|
|
{
|
|
name: "Weight",
|
|
tree: chartsFromFull({
|
|
pattern: blocks.weight,
|
|
metric: "Block Weight",
|
|
unit: Unit.wu,
|
|
}),
|
|
},
|
|
{
|
|
name: "vBytes",
|
|
tree: chartsFromFull({
|
|
pattern: blocks.vbytes,
|
|
metric: "Block vBytes",
|
|
unit: Unit.vb,
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
|
|
// Transactions
|
|
{
|
|
name: "Transactions",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: chartsFromFullPerBlock({
|
|
pattern: transactions.count.total,
|
|
metric: "Transaction Count",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Per Second",
|
|
tree: averagesArray({
|
|
windows: transactions.volume.txPerSec,
|
|
metric: "Transactions per Second",
|
|
unit: Unit.perSec,
|
|
}),
|
|
},
|
|
{
|
|
name: "Volume",
|
|
tree: satsBtcUsdFullTree({
|
|
pattern: transactions.volume.transferVolume,
|
|
metric: "Transaction Volume",
|
|
}),
|
|
},
|
|
{
|
|
name: "Effective Fee Rate",
|
|
tree: chartsFromBlockAnd6b({
|
|
pattern: transactions.fees.effectiveFeeRate,
|
|
metric: "Effective Transaction Fee Rate",
|
|
unit: Unit.feeRate,
|
|
}),
|
|
},
|
|
{
|
|
name: "Fee",
|
|
tree: chartsFromBlockAnd6b({
|
|
pattern: transactions.fees.fee,
|
|
metric: "Transaction Fee",
|
|
unit: Unit.sats,
|
|
}),
|
|
},
|
|
{
|
|
name: "Size",
|
|
tree: [
|
|
{
|
|
name: "Weight",
|
|
tree: chartsFromBlockAnd6b({
|
|
pattern: transactions.size.weight,
|
|
metric: "Transaction Weight",
|
|
unit: Unit.wu,
|
|
}),
|
|
},
|
|
{
|
|
name: "Virtual",
|
|
tree: chartsFromBlockAnd6b({
|
|
pattern: transactions.size.vsize,
|
|
metric: "Transaction vSize",
|
|
unit: Unit.vb,
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Versions",
|
|
tree: chartsFromCountEntries({
|
|
entries: entries(transactions.versions),
|
|
metric: "Transaction Versions",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Velocity",
|
|
title: "Transaction Velocity",
|
|
bottom: [
|
|
line({
|
|
series: supply.velocity.native,
|
|
name: "BTC",
|
|
unit: Unit.ratio,
|
|
}),
|
|
line({
|
|
series: supply.velocity.fiat,
|
|
name: "USD",
|
|
color: colors.usd,
|
|
unit: Unit.ratio,
|
|
}),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
|
|
// UTXOs
|
|
{
|
|
name: "UTXOs",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
title: "UTXO Count",
|
|
bottom: [
|
|
line({
|
|
series: outputs.unspent.count,
|
|
name: "Count",
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
...simpleDeltaTree({
|
|
delta: cohorts.utxo.all.outputs.unspentCount.delta,
|
|
metric: "UTXO Count",
|
|
unit: Unit.count,
|
|
}),
|
|
{
|
|
name: "Flow",
|
|
tree: multiSeriesTree({
|
|
entries: [
|
|
{
|
|
name: "Created",
|
|
color: colors.entity.output,
|
|
average: outputs.count.total.rolling.average,
|
|
sum: outputs.count.total.rolling.sum,
|
|
cumulative: outputs.count.total.cumulative,
|
|
},
|
|
{
|
|
name: "Spent",
|
|
color: colors.entity.input,
|
|
average: inputs.count.rolling.average,
|
|
sum: inputs.count.rolling.sum,
|
|
cumulative: inputs.count.cumulative,
|
|
},
|
|
],
|
|
metric: "UTXO Flow",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Outputs",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: [
|
|
{
|
|
name: "Compare",
|
|
title: "Output Count",
|
|
bottom: ROLLING_WINDOWS.map((w) =>
|
|
line({
|
|
series: outputs.count.total.rolling.average[w.key],
|
|
name: w.name,
|
|
color: w.color,
|
|
unit: Unit.count,
|
|
}),
|
|
),
|
|
},
|
|
...ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Output Count`,
|
|
bottom: [
|
|
line({
|
|
series: outputs.count.total.rolling.sum[w.key],
|
|
name: "Total (Sum)",
|
|
color: w.color,
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
series: outputs.byType.spendableOutputCount.sum[w.key],
|
|
name: "Spendable (Sum)",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
series: outputs.count.total.rolling.average[w.key],
|
|
name: "Total (Avg)",
|
|
color: w.color,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
line({
|
|
series: outputs.byType.spendableOutputCount.average[w.key],
|
|
name: "Spendable (Avg)",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
defaultActive: false,
|
|
}),
|
|
],
|
|
})),
|
|
{
|
|
name: "Cumulative",
|
|
title: "Cumulative Output Count",
|
|
bottom: [
|
|
line({
|
|
series: outputs.count.total.cumulative,
|
|
name: "Total",
|
|
unit: Unit.count,
|
|
}),
|
|
line({
|
|
series: outputs.byType.spendableOutputCount.cumulative,
|
|
name: "Spendable",
|
|
color: colors.gray,
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
distributionWindowsTree({
|
|
pattern: outputs.count.total.rolling,
|
|
metric: "Output Count per Block",
|
|
unit: Unit.count,
|
|
}),
|
|
],
|
|
},
|
|
{
|
|
name: "Per Second",
|
|
tree: averagesArray({
|
|
windows: outputs.perSec,
|
|
metric: "Outputs per Second",
|
|
unit: Unit.perSec,
|
|
}),
|
|
},
|
|
{
|
|
name: "By Type",
|
|
tree: createByTypeTree({
|
|
label: "Output",
|
|
count: outputs.byType.outputCount,
|
|
share: outputs.byType.outputShare,
|
|
txCount: outputs.byType.txCount,
|
|
txShare: outputs.byType.txShare,
|
|
types: outputTypes,
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Inputs",
|
|
tree: [
|
|
{
|
|
name: "Count",
|
|
tree: chartsFromAggregatedPerBlock({
|
|
pattern: inputs.count,
|
|
metric: "Input Count",
|
|
unit: Unit.count,
|
|
}),
|
|
},
|
|
{
|
|
name: "Per Second",
|
|
tree: averagesArray({
|
|
windows: inputs.perSec,
|
|
metric: "Inputs per Second",
|
|
unit: Unit.perSec,
|
|
}),
|
|
},
|
|
{
|
|
name: "By Type",
|
|
tree: createByTypeTree({
|
|
label: "Prev-Out",
|
|
count: inputs.byType.inputCount,
|
|
share: inputs.byType.inputShare,
|
|
txCount: inputs.byType.txCount,
|
|
txShare: inputs.byType.txShare,
|
|
types: inputTypes,
|
|
}),
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: "Throughput",
|
|
tree: ROLLING_WINDOWS.map((w) => ({
|
|
name: w.name,
|
|
title: `${w.title} Throughput`,
|
|
bottom: [
|
|
line({
|
|
series: transactions.volume.txPerSec[w.key],
|
|
name: "TX/sec",
|
|
color: colors.entity.tx,
|
|
unit: Unit.perSec,
|
|
}),
|
|
line({
|
|
series: inputs.perSec[w.key],
|
|
name: "Inputs/sec",
|
|
color: colors.entity.input,
|
|
unit: Unit.perSec,
|
|
}),
|
|
line({
|
|
series: outputs.perSec[w.key],
|
|
name: "Outputs/sec",
|
|
color: colors.entity.output,
|
|
unit: Unit.perSec,
|
|
}),
|
|
],
|
|
})),
|
|
},
|
|
|
|
// Addresses
|
|
{
|
|
name: "Addresses",
|
|
tree: [
|
|
...createAddressSeriesTreeForAll(),
|
|
{
|
|
name: "By Type",
|
|
tree: [
|
|
createAddressByTypeCompare(),
|
|
...addressTypes.map((t) => ({
|
|
name: t.name,
|
|
tree: createAddressSeriesTreeForType(t.key, t.name),
|
|
})),
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
}
|