From 0fc61d7932cd2698775755d667f8a4badb10f86f Mon Sep 17 00:00:00 2001 From: nym21 Date: Sun, 19 Jul 2026 09:34:03 +0200 Subject: [PATCH] global: opreturn part 4 --- crates/brk_client/src/lib.rs | 40 +++- crates/brk_computer/src/lib.rs | 2 +- crates/brk_computer/src/op_return/compute.rs | 40 +++- crates/brk_computer/src/op_return/import.rs | 4 +- crates/brk_computer/src/op_return/mod.rs | 2 +- crates/brk_computer/src/op_return/vecs.rs | 89 ++++++-- modules/brk-client/index.js | 43 +++- packages/brk_client/brk_client/__init__.py | 20 +- website/scripts/options/distribution/index.js | 2 +- .../options/distribution/profitability.js | 4 +- website/scripts/options/network.js | 24 ++- website/scripts/options/network/op-return.js | 192 ++++++++++++++++++ website_next/explore/block/format.js | 26 +++ website_next/explore/block/header/index.js | 29 ++- website_next/explore/block/header/style.css | 42 ++-- .../explore/block/preview/heatmap/fees.js | 12 +- .../explore/block/preview/heatmap/pack.js | 17 +- website_next/explore/block/receipt/index.js | 6 +- website_next/explore/block/receipt/style.css | 24 ++- 19 files changed, 510 insertions(+), 108 deletions(-) create mode 100644 website/scripts/options/network/op-return.js diff --git a/crates/brk_client/src/lib.rs b/crates/brk_client/src/lib.rs index fedae26bd..1c670e130 100644 --- a/crates/brk_client/src/lib.rs +++ b/crates/brk_client/src/lib.rs @@ -2291,6 +2291,28 @@ impl CapLossMvrvPriceProfitPattern { } } +/// Pattern struct for repeated tree structure. +pub struct CarrierDataOutputPostPattern { + pub carrier_tx_count: AverageBlockCumulativeSumPattern, + pub carrier_vsize: AverageBlockCumulativeSumPattern, + pub data_share: BpsPercentRatioPattern2, + pub output_count: AverageBlockCumulativeSumPattern, + pub post_op_return_bytes: AverageBlockCumulativeSumPattern, +} + +impl CarrierDataOutputPostPattern { + /// Create a new pattern node with accumulated series name. + pub fn new(client: Arc, acc: String) -> Self { + Self { + carrier_tx_count: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "carrier_tx_count")), + carrier_vsize: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "carrier_vsize")), + data_share: BpsPercentRatioPattern2::new(client.clone(), _m(&acc, "data_share")), + output_count: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "output_count")), + post_op_return_bytes: AverageBlockCumulativeSumPattern::new(client.clone(), _m(&acc, "post_op_return_bytes")), + } + } +} + /// Pattern struct for repeated tree structure. pub struct CentsToUsdPattern4 { pub cents: SeriesPattern1, @@ -5388,6 +5410,7 @@ pub struct SeriesTree_OpReturn_Total { pub post_op_return_bytes: AverageBlockCumulativeSumPattern, pub carrier_tx_count: AverageBlockCumulativeSumPattern, pub carrier_vsize: AverageBlockCumulativeSumPattern, + pub data_share: BpsPercentRatioPattern2, } impl SeriesTree_OpReturn_Total { @@ -5396,6 +5419,7 @@ impl SeriesTree_OpReturn_Total { post_op_return_bytes: AverageBlockCumulativeSumPattern::new(client.clone(), "op_return_post_op_return_bytes".to_string()), carrier_tx_count: AverageBlockCumulativeSumPattern::new(client.clone(), "op_return_carrier_tx_count".to_string()), carrier_vsize: AverageBlockCumulativeSumPattern::new(client.clone(), "op_return_carrier_vsize".to_string()), + data_share: BpsPercentRatioPattern2::new(client.clone(), "op_return_data_share".to_string()), } } } @@ -5459,19 +5483,19 @@ impl SeriesTree_OpReturn_ByKind { /// Series tree node. pub struct SeriesTree_OpReturn_Policy { - pub standard: CarrierOutputPostPattern, - pub oversized: CarrierOutputPostPattern, - pub multiple: CarrierOutputPostPattern, - pub pre_v30_nonstandard: CarrierOutputPostPattern, + pub standard: CarrierDataOutputPostPattern, + pub oversized: CarrierDataOutputPostPattern, + pub multiple: CarrierDataOutputPostPattern, + pub pre_v30_nonstandard: CarrierDataOutputPostPattern, } impl SeriesTree_OpReturn_Policy { pub fn new(client: Arc, base_path: String) -> Self { Self { - standard: CarrierOutputPostPattern::new(client.clone(), "op_return_policy_standard".to_string()), - oversized: CarrierOutputPostPattern::new(client.clone(), "op_return_policy_oversized".to_string()), - multiple: CarrierOutputPostPattern::new(client.clone(), "op_return_policy_multiple".to_string()), - pre_v30_nonstandard: CarrierOutputPostPattern::new(client.clone(), "op_return_policy_pre_v30_nonstandard".to_string()), + standard: CarrierDataOutputPostPattern::new(client.clone(), "op_return_policy_standard".to_string()), + oversized: CarrierDataOutputPostPattern::new(client.clone(), "op_return_policy_oversized".to_string()), + multiple: CarrierDataOutputPostPattern::new(client.clone(), "op_return_policy_multiple".to_string()), + pre_v30_nonstandard: CarrierDataOutputPostPattern::new(client.clone(), "op_return_policy_pre_v30_nonstandard".to_string()), } } } diff --git a/crates/brk_computer/src/lib.rs b/crates/brk_computer/src/lib.rs index f7cf80f89..44146b707 100644 --- a/crates/brk_computer/src/lib.rs +++ b/crates/brk_computer/src/lib.rs @@ -362,7 +362,7 @@ impl Computer { let op_return = scope.spawn(|| { timed("Computed OP_RETURN", || { - self.op_return.compute(indexer, exit) + self.op_return.compute(indexer, &self.blocks, exit) }) }); diff --git a/crates/brk_computer/src/op_return/compute.rs b/crates/brk_computer/src/op_return/compute.rs index c56f71892..838fe281d 100644 --- a/crates/brk_computer/src/op_return/compute.rs +++ b/crates/brk_computer/src/op_return/compute.rs @@ -1,9 +1,13 @@ use brk_error::Result; use brk_indexer::Indexer; -use brk_types::{OpReturnKind, VSize}; +use brk_types::{BasisPoints16, Height, OpReturnKind, StoredU64, VSize}; use vecdb::{AnyVec, Exit, ReadableVec, VecIndex}; use super::{Vecs, vecs::Totals}; +use crate::{ + blocks, + internal::{PercentPerBlock, RatioU64Bp16}, +}; const KIND_COUNT: usize = OpReturnKind::Unknown as usize + 1; const OLD_STANDARD_MAX_POST_OP_RETURN_BYTES: u64 = 82; @@ -40,7 +44,12 @@ impl Carrier { } impl Vecs { - pub(crate) fn compute(&mut self, indexer: &Indexer, exit: &Exit) -> Result<()> { + pub(crate) fn compute( + &mut self, + indexer: &Indexer, + blocks: &blocks::Vecs, + exit: &Exit, + ) -> Result<()> { self.db.sync_bg_tasks()?; let starting_lengths = indexer.safe_lengths(); @@ -132,6 +141,23 @@ impl Vecs { } self.compute_cumulative(starting_lengths.height, exit)?; + let block_size = &blocks.size.size.cumulative.height; + compute_data_share( + starting_lengths.height, + &mut self.total.data_share, + &self.total.metrics.post_op_return_bytes.cumulative.height, + block_size, + exit, + )?; + for policy in self.policy.iter_mut() { + compute_data_share( + starting_lengths.height, + &mut policy.data_share, + &policy.metrics.total.post_op_return_bytes.cumulative.height, + block_size, + exit, + )?; + } let exit = exit.clone(); self.db.run_bg(move |db| { @@ -142,6 +168,16 @@ impl Vecs { } } +fn compute_data_share( + max_from: Height, + target: &mut PercentPerBlock, + data: &impl ReadableVec, + block_size: &impl ReadableVec, + exit: &Exit, +) -> Result<()> { + target.compute_binary::(max_from, data, block_size, exit) +} + fn finalize_transaction( total: &mut Totals, by_kind: &mut [Totals; KIND_COUNT], diff --git a/crates/brk_computer/src/op_return/import.rs b/crates/brk_computer/src/op_return/import.rs index 450d1f392..564ca2618 100644 --- a/crates/brk_computer/src/op_return/import.rs +++ b/crates/brk_computer/src/op_return/import.rs @@ -3,7 +3,7 @@ use std::path::Path; use brk_error::Result; use brk_types::Version; -use super::{ByKind, Metrics, Policy, TotalMetrics, Vecs}; +use super::{ByKind, Metrics, Policy, Total, Vecs}; use crate::{ indexes, internal::{ @@ -20,7 +20,7 @@ impl Vecs { cached_starts: &Windows<&WindowStartVec>, ) -> Result { let db = open_db(parent_path, super::DB_NAME, 1_000_000)?; - let total = TotalMetrics::forced_import(&db, "op_return", version, indexes, cached_starts)?; + let total = Total::forced_import(&db, "op_return", version, indexes, cached_starts)?; let by_kind = ByKind::try_new(|_, name| { Metrics::forced_import( &db, diff --git a/crates/brk_computer/src/op_return/mod.rs b/crates/brk_computer/src/op_return/mod.rs index a46ace6b1..3d913d62f 100644 --- a/crates/brk_computer/src/op_return/mod.rs +++ b/crates/brk_computer/src/op_return/mod.rs @@ -4,6 +4,6 @@ mod import; mod vecs; pub use by_kind::ByKind; -pub use vecs::{Metrics, Policy, TotalMetrics, Vecs}; +pub use vecs::{Metrics, Policy, Total, Vecs}; pub const DB_NAME: &str = "op_return"; diff --git a/crates/brk_computer/src/op_return/vecs.rs b/crates/brk_computer/src/op_return/vecs.rs index fe50c58d8..3ff31a597 100644 --- a/crates/brk_computer/src/op_return/vecs.rs +++ b/crates/brk_computer/src/op_return/vecs.rs @@ -1,12 +1,13 @@ use brk_error::Result; use brk_traversable::Traversable; -use brk_types::{Height, StoredU64, VSize, Version}; +use brk_types::{BasisPoints16, Height, StoredU64, VSize, Version}; +use derive_more::{Deref, DerefMut}; use vecdb::{AnyStoredVec, AnyVec, Database, Exit, Rw, StorageMode, WritableVec}; use super::ByKind; use crate::{ indexes, - internal::{PerBlockCumulativeRolling, WindowStartVec, Windows}, + internal::{PerBlockCumulativeRolling, PercentPerBlock, WindowStartVec, Windows}, }; pub type Series = PerBlockCumulativeRolling; @@ -112,6 +113,35 @@ impl TotalMetrics { } } +#[derive(Deref, DerefMut, Traversable)] +pub struct Total { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub metrics: TotalMetrics, + pub data_share: PercentPerBlock, +} + +impl Total { + pub(crate) fn forced_import( + db: &Database, + prefix: &str, + version: Version, + indexes: &indexes::Vecs, + cached_starts: &Windows<&WindowStartVec>, + ) -> Result { + Ok(Self { + metrics: TotalMetrics::forced_import(db, prefix, version, indexes, cached_starts)?, + data_share: PercentPerBlock::forced_import( + db, + &format!("{prefix}_data_share"), + version, + indexes, + )?, + }) + } +} + #[derive(Traversable)] pub struct Metrics { pub output_count: Series, @@ -175,12 +205,41 @@ impl Metrics { } } +#[derive(Deref, DerefMut, Traversable)] +pub struct PolicyMetrics { + #[deref] + #[deref_mut] + #[traversable(flatten)] + pub metrics: Metrics, + pub data_share: PercentPerBlock, +} + +impl PolicyMetrics { + fn forced_import( + db: &Database, + prefix: &str, + version: Version, + indexes: &indexes::Vecs, + cached_starts: &Windows<&WindowStartVec>, + ) -> Result { + Ok(Self { + metrics: Metrics::forced_import(db, prefix, version, indexes, cached_starts)?, + data_share: PercentPerBlock::forced_import( + db, + &format!("{prefix}_data_share"), + version, + indexes, + )?, + }) + } +} + #[derive(Traversable)] pub struct Policy { - pub standard: Metrics, - pub oversized: Metrics, - pub multiple: Metrics, - pub pre_v30_nonstandard: Metrics, + pub standard: PolicyMetrics, + pub oversized: PolicyMetrics, + pub multiple: PolicyMetrics, + pub pre_v30_nonstandard: PolicyMetrics, } impl Policy { @@ -191,7 +250,7 @@ impl Policy { cached_starts: &Windows<&WindowStartVec>, ) -> Result { let import = |name| { - Metrics::forced_import( + PolicyMetrics::forced_import( db, &format!("op_return_policy_{name}"), version, @@ -208,7 +267,7 @@ impl Policy { }) } - fn iter(&self) -> impl Iterator { + fn iter(&self) -> impl Iterator { [ &self.standard, &self.oversized, @@ -218,7 +277,7 @@ impl Policy { .into_iter() } - fn iter_mut(&mut self) -> impl Iterator { + pub(super) fn iter_mut(&mut self) -> impl Iterator { [ &mut self.standard, &mut self.oversized, @@ -233,18 +292,22 @@ impl Policy { pub struct Vecs { #[traversable(skip)] pub(crate) db: Database, - pub total: TotalMetrics, + pub total: Total, pub by_kind: ByKind>, pub policy: Policy, } impl Vecs { pub(crate) fn min_len(&self) -> usize { - self.by_kind + let len = self + .by_kind .iter() - .chain(self.policy.iter()) .map(Metrics::len) - .fold(self.total.len(), usize::min) + .fold(self.total.len(), usize::min); + self.policy + .iter() + .map(|metrics| metrics.len()) + .fold(len, usize::min) } pub(crate) fn validate_and_truncate(&mut self, version: Version, height: Height) -> Result<()> { diff --git a/modules/brk-client/index.js b/modules/brk-client/index.js index d9e684447..e404ef363 100644 --- a/modules/brk-client/index.js +++ b/modules/brk-client/index.js @@ -3820,6 +3820,31 @@ function createCapLossMvrvPriceProfitPattern(client, acc) { }; } +/** + * @typedef {Object} CarrierDataOutputPostPattern + * @property {AverageBlockCumulativeSumPattern} carrierTxCount + * @property {AverageBlockCumulativeSumPattern} carrierVsize + * @property {BpsPercentRatioPattern2} dataShare + * @property {AverageBlockCumulativeSumPattern} outputCount + * @property {AverageBlockCumulativeSumPattern} postOpReturnBytes + */ + +/** + * Create a CarrierDataOutputPostPattern pattern node + * @param {BrkClient} client + * @param {string} acc - Accumulated series name + * @returns {CarrierDataOutputPostPattern} + */ +function createCarrierDataOutputPostPattern(client, acc) { + return { + carrierTxCount: createAverageBlockCumulativeSumPattern(client, _m(acc, 'carrier_tx_count')), + carrierVsize: createAverageBlockCumulativeSumPattern(client, _m(acc, 'carrier_vsize')), + dataShare: createBpsPercentRatioPattern2(client, _m(acc, 'data_share')), + outputCount: createAverageBlockCumulativeSumPattern(client, _m(acc, 'output_count')), + postOpReturnBytes: createAverageBlockCumulativeSumPattern(client, _m(acc, 'post_op_return_bytes')), + }; +} + /** * @typedef {Object} CentsToUsdPattern4 * @property {SeriesPattern1} cents @@ -6192,6 +6217,7 @@ function createTransferPattern(client, acc) { * @property {AverageBlockCumulativeSumPattern} postOpReturnBytes * @property {AverageBlockCumulativeSumPattern} carrierTxCount * @property {AverageBlockCumulativeSumPattern} carrierVsize + * @property {BpsPercentRatioPattern2} dataShare */ /** @@ -6223,10 +6249,10 @@ function createTransferPattern(client, acc) { /** * @typedef {Object} SeriesTree_OpReturn_Policy - * @property {CarrierOutputPostPattern} standard - * @property {CarrierOutputPostPattern} oversized - * @property {CarrierOutputPostPattern} multiple - * @property {CarrierOutputPostPattern} preV30Nonstandard + * @property {CarrierDataOutputPostPattern} standard + * @property {CarrierDataOutputPostPattern} oversized + * @property {CarrierDataOutputPostPattern} multiple + * @property {CarrierDataOutputPostPattern} preV30Nonstandard */ /** @@ -9988,6 +10014,7 @@ class BrkClient extends BrkClientBase { postOpReturnBytes: createAverageBlockCumulativeSumPattern(this, 'op_return_post_op_return_bytes'), carrierTxCount: createAverageBlockCumulativeSumPattern(this, 'op_return_carrier_tx_count'), carrierVsize: createAverageBlockCumulativeSumPattern(this, 'op_return_carrier_vsize'), + dataShare: createBpsPercentRatioPattern2(this, 'op_return_data_share'), }, byKind: { runes: createCarrierOutputPostPattern(this, 'op_return_runes'), @@ -10015,10 +10042,10 @@ class BrkClient extends BrkClientBase { unknown: createCarrierOutputPostPattern(this, 'op_return_unknown'), }, policy: { - standard: createCarrierOutputPostPattern(this, 'op_return_policy_standard'), - oversized: createCarrierOutputPostPattern(this, 'op_return_policy_oversized'), - multiple: createCarrierOutputPostPattern(this, 'op_return_policy_multiple'), - preV30Nonstandard: createCarrierOutputPostPattern(this, 'op_return_policy_pre_v30_nonstandard'), + standard: createCarrierDataOutputPostPattern(this, 'op_return_policy_standard'), + oversized: createCarrierDataOutputPostPattern(this, 'op_return_policy_oversized'), + multiple: createCarrierDataOutputPostPattern(this, 'op_return_policy_multiple'), + preV30Nonstandard: createCarrierDataOutputPostPattern(this, 'op_return_policy_pre_v30_nonstandard'), }, }, mining: { diff --git a/packages/brk_client/brk_client/__init__.py b/packages/brk_client/brk_client/__init__.py index 2b630e05e..f70aed35d 100644 --- a/packages/brk_client/brk_client/__init__.py +++ b/packages/brk_client/brk_client/__init__.py @@ -3577,6 +3577,17 @@ class CapLossMvrvPriceProfitPattern: self.price: BpsCentsRatioSatsUsdPattern = BpsCentsRatioSatsUsdPattern(client, _m(acc, 'realized_price')) self.profit: BlockCumulativeSumPattern = BlockCumulativeSumPattern(client, _m(acc, 'realized_profit')) +class CarrierDataOutputPostPattern: + """Pattern struct for repeated tree structure.""" + + def __init__(self, client: BrkClient, acc: str): + """Create pattern node with accumulated series name.""" + self.carrier_tx_count: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _m(acc, 'carrier_tx_count')) + self.carrier_vsize: AverageBlockCumulativeSumPattern[VSize] = AverageBlockCumulativeSumPattern(client, _m(acc, 'carrier_vsize')) + self.data_share: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, _m(acc, 'data_share')) + self.output_count: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _m(acc, 'output_count')) + self.post_op_return_bytes: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, _m(acc, 'post_op_return_bytes')) + class CentsToUsdPattern4: """Pattern struct for repeated tree structure.""" @@ -5064,6 +5075,7 @@ class SeriesTree_OpReturn_Total: self.post_op_return_bytes: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, 'op_return_post_op_return_bytes') self.carrier_tx_count: AverageBlockCumulativeSumPattern[StoredU64] = AverageBlockCumulativeSumPattern(client, 'op_return_carrier_tx_count') self.carrier_vsize: AverageBlockCumulativeSumPattern[VSize] = AverageBlockCumulativeSumPattern(client, 'op_return_carrier_vsize') + self.data_share: BpsPercentRatioPattern2 = BpsPercentRatioPattern2(client, 'op_return_data_share') class SeriesTree_OpReturn_ByKind: """Series tree node.""" @@ -5097,10 +5109,10 @@ class SeriesTree_OpReturn_Policy: """Series tree node.""" def __init__(self, client: BrkClient, base_path: str = ''): - self.standard: CarrierOutputPostPattern = CarrierOutputPostPattern(client, 'op_return_policy_standard') - self.oversized: CarrierOutputPostPattern = CarrierOutputPostPattern(client, 'op_return_policy_oversized') - self.multiple: CarrierOutputPostPattern = CarrierOutputPostPattern(client, 'op_return_policy_multiple') - self.pre_v30_nonstandard: CarrierOutputPostPattern = CarrierOutputPostPattern(client, 'op_return_policy_pre_v30_nonstandard') + self.standard: CarrierDataOutputPostPattern = CarrierDataOutputPostPattern(client, 'op_return_policy_standard') + self.oversized: CarrierDataOutputPostPattern = CarrierDataOutputPostPattern(client, 'op_return_policy_oversized') + self.multiple: CarrierDataOutputPostPattern = CarrierDataOutputPostPattern(client, 'op_return_policy_multiple') + self.pre_v30_nonstandard: CarrierDataOutputPostPattern = CarrierDataOutputPostPattern(client, 'op_return_policy_pre_v30_nonstandard') class SeriesTree_OpReturn: """Series tree node.""" diff --git a/website/scripts/options/distribution/index.js b/website/scripts/options/distribution/index.js index b092bbe3f..c555ba227 100644 --- a/website/scripts/options/distribution/index.js +++ b/website/scripts/options/distribution/index.js @@ -164,7 +164,7 @@ export function createCohortFolderWithNupl(cohort) { createValuationSectionFull({ cohort, title }), createPricesSectionFull({ cohort, title }), createCostBasisSectionWithPercentiles({ cohort, title }), - createProfitabilitySection({ cohort, title }), + createProfitabilitySectionFull({ cohort, title }), createActivitySection({ cohort, title }), ], }; diff --git a/website/scripts/options/distribution/profitability.js b/website/scripts/options/distribution/profitability.js index cd6b7c9cc..7fb008ecf 100644 --- a/website/scripts/options/distribution/profitability.js +++ b/website/scripts/options/distribution/profitability.js @@ -808,8 +808,8 @@ export function createProfitabilitySectionAll({ cohort, title }) { } /** - * Section for Full cohorts (STH) - * @param {{ cohort: CohortFull, title: (name: string) => string }} args + * Section for cohorts with full realized and unrealized profitability data. + * @param {{ cohort: { tree: { unrealized: FullRelativePattern, realized: RealizedPattern } }, title: (name: string) => string }} args * @returns {PartialOptionsGroup} */ export function createProfitabilitySectionFull({ cohort, title }) { diff --git a/website/scripts/options/network.js b/website/scripts/options/network.js index b752366d1..f60ab2ef5 100644 --- a/website/scripts/options/network.js +++ b/website/scripts/options/network.js @@ -34,6 +34,7 @@ import { exposedSubtree, reusedSubtree, } from "./shared.js"; +import { createOpReturnSection } from "./network/op-return.js"; /** * Create Network section @@ -829,8 +830,8 @@ export function createNetworkSection() { * @template {string} K * @param {Object} args * @param {string} args.label - Singular noun for count/tree labels ("Output" / "Prev-Out") - * @param {Readonly>>} args.count - * @param {Readonly>>} args.txCount + * @param {Readonly>>} args.count + * @param {Readonly>>} args.txCount * @param {Readonly>} args.share * @param {Readonly>} args.txShare * @param {ReadonlyArray<{key: K, name: string, color: Color, defaultActive: boolean}>} args.types @@ -845,6 +846,15 @@ export function createNetworkSection() { types, }) => { const lowerLabel = label.toLowerCase(); + const countTypes = /** @type {const} */ ([ + ...types, + { + key: "all", + name: "All", + color: colors.default, + defaultActive: false, + }, + ]); return [ { name: "Compare", @@ -855,7 +865,7 @@ export function createNetworkSection() { ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} ${label} Count by Type`, - bottom: types.map((t) => + bottom: countTypes.map((t) => line({ series: count[t.key].sum[w.key], name: t.name, @@ -868,7 +878,7 @@ export function createNetworkSection() { { name: "Cumulative", title: `Cumulative ${label} Count by Type`, - bottom: types.map((t) => + bottom: countTypes.map((t) => line({ series: count[t.key].cumulative, name: t.name, @@ -898,7 +908,7 @@ export function createNetworkSection() { ...ROLLING_WINDOWS.map((w) => ({ name: w.name, title: `${w.title} Transactions by ${label} Type`, - bottom: types.map((t) => + bottom: countTypes.map((t) => line({ series: txCount[t.key].sum[w.key], name: t.name, @@ -911,7 +921,7 @@ export function createNetworkSection() { { name: "Cumulative", title: `Cumulative Transactions by ${label} Type`, - bottom: types.map((t) => + bottom: countTypes.map((t) => line({ series: txCount[t.key].cumulative, name: t.name, @@ -1231,6 +1241,8 @@ export function createNetworkSection() { ], }, + createOpReturnSection(), + // UTXOs { name: "UTXOs", diff --git a/website/scripts/options/network/op-return.js b/website/scripts/options/network/op-return.js new file mode 100644 index 000000000..39a598319 --- /dev/null +++ b/website/scripts/options/network/op-return.js @@ -0,0 +1,192 @@ +import { colors } from "../../utils/colors.js"; +import { brk } from "../../utils/client.js"; +import { Unit } from "../../utils/units.js"; +import { chartsFromCount, line } from "../series.js"; +import { groupedWindowsCumulative } from "../shared.js"; + +const TYPE_DEFINITIONS = /** @type {const} */ ([ + { key: "runes", name: "Runes", defaultActive: true }, + { key: "veriBlock", name: "VeriBlock", defaultActive: true }, + { key: "omni", name: "Omni", defaultActive: true }, + { key: "stacks", name: "Stacks", defaultActive: true }, + { key: "blockstack", name: "Blockstack", defaultActive: false }, + { key: "colu", name: "Colu", defaultActive: false }, + { key: "openAssets", name: "Open Assets", defaultActive: false }, + { key: "komodo", name: "Komodo", defaultActive: false }, + { key: "coinSpark", name: "CoinSpark", defaultActive: false }, + { key: "poet", name: "Po.et", defaultActive: false }, + { key: "docproof", name: "Docproof", defaultActive: false }, + { key: "openTimestamps", name: "OpenTimestamps", defaultActive: true }, + { key: "factom", name: "Factom", defaultActive: false }, + { key: "eternityWall", name: "Eternity Wall", defaultActive: false }, + { key: "memo", name: "Memo", defaultActive: false }, + { key: "bitproof", name: "Bitproof", defaultActive: false }, + { key: "ascribe", name: "Ascribe", defaultActive: false }, + { key: "stampery", name: "Stampery", defaultActive: false }, + { key: "epobc", name: "EPOBC", defaultActive: false }, + { key: "bareHash", name: "Bare Hash", defaultActive: false }, + { key: "text", name: "Text", defaultActive: true }, + { key: "empty", name: "Empty", defaultActive: false }, + { key: "unknown", name: "Unknown", defaultActive: true }, +]); + +const METRICS = /** @type {const} */ ([ + { + key: "carrierTxCount", + name: "Transactions", + title: "Carrier Transaction Count", + unit: Unit.count, + }, + { + key: "outputCount", + name: "Outputs", + title: "Output Count", + unit: Unit.count, + }, + { + key: "carrierVsize", + name: "vBytes", + title: "Carrier Transaction vBytes", + unit: Unit.vb, + }, + { + key: "postOpReturnBytes", + name: "Data", + title: "Data Bytes", + unit: Unit.bytes, + }, +]); + +/** @typedef {(typeof brk.series.opReturn.byKind)[keyof typeof brk.series.opReturn.byKind]} OpReturnMetrics */ + +/** + * @param {Object} args + * @param {readonly { name: string, color: Color, defaultActive: boolean, pattern: OpReturnMetrics }[]} args.list + * @param {string} args.category + * @returns {PartialOptionsTree} + */ +function createComparisons({ list, category }) { + return METRICS.map((metric) => ({ + name: metric.name, + tree: groupedWindowsCumulative({ + list, + title: (title) => `OP_RETURN ${title}`, + metricTitle: `${metric.title} by ${category}`, + getWindowSeries: (entry, window) => + entry.pattern[metric.key].sum[window], + getCumulativeSeries: (entry) => + entry.pattern[metric.key].cumulative, + seriesFn: line, + unit: metric.unit, + }), + })); +} + +/** @returns {PartialOptionsGroup} */ +export function createOpReturnSection() { + const opReturn = brk.series.opReturn; + const types = TYPE_DEFINITIONS.map((type, index) => ({ + ...type, + color: colors.at(index, TYPE_DEFINITIONS.length), + pattern: opReturn.byKind[type.key], + })); + const policies = [ + { + name: "Standard", + color: colors.profit, + defaultActive: true, + pattern: opReturn.policy.standard, + }, + { + name: "Oversized", + color: colors.loss, + defaultActive: true, + pattern: opReturn.policy.oversized, + }, + { + name: "Multiple Outputs", + color: colors.bitcoin, + defaultActive: true, + pattern: opReturn.policy.multiple, + }, + { + name: "Pre-v30 Nonstandard", + color: colors.gray, + defaultActive: true, + pattern: opReturn.policy.preV30Nonstandard, + }, + ]; + + return { + name: "OP_RETURN", + tree: [ + { + name: "Total", + tree: [ + { + name: "Transactions", + tree: chartsFromCount({ + pattern: opReturn.total.carrierTxCount, + metric: "OP_RETURN Carrier Transaction Count", + unit: Unit.count, + color: colors.scriptType.opReturn, + }), + }, + { + name: "vBytes", + tree: chartsFromCount({ + pattern: opReturn.total.carrierVsize, + metric: "OP_RETURN Carrier Transaction vBytes", + unit: Unit.vb, + color: colors.scriptType.opReturn, + }), + }, + { + name: "Data", + tree: chartsFromCount({ + pattern: opReturn.total.postOpReturnBytes, + metric: "OP_RETURN Data Bytes", + unit: Unit.bytes, + color: colors.scriptType.opReturn, + }), + }, + { + name: "Share", + title: "Cumulative OP_RETURN Data Share of Blockchain Size", + bottom: [ + line({ + series: opReturn.total.dataShare.percent, + name: "OP_RETURN Data", + color: colors.scriptType.opReturn, + unit: Unit.percentage, + }), + ], + }, + ], + }, + { + name: "Type", + tree: createComparisons({ list: types, category: "Type" }), + }, + { + name: "Policy", + tree: [ + { + name: "Share", + title: "Cumulative OP_RETURN Data Share of Blockchain Size by Policy", + bottom: policies.map(({ name, color, defaultActive, pattern }) => + line({ + series: pattern.dataShare.percent, + name, + color, + defaultActive, + unit: Unit.percentage, + }), + ), + }, + ...createComparisons({ list: policies, category: "Policy" }), + ], + }, + ], + }; +} diff --git a/website_next/explore/block/format.js b/website_next/explore/block/format.js index 00f78aeb7..27cdc64b8 100644 --- a/website_next/explore/block/format.js +++ b/website_next/explore/block/format.js @@ -3,6 +3,15 @@ export const HALVING_EPOCH_BLOCKS = 210_000; export const MAX_BLOCK_WEIGHT = 4_000_000; +const RELATIVE_TIME = new Intl.RelativeTimeFormat(undefined); +const RELATIVE_UNITS = /** @type {const} */ ([ + ["year", 31_557_600], + ["month", 2_629_800], + ["day", 86_400], + ["hour", 3_600], + ["minute", 60], +]); + /** @param {number} value */ export function formatNumber(value) { return value.toLocaleString(); @@ -22,6 +31,23 @@ export function formatDateTime(unixSeconds) { }); } +/** @param {number} unixSeconds */ +export function formatDateAndAge(unixSeconds) { + const date = new Date(unixSeconds * 1_000).toLocaleDateString(undefined, { + dateStyle: "medium", + }); + const difference = unixSeconds - Date.now() / 1_000; + const absolute = Math.abs(difference); + + if (absolute < 60) return `${date} · just now`; + + const [unit, seconds] = RELATIVE_UNITS.find(([, duration]) => { + return absolute >= duration; + }) ?? RELATIVE_UNITS.at(-1); + + return `${date} · ${RELATIVE_TIME.format(Math.trunc(difference / seconds), unit)}`; +} + /** @param {number} bytes */ export function formatBytes(bytes) { return bytes >= 1_000_000 diff --git a/website_next/explore/block/header/index.js b/website_next/explore/block/header/index.js index 765a773a6..45440e6bb 100644 --- a/website_next/explore/block/header/index.js +++ b/website_next/explore/block/header/index.js @@ -1,5 +1,5 @@ import { createUsdAmount, renderUsdAmount } from "../../../usd/index.js"; -import { formatDateTime } from "../format.js"; +import { formatDateAndAge } from "../format.js"; import { createBlockTitle } from "../title/index.js"; /** @typedef {import("../../../modules/brk-client/index.js").BlockInfoV1} Block */ @@ -24,38 +24,37 @@ function createHashElement(hash) { /** @param {Node[]} [actions] */ export function createBlockHeader(actions = []) { const element = document.createElement("header"); - const titleRow = document.createElement("div"); - const main = document.createElement("div"); const title = document.createElement("h1"); - const side = document.createElement("div"); const actionList = document.createElement("div"); const date = document.createElement("time"); const hash = document.createElement("p"); const price = createUsdAmount("output", 0, { + size: "title", tone: "positive", }); + let timestamp = 0; + let dateTimer = 0; element.dataset.blockHeader = ""; title.dataset.blockTitleText = ""; - main.dataset.blockMain = ""; - titleRow.dataset.blockTitle = ""; - side.dataset.blockSide = ""; - actionList.dataset.blockActions = ""; - date.dataset.blockDate = ""; - hash.dataset.blockHashLine = ""; actionList.append(...actions); - side.append(date, price, actionList); - main.append(title, hash); - titleRow.append(main, side); - element.append(titleRow); + element.append(title, price, hash, actionList, date); + + function refreshDate() { + date.textContent = formatDateAndAge(timestamp); + dateTimer = window.setTimeout(refreshDate, 60_000); + } /** @param {Block} block */ function update(block) { title.replaceChildren(...createBlockTitle(block.height)); date.dateTime = new Date(block.timestamp * 1_000).toISOString(); - date.textContent = formatDateTime(block.timestamp); + timestamp = block.timestamp; + window.clearTimeout(dateTimer); + refreshDate(); hash.replaceChildren(createHashElement(block.id)); renderUsdAmount(price, block.extras.price, { + size: "title", tone: "positive", }); } diff --git a/website_next/explore/block/header/style.css b/website_next/explore/block/header/style.css index db7187946..9eb946628 100644 --- a/website_next/explore/block/header/style.css +++ b/website_next/explore/block/header/style.css @@ -5,44 +5,41 @@ --block-title-label-size: 2.5rem; display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 0.15rem 1rem; + align-items: baseline; padding-bottom: 1.25rem; [data-dim] { opacity: 0.5; } - [data-block-title] { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - gap: 0.35rem 1rem; - align-items: start; + > h1 { min-width: 0; } - [data-block-main] { - display: grid; - gap: 0.15rem; - min-width: 0; + > output { + justify-self: end; + white-space: nowrap; } - [data-block-date], - [data-block-hash-line] { + > p, + > time { color: var(--gray); } - [data-block-date] { + > p { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; - text-align: right; } - [data-block-side] { - display: grid; - gap: 0.15rem; - justify-items: end; - min-width: max-content; + > time { + white-space: nowrap; } - [data-block-actions] { + > div { display: flex; flex-wrap: wrap; gap: 0.5rem; @@ -60,11 +57,4 @@ padding: 0.25rem 0.375rem; } } - - [data-block-hash-line] { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } } diff --git a/website_next/explore/block/preview/heatmap/fees.js b/website_next/explore/block/preview/heatmap/fees.js index c7147b52c..a11f7941a 100644 --- a/website_next/explore/block/preview/heatmap/fees.js +++ b/website_next/explore/block/preview/heatmap/fees.js @@ -34,12 +34,8 @@ export function orderTransactions(weights, feeRates) { * @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 }; + return { + layouts: packCells(cells, columns, rows), + resolvedCells: cells, + }; } diff --git a/website_next/explore/block/preview/heatmap/pack.js b/website_next/explore/block/preview/heatmap/pack.js index 75e4b9bc7..bc5032657 100644 --- a/website_next/explore/block/preview/heatmap/pack.js +++ b/website_next/explore/block/preview/heatmap/pack.js @@ -2,7 +2,7 @@ * @param {readonly PackCell[]} cells * @param {number} columns * @param {number} rows - * @returns {PackLayout[] | null} + * @returns {PackLayout[]} */ export function packCells(cells, columns, rows) { const occupied = new Uint8Array(columns * rows); @@ -10,8 +10,8 @@ export function packCells(cells, columns, rows) { const layouts = []; for (const cell of cells) { - const span = Math.min(cell.span, columns); - const position = findPosition( + let span = Math.min(cell.span, columns); + let position = findPosition( occupied, columns, rows, @@ -19,7 +19,16 @@ export function packCells(cells, columns, rows) { cursors[span], ); - if (position === null) return null; + while (position === null) { + span -= 1; + position = findPosition( + occupied, + columns, + rows, + span, + cursors[span], + ); + } fillCells(occupied, columns, position.x, position.y, span); cursors[span] = position.index + span; diff --git a/website_next/explore/block/receipt/index.js b/website_next/explore/block/receipt/index.js index 277ce6dbb..655ed70d9 100644 --- a/website_next/explore/block/receipt/index.js +++ b/website_next/explore/block/receipt/index.js @@ -76,6 +76,7 @@ function createReceiptBrand() { /** @param {Block} block */ function openReceiptDialog(block) { const dialog = document.createElement("dialog"); + const content = document.createElement("main"); const paper = document.createElement("article"); const controls = document.createElement("footer"); const print = document.createElement("button"); @@ -104,14 +105,15 @@ function openReceiptDialog(block) { createReceiptQr(block, url), createReceiptBrand(), ); - dialog.append(paper, controls); + content.append(paper); + dialog.append(content, controls); print.addEventListener("click", () => { window.print(); }); openDialog(dialog, document.body); dialog.focus({ preventScroll: true }); - dialog.scrollTop = 0; + content.scrollTop = 0; } export function createBlockReceipt() { diff --git a/website_next/explore/block/receipt/style.css b/website_next/explore/block/receipt/style.css index 89fbd082b..b6a27ef22 100644 --- a/website_next/explore/block/receipt/style.css +++ b/website_next/explore/block/receipt/style.css @@ -10,18 +10,25 @@ dialog[data-block-receipt] { position: fixed; inset: 0 0 auto; width: min(100% - 2rem, 28rem); + height: 100dvh; max-height: 100dvh; margin: 0 auto; border-radius: 0; - overflow-y: auto; - padding-block: var(--receipt-dialog-padding-block); + overflow: hidden; + padding: 0; color: var(--black); background: transparent; font-family: var(--font-mono); - overscroll-behavior: contain; - scrollbar-width: none; translate: none; + > main { + height: 100%; + overflow-y: auto; + padding-block: var(--receipt-dialog-padding-block); + overscroll-behavior: contain; + scrollbar-width: none; + } + :is( [data-receipt-section] > h3, [data-receipt-row] > span, @@ -190,7 +197,7 @@ dialog[data-block-receipt] { } [data-receipt-controls] { - position: fixed; + position: absolute; bottom: 0.5rem; left: 50%; z-index: 1; @@ -265,6 +272,7 @@ dialog[data-block-receipt] { position: static; display: block; width: var(--receipt-page-width); + height: auto; max-width: none; max-height: none; margin: 0; @@ -275,6 +283,12 @@ dialog[data-block-receipt] { overscroll-behavior: auto; } + dialog[data-block-receipt] > main { + height: auto; + overflow: visible; + padding: 0; + } + dialog[data-block-receipt]::backdrop { display: none; }