Skip to content

Commit e499df0

Browse files
committed
simplify: consolidate section helpers, satellite breakdown, comment trims
Post-/simplify review consolidations: OrdpoolBlocksRepository.ts - Collapse 4 nearly-identical section helpers (amountCols, feeCols, cat21Cols, runeCols) into one generic sectionCols<Section>(...) that takes a colPrefix, aliasPrefix, pick function, and field list. - inscriptionSizeCols now reuses sectionCols + the shared INSCRIPTION_SIZE_FIELDS constant. - Pre-compute ORDPOOL_STATS_INSERT_SQL at module load. The column list and placeholder list are constant; only the params array needs to materialise per call. Saves 2 .map() passes over a 90-element array per saveBlockOrdpoolStatsInDatabase invocation. - LEFT(?, 20) magic string → MOST_ACTIVE_MINT_TRUNC + TRUNCATED_PLACEHOLDER constants. truncated20 → truncatedMostActive (named for the use case). ordpool-statistics.api.ts - Replace getAtomicalOpsBreakdown + getCounterpartyMessagesBreakdown (90% identical) with a single getSatelliteBreakdown(table, col, alias). Drop ~50 lines of duplicate SQL templating and try/catch boilerplate. frontend ordpool-stats.component.helper.ts - Replace the long stat-type union in getTooltipContent with the existing OrdpoolStatisticResponse alias. - Drop the matchType JSDoc (says nothing the type system doesn't). - Drop the satellite-chart "follow-up backlog" comment. ordpool-statistics-interface.ts - Trim UI prescription from RuneActivityStatistic doc comment; interface describes data shape, consumer decides rendering.
1 parent c43faeb commit e499df0

4 files changed

Lines changed: 90 additions & 170 deletions

File tree

backend/src/api/explorer/_ordpool/ordpool-statistics-interface.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,8 @@ export interface Cat21StatStatistic extends BaseStatistic {
118118
cat21MaxFeeRate?: number | null;
119119
}
120120

121-
// Rune block aggregates per period — both overall + non-uncommon variants in
122-
// the same response. UI shows both lines together (don't hide UNCOMMON•GOODS,
123-
// it's the truth).
121+
// Rune block aggregates per period — both overall + non-uncommon variants
122+
// in the same response so consumers can render both series.
124123
export interface RuneActivityStatistic extends BaseStatistic {
125124
uniqueMints?: number;
126125
uniqueMintsNonUncommon?: number;

backend/src/api/explorer/_ordpool/ordpool-statistics.api.ts

Lines changed: 15 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ class OrdpoolStatisticsApi {
1818
const firstInscriptionHeight = getFirstInscriptionHeight(config.MEMPOOL.NETWORK);
1919
const sqlInterval = getSqlInterval(interval);
2020

21-
// Satellite-table-driven charts use their own JOIN target + extra
22-
// GROUP BY discriminator (operation / message_type). Build the query
23-
// separately rather than overloading the per-time aggregation path.
21+
// Satellite-table charts use their own JOIN target + an extra GROUP BY
22+
// discriminator (one series per operation / message_type).
2423
if (type === 'atomical-ops') {
25-
return this.getAtomicalOpsBreakdown(firstInscriptionHeight, sqlInterval, aggregation);
24+
return this.getSatelliteBreakdown(firstInscriptionHeight, sqlInterval, aggregation,
25+
'ordpool_stats_atomical_op', 'sat.operation', 'operation');
2626
}
2727
if (type === 'counterparty-messages') {
28-
return this.getCounterpartyMessagesBreakdown(firstInscriptionHeight, sqlInterval, aggregation);
28+
return this.getSatelliteBreakdown(firstInscriptionHeight, sqlInterval, aggregation,
29+
'ordpool_stats_counterparty', 'sat.message_type', 'messageType');
2930
}
3031

3132
const selectClause = this.getSelectClause(type);
@@ -50,51 +51,13 @@ class OrdpoolStatisticsApi {
5051
}
5152
}
5253

53-
/**
54-
* Per-operation breakdown for the atomical-ops chart. Joins
55-
* ordpool_stats_atomical_op (one row per atomical mint/update) and groups
56-
* by operation type AND time bucket, so the response is one row per
57-
* (period, operation) combination.
58-
*/
59-
private async getAtomicalOpsBreakdown(
54+
private async getSatelliteBreakdown(
6055
firstInscriptionHeight: number,
6156
sqlInterval: string,
62-
aggregation: Aggregation
63-
): Promise<OrdpoolStatisticResponse[]> {
64-
const groupByTime = this.getGroupByClause(aggregation).replace(/^GROUP BY/, '');
65-
const query = `
66-
SELECT
67-
MIN(b.height) AS minHeight,
68-
MAX(b.height) AS maxHeight,
69-
MIN(UNIX_TIMESTAMP(b.blockTimestamp)) AS minTime,
70-
MAX(UNIX_TIMESTAMP(b.blockTimestamp)) AS maxTime,
71-
ao.operation AS operation,
72-
COUNT(*) AS count
73-
FROM blocks b
74-
JOIN ordpool_stats_atomical_op ao ON ao.hash = b.hash
75-
WHERE b.height >= ${firstInscriptionHeight}
76-
AND b.blockTimestamp >= DATE_SUB(NOW(), INTERVAL ${sqlInterval})
77-
GROUP BY ${groupByTime}, ao.operation
78-
ORDER BY b.blockTimestamp DESC
79-
`;
80-
try {
81-
const [rows] : any[] = await DB.query(query);
82-
return rows;
83-
} catch (error) {
84-
logger.err(`Error executing atomical-ops query: ${error}`, 'Ordpool');
85-
throw error;
86-
}
87-
}
88-
89-
/**
90-
* Per-message-type breakdown for the counterparty-messages chart.
91-
* Same pattern as getAtomicalOpsBreakdown, joining
92-
* ordpool_stats_counterparty.
93-
*/
94-
private async getCounterpartyMessagesBreakdown(
95-
firstInscriptionHeight: number,
96-
sqlInterval: string,
97-
aggregation: Aggregation
57+
aggregation: Aggregation,
58+
satelliteTable: string,
59+
discriminatorCol: string,
60+
discriminatorAlias: string,
9861
): Promise<OrdpoolStatisticResponse[]> {
9962
const groupByTime = this.getGroupByClause(aggregation).replace(/^GROUP BY/, '');
10063
const query = `
@@ -103,20 +66,20 @@ class OrdpoolStatisticsApi {
10366
MAX(b.height) AS maxHeight,
10467
MIN(UNIX_TIMESTAMP(b.blockTimestamp)) AS minTime,
10568
MAX(UNIX_TIMESTAMP(b.blockTimestamp)) AS maxTime,
106-
cp.message_type AS messageType,
69+
${discriminatorCol} AS ${discriminatorAlias},
10770
COUNT(*) AS count
10871
FROM blocks b
109-
JOIN ordpool_stats_counterparty cp ON cp.hash = b.hash
72+
JOIN ${satelliteTable} sat ON sat.hash = b.hash
11073
WHERE b.height >= ${firstInscriptionHeight}
11174
AND b.blockTimestamp >= DATE_SUB(NOW(), INTERVAL ${sqlInterval})
112-
GROUP BY ${groupByTime}, cp.message_type
75+
GROUP BY ${groupByTime}, ${discriminatorCol}
11376
ORDER BY b.blockTimestamp DESC
11477
`;
11578
try {
11679
const [rows] : any[] = await DB.query(query);
11780
return rows;
11881
} catch (error) {
119-
logger.err(`Error executing counterparty-messages query: ${error}`, 'Ordpool');
82+
logger.err(`Error executing ${satelliteTable} breakdown query: ${error}`, 'Ordpool');
12083
throw error;
12184
}
12285
}

backend/src/repositories/OrdpoolBlocksRepository.ts

Lines changed: 71 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -324,120 +324,105 @@ interface OrdpoolStatColumn {
324324
}
325325

326326
const camelToSnake = (s: string): string => s.replace(/[A-Z]/g, m => '_' + m.toLowerCase());
327-
328-
// Section helpers: each takes a list of camelCase field names and emits one
329-
// OrdpoolStatColumn per field with `<section>_<snake>` SQL column, the
330-
// matching camelCase alias, and typed get/set lambdas. TypeScript checks
331-
// the field names against the actual OrdpoolStats shape.
332-
const amountCols = (...fields: (keyof OrdpoolStats['amounts'])[]): OrdpoolStatColumn[] =>
333-
fields.map(f => ({
334-
col: 'amounts_' + camelToSnake(f as string),
335-
alias: 'amounts' + (f as string).charAt(0).toUpperCase() + (f as string).slice(1),
336-
val: s => s.amounts[f],
337-
set: (t, v) => { (t.amounts as any)[f] = v; },
338-
}));
339-
const feeCols = (...fields: (keyof OrdpoolStats['fees'])[]): OrdpoolStatColumn[] =>
340-
fields.map(f => ({
341-
col: 'fees_' + camelToSnake(f as string),
342-
alias: 'fees' + (f as string).charAt(0).toUpperCase() + (f as string).slice(1),
343-
val: s => s.fees[f],
344-
set: (t, v) => { (t.fees as any)[f] = v; },
345-
}));
346-
const cat21Cols = (...fields: (keyof OrdpoolStats['cat21'])[]): OrdpoolStatColumn[] =>
327+
const cap = (s: string): string => s.charAt(0).toUpperCase() + s.slice(1);
328+
329+
const MOST_ACTIVE_MINT_TRUNC = 20;
330+
const TRUNCATED_PLACEHOLDER = `LEFT(?, ${MOST_ACTIVE_MINT_TRUNC})`;
331+
332+
/** Emit one OrdpoolStatColumn per camelCase field, mapping
333+
* `field` ↔ `${colPrefix}_${snake_field}` (SQL) ↔ `${aliasPrefix}${PascalField}` (camelCase).
334+
* `pick` returns the section object on a stats — typically a top-level key
335+
* (`s => s.amounts`) but can drill deeper (`s => s.inscriptions.image`). */
336+
const sectionCols = <Section extends object>(
337+
colPrefix: string,
338+
aliasPrefix: string,
339+
pick: (s: OrdpoolStats) => Section,
340+
fields: (keyof Section & string)[],
341+
): OrdpoolStatColumn[] =>
347342
fields.map(f => ({
348-
col: 'cat21_' + camelToSnake(f as string),
349-
alias: 'cat21' + (f as string).charAt(0).toUpperCase() + (f as string).slice(1),
350-
val: s => s.cat21[f],
351-
set: (t, v) => { (t.cat21 as any)[f] = v; },
352-
}));
353-
const runeCols = (...fields: (keyof OrdpoolStats['runes'])[]): OrdpoolStatColumn[] =>
354-
fields.map(f => ({
355-
col: 'runes_' + camelToSnake(f as string),
356-
alias: 'runes' + (f as string).charAt(0).toUpperCase() + (f as string).slice(1),
357-
val: s => s.runes[f],
358-
set: (t, v) => { (t.runes as any)[f] = v; },
343+
col: `${colPrefix}_${camelToSnake(f)}`,
344+
alias: aliasPrefix + cap(f),
345+
val: s => (pick(s) as any)[f],
346+
set: (t, v) => { (pick(t) as any)[f] = v; },
359347
}));
360348

361-
// All 8 fields of an InscriptionSizeAggregate. Used 4× — once for the global
362-
// aggregate and once per content-type bucket (image/text/json).
349+
/** All 8 fields of an InscriptionSizeAggregate. Used 4× — global + per-bucket. */
350+
const INSCRIPTION_SIZE_FIELDS: (keyof InscriptionSizeAggregate)[] = [
351+
'totalEnvelopeSize', 'totalContentSize',
352+
'largestEnvelopeSize', 'largestContentSize',
353+
'largestEnvelopeInscriptionId', 'largestContentInscriptionId',
354+
'averageEnvelopeSize', 'averageContentSize',
355+
];
356+
363357
const inscriptionSizeCols = (
364-
colPrefix: string, // e.g. 'inscriptions_image'
365-
aliasPrefix: string, // e.g. 'inscriptionsImage'
358+
colPrefix: string,
359+
aliasPrefix: string,
366360
pick: (s: OrdpoolStats) => InscriptionSizeAggregate,
367-
pickTarget: (t: OrdpoolStats) => InscriptionSizeAggregate,
368-
): OrdpoolStatColumn[] => {
369-
const fields: (keyof InscriptionSizeAggregate)[] = [
370-
'totalEnvelopeSize', 'totalContentSize',
371-
'largestEnvelopeSize', 'largestContentSize',
372-
'largestEnvelopeInscriptionId', 'largestContentInscriptionId',
373-
'averageEnvelopeSize', 'averageContentSize',
374-
];
375-
return fields.map(f => ({
376-
col: `${colPrefix}_${camelToSnake(f as string)}`,
377-
alias: aliasPrefix + (f as string).charAt(0).toUpperCase() + (f as string).slice(1),
378-
val: s => pick(s)[f],
379-
set: (t, v) => { (pickTarget(t) as any)[f] = v; },
380-
}));
381-
};
361+
): OrdpoolStatColumn[] =>
362+
sectionCols(colPrefix, aliasPrefix, pick, INSCRIPTION_SIZE_FIELDS);
382363

383-
const truncated20 = (
364+
const truncatedMostActive = (
384365
col: string,
385366
alias: string,
386367
val: (s: OrdpoolStats) => unknown,
387368
set: (t: OrdpoolStats, v: any) => void,
388-
): OrdpoolStatColumn => ({ col, alias, placeholder: 'LEFT(?, 20)', val, set });
369+
): OrdpoolStatColumn => ({ col, alias, placeholder: TRUNCATED_PLACEHOLDER, val, set });
389370

390371
export const ORDPOOL_STATS_COLUMNS: OrdpoolStatColumn[] = [
391-
...amountCols(
372+
...sectionCols('amounts', 'amounts', s => s.amounts, [
392373
'atomical', 'atomicalMint', 'atomicalUpdate',
393374
'counterparty', 'stamp', 'src721', 'src101',
394375
'cat21', 'cat21Mint',
395376
'inscription', 'inscriptionMint', 'inscriptionImage', 'inscriptionText', 'inscriptionJson',
396377
'rune', 'runeEtch', 'runeMint', 'runeCenotaph',
397378
'brc20', 'brc20Deploy', 'brc20Mint', 'brc20Transfer',
398379
'src20', 'src20Deploy', 'src20Mint', 'src20Transfer',
399-
),
400-
...feeCols(
380+
]),
381+
...sectionCols('fees', 'fees', s => s.fees, [
401382
'runeMints', 'nonUncommonRuneMints', 'brc20Mints', 'src20Mints',
402383
'cat21Mints', 'atomicals', 'inscriptionMints',
403384
'inscriptionImageMints', 'inscriptionTextMints', 'inscriptionJsonMints',
404-
),
405-
...inscriptionSizeCols('inscriptions', 'inscriptions', s => s.inscriptions, t => t.inscriptions),
406-
...inscriptionSizeCols('inscriptions_image', 'inscriptionsImage', s => s.inscriptions.image, t => t.inscriptions.image),
407-
...inscriptionSizeCols('inscriptions_text', 'inscriptionsText', s => s.inscriptions.text, t => t.inscriptions.text),
408-
...inscriptionSizeCols('inscriptions_json', 'inscriptionsJson', s => s.inscriptions.json, t => t.inscriptions.json),
409-
{
410-
col: 'inscriptions_brotli_count', alias: 'inscriptionsBrotliCount',
411-
val: s => s.inscriptions.brotliCount,
412-
set: (t, v) => { t.inscriptions.brotliCount = v; },
413-
},
414-
{
415-
col: 'inscriptions_gzip_count', alias: 'inscriptionsGzipCount',
416-
val: s => s.inscriptions.gzipCount,
417-
set: (t, v) => { t.inscriptions.gzipCount = v; },
418-
},
419-
{
420-
col: 'inscriptions_compressed_envelope_bytes', alias: 'inscriptionsCompressedEnvelopeBytes',
421-
val: s => s.inscriptions.compressedEnvelopeBytes,
422-
set: (t, v) => { t.inscriptions.compressedEnvelopeBytes = v; },
423-
},
424-
...cat21Cols('genesisCount', 'avgFeeRate', 'minFeeRate', 'maxFeeRate'),
425-
...runeCols('uniqueMintsCount', 'uniqueMintsCountNonUncommon', 'topMintCount', 'topMintCountNonUncommon'),
426-
truncated20('runes_most_active_mint', 'runesMostActiveMint',
385+
]),
386+
...inscriptionSizeCols('inscriptions', 'inscriptions', s => s.inscriptions),
387+
...inscriptionSizeCols('inscriptions_image', 'inscriptionsImage', s => s.inscriptions.image),
388+
...inscriptionSizeCols('inscriptions_text', 'inscriptionsText', s => s.inscriptions.text),
389+
...inscriptionSizeCols('inscriptions_json', 'inscriptionsJson', s => s.inscriptions.json),
390+
...sectionCols('inscriptions', 'inscriptions', s => s.inscriptions, [
391+
'brotliCount', 'gzipCount', 'compressedEnvelopeBytes',
392+
]),
393+
...sectionCols('cat21', 'cat21', s => s.cat21, [
394+
'genesisCount', 'avgFeeRate', 'minFeeRate', 'maxFeeRate',
395+
]),
396+
...sectionCols('runes', 'runes', s => s.runes, [
397+
'uniqueMintsCount', 'uniqueMintsCountNonUncommon', 'topMintCount', 'topMintCountNonUncommon',
398+
]),
399+
truncatedMostActive('runes_most_active_mint', 'runesMostActiveMint',
427400
s => s.runes.mostActiveMint, (t, v) => { t.runes.mostActiveMint = v; }),
428-
truncated20('runes_most_active_non_uncommon_mint', 'runesMostActiveNonUncommonMint',
401+
truncatedMostActive('runes_most_active_non_uncommon_mint', 'runesMostActiveNonUncommonMint',
429402
s => s.runes.mostActiveNonUncommonMint, (t, v) => { t.runes.mostActiveNonUncommonMint = v; }),
430-
truncated20('brc20_most_active_mint', 'brc20MostActiveMint',
403+
truncatedMostActive('brc20_most_active_mint', 'brc20MostActiveMint',
431404
s => s.brc20.mostActiveMint, (t, v) => { t.brc20.mostActiveMint = v; }),
432-
truncated20('src20_most_active_mint', 'src20MostActiveMint',
405+
truncatedMostActive('src20_most_active_mint', 'src20MostActiveMint',
433406
s => s.src20.mostActiveMint, (t, v) => { t.src20.mostActiveMint = v; }),
407+
// analyser_version doubles as the "is this row populated?" sentinel —
408+
// formatDbBlockIntoOrdpoolStats returns undefined when it's 0.
434409
{
435410
col: 'analyser_version', alias: 'analyserVersion',
436411
val: s => s.version,
437412
set: (t, v) => { t.version = v; },
438413
},
439414
];
440415

416+
// Static parts of the INSERT — column list, placeholder list, and the SQL
417+
// string itself. Computed once at module load. Per-call work in
418+
// saveBlockOrdpoolStatsInDatabase is reduced to one .map() over the spec
419+
// to materialise the param values.
420+
const ORDPOOL_STATS_INSERT_SQL = (() => {
421+
const cols = ['hash', 'height', ...ORDPOOL_STATS_COLUMNS.map(c => c.col)];
422+
const phs = ['?', '?', ...ORDPOOL_STATS_COLUMNS.map(c => c.placeholder ?? '?')];
423+
return `INSERT INTO ordpool_stats(${cols.join(', ')}) VALUES (${phs.join(', ')})`;
424+
})();
425+
441426

442427
class OrdpoolBlocksRepository {
443428
/**
@@ -461,16 +446,11 @@ class OrdpoolBlocksRepository {
461446

462447
const stats = block.extras.ordpoolStats;
463448

464-
// Single source of truth: column list, placeholder list, and params
465-
// array all derive from ORDPOOL_STATS_COLUMNS in the same .map() pass,
466-
// so positional drift between them is impossible.
467-
const cols = ['hash', 'height', ...ORDPOOL_STATS_COLUMNS.map(c => c.col)];
468-
const placeholders = ['?', '?', ...ORDPOOL_STATS_COLUMNS.map(c => c.placeholder ?? '?')];
469-
const params = [block.id, block.height, ...ORDPOOL_STATS_COLUMNS.map(c => c.val(stats))];
470-
471-
const query = `INSERT INTO ordpool_stats(${cols.join(', ')}) VALUES (${placeholders.join(', ')})`;
472-
473-
await DB.query(query, params, 'silent');
449+
// SQL is precomputed at module load; per-call work is just materialising
450+
// the param values. Positional alignment is preserved because both
451+
// the SQL and the params iterate ORDPOOL_STATS_COLUMNS in the same order.
452+
const params = [block.id, block.height, ...ORDPOOL_STATS_COLUMNS.map(c => c.val(stats))];
453+
await DB.query(ORDPOOL_STATS_INSERT_SQL, params, 'silent');
474454

475455
logger.debug(`$saveBlockOrdpoolStatsInDatabase() - Block ${block.height} successfully stored!`, 'Ordpool');
476456

@@ -508,9 +488,6 @@ class OrdpoolBlocksRepository {
508488
result.src20.src20MintActivity = compactToMintActivity(dbBlk.src20MintActivity);
509489
result.src20.src20DeployAttempts = compactToSrc20DeployAttempts(dbBlk.src20DeployAttempts);
510490
result.cat21.minimalCat21MintActivity = compactToMinimalCat21Mints(dbBlk.cat21MintActivity);
511-
// Block-detail responses don't carry the atomical_op /
512-
// counterparty per-row satellite arrays — chart endpoints
513-
// query those tables directly via GROUP BY.
514491

515492
return result;
516493
}

frontend/src/app/components/_ordpool/ordpool-stats/ordpool-stats.component.helper.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
Interval,
1616
MintStatistic,
1717
NewTokenStatistic,
18+
OrdpoolStatisticResponse,
1819
ProtocolStatistic,
1920
RuneActivityStatistic,
2021
} from '../../../../../../backend/src/api/explorer/_ordpool/ordpool-statistics-interface';
@@ -36,14 +37,6 @@ type ExtractStatistic<T extends ChartType> =
3637
T extends 'counterparty-messages' ? CounterpartyMessagesStatistic :
3738
never;
3839

39-
/**
40-
* A utility function to map the chart type to its corresponding data handler.
41-
* Ensures type safety and clear mappings for each chart type.
42-
*
43-
* @param type - The chart type (e.g., 'mints', 'new-tokens', 'fees', 'inscription-sizes').
44-
* @param cases - A map of chart types to their respective handlers.
45-
* @returns A callback function for the specified chart type.
46-
*/
4740
export function matchType<T extends ChartType>(
4841
type: T,
4942
cases: { [K in ChartType]: (stats: ExtractStatistic<K>[]) => LineSeriesOption[] }
@@ -143,10 +136,6 @@ export function getSeriesData<T extends ChartType>(
143136
{ name: 'Top mint count', type: 'line', data: stats.map((stat) => [stat.minTime, stat.topMintCount]) },
144137
{ name: 'Top mint count (excluding ⧉ UNCOMMON•GOODS)', type: 'line', data: stats.map((stat) => [stat.minTime, stat.topMintCountNonUncommon]) },
145138
],
146-
// atomical-ops + counterparty-messages return one row per (period, op).
147-
// Single aggregate line for now; per-op breakdown is a follow-up
148-
// (needs grouping the rows by `operation` / `messageType` and emitting
149-
// one series per distinct value — depends on the time-series UI design).
150139
'atomical-ops': (stats: AtomicalOpsStatistic[]) => [
151140
{ name: 'Atomical operations', type: 'line', data: stats.map((stat) => [stat.minTime, stat.count]) },
152141
],
@@ -156,17 +145,9 @@ export function getSeriesData<T extends ChartType>(
156145
})(statistics);
157146
}
158147

159-
/**
160-
* Generates tooltip content based on the chart type.
161-
* @param type - The chart type.
162-
* @param stat - The statistics object to generate the tooltip content from.
163-
* @returns Tooltip HTML content as a string.
164-
*/
165148
export function getTooltipContent(
166149
type: ChartType,
167-
stat: MintStatistic | NewTokenStatistic | FeeStatistic | InscriptionSizeStatistic | ProtocolStatistic | InscriptionTypeStatistic
168-
| InscriptionTypeSizeStatistic | InscriptionTypeFeeStatistic | InscriptionCompressionStatistic
169-
| Cat21StatStatistic | RuneActivityStatistic | AtomicalOpsStatistic | CounterpartyMessagesStatistic
150+
stat: OrdpoolStatisticResponse,
170151
): string {
171152

172153
const baseContent = `

0 commit comments

Comments
 (0)