Skip to content

Commit b1ada72

Browse files
committed
ordpool-stats: add 'OpenTimestamps' chart type (stamps per period)
New 'ots' ChartType in the /ordpool-stats family. Single line series showing the number of confirmed OTS calendar batch-commit transactions per period. Backend: - New OtsStatistic interface (count per period). - getOrdpoolStatistics dispatches 'ots' to a new getSatelliteTotal helper (single COUNT(*) per period, no per-discriminator split -- we want a tidy single-series sparkline, not a per-calendar fan-out). - Joins ordpool_stats_ots on sat.blockhash = b.hash so still-pending rows (NULL blockhash, not yet confirmed) get filtered automatically. - 1 new statistics-api spec asserts the new SQL shape. Frontend: - ChartType + ExtractStatistic both extended with 'ots'. - getSeriesData('ots', ...) emits one line: 'OpenTimestamps batch commits'. - getTooltipContent('ots', ...) shows 'OTS batch commits: N'. - formatChartHeading + formatChartDescription handle the new type. - One more button in ordpool-stats.component.html ('OpenTimestamps'). - 2 new helper specs pin the series shape + tooltip text. 289 backend tests pass (was 288). 173 frontend tests pass (was 171). AOT prod build clean. \xf0\x9f\x98\xba
1 parent b05d882 commit b1ada72

6 files changed

Lines changed: 103 additions & 2 deletions

File tree

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export type ChartType =
1919
| 'cat21-stats' // CAT-21 block aggregates: genesis count, fee-rate spread
2020
| 'rune-activity' // unique runes minted + top mint count, in pairs
2121
| 'atomical-ops' // per-operation counts from satellite (dft/nft/mod/...)
22-
| 'counterparty-messages'; // per-message-type counts from satellite
22+
| 'counterparty-messages' // per-message-type counts from satellite
23+
| 'ots'; // OpenTimestamps batch-commits per period
2324

2425
export interface BaseStatistic {
2526
minHeight: number;
@@ -139,6 +140,14 @@ export interface CounterpartyMessagesStatistic extends BaseStatistic {
139140
count?: number;
140141
}
141142

143+
// OpenTimestamps batch-commit counts per period. One row per period; the
144+
// `count` is the number of confirmed OTS calendar batch transactions whose
145+
// blockheight falls inside the period. Comes from the ordpool_stats_ots
146+
// satellite (the same table OtsTxidSet hydrates from on backend boot).
147+
export interface OtsStatistic extends BaseStatistic {
148+
count?: number;
149+
}
150+
142151
export type OrdpoolStatisticResponse =
143152
MintStatistic |
144153
NewTokenStatistic |
@@ -152,7 +161,8 @@ export type OrdpoolStatisticResponse =
152161
Cat21StatStatistic |
153162
RuneActivityStatistic |
154163
AtomicalOpsStatistic |
155-
CounterpartyMessagesStatistic;
164+
CounterpartyMessagesStatistic |
165+
OtsStatistic;
156166

157167
export function isMintStatistic(stat: OrdpoolStatisticResponse): stat is MintStatistic {
158168
return 'cat21Mints' in stat;

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,21 @@ describe('OrdpoolStatisticsApi', () => {
8080
expect(result).toEqual([{ inscriptionImages: 100, inscriptionTexts: 200, inscriptionJsons: 300 }]);
8181
});
8282

83+
it('should call the satellite-total query for ots (single COUNT(*) per period)', async () => {
84+
(DB.query as jest.Mock).mockResolvedValueOnce([[{ minHeight: 800000, maxHeight: 800100, minTime: 1, maxTime: 2, count: 42 }]]);
85+
86+
const result = await OrdpoolStatisticsApi.getOrdpoolStatistics('ots', '1w', 'day');
87+
88+
// ordpool_stats_ots is the satellite, joined on sat.blockhash = b.hash
89+
// so pending rows (NULL blockhash) get filtered automatically.
90+
expect(DB.query).toHaveBeenCalledWith(expect.stringContaining('JOIN ordpool_stats_ots sat ON sat.blockhash = b.hash'));
91+
// No discriminator -- single COUNT(*) per period, not per-calendar
92+
expect(DB.query).toHaveBeenCalledWith(expect.stringContaining('COUNT(*) AS count'));
93+
// The same interval filter applies as on the main path
94+
expect(DB.query).toHaveBeenCalledWith(expect.stringContaining('AND b.blockTimestamp >= DATE_SUB(NOW(), INTERVAL 1 WEEK)'));
95+
expect(result).toEqual([{ minHeight: 800000, maxHeight: 800100, minTime: 1, maxTime: 2, count: 42 }]);
96+
});
97+
8398
it('should apply interval filtering', async () => {
8499
(DB.query as jest.Mock).mockResolvedValueOnce([[{ cat21Mints: 5, inscriptionMints: 10 }]]);
85100

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@ class OrdpoolStatisticsApi {
2828
return this.getSatelliteBreakdown(firstInscriptionHeight, sqlInterval, aggregation,
2929
'ordpool_stats_counterparty', 'sat.message_type', 'messageType');
3030
}
31+
if (type === 'ots') {
32+
// ordpool_stats_ots only carries confirmed-by-block rows once the
33+
// poller's confirm step fills in blockhash/blockheight. Pending rows
34+
// (NULL blockhash) deliberately skip aggregation -- they're not on
35+
// chain yet.
36+
return this.getSatelliteTotal(firstInscriptionHeight, sqlInterval, aggregation,
37+
'ordpool_stats_ots');
38+
}
3139

3240
const selectClause = this.getSelectClause(type);
3341
const groupByClause = this.getGroupByClause(aggregation);
@@ -57,6 +65,41 @@ class OrdpoolStatisticsApi {
5765
* discriminator value. Examples:
5866
* atomical-ops → discriminator = sat.operation
5967
* counterparty-messages → discriminator = sat.message_type */
68+
/** Single-series total per period from a satellite table (no discriminator
69+
* column). Used by the `ots` chart -- one COUNT(*) per period. The
70+
* satellite is joined on `sat.blockhash = b.hash`; rows whose blockhash
71+
* is NULL (i.e. still pending, not yet confirmed) are filtered by the
72+
* INNER JOIN. */
73+
private async getSatelliteTotal(
74+
firstInscriptionHeight: number,
75+
sqlInterval: string,
76+
aggregation: Aggregation,
77+
satelliteTable: string,
78+
): Promise<OrdpoolStatisticResponse[]> {
79+
const groupByTime = this.getGroupByClause(aggregation).replace(/^GROUP BY/, '');
80+
const query = `
81+
SELECT
82+
MIN(b.height) AS minHeight,
83+
MAX(b.height) AS maxHeight,
84+
MIN(UNIX_TIMESTAMP(b.blockTimestamp)) AS minTime,
85+
MAX(UNIX_TIMESTAMP(b.blockTimestamp)) AS maxTime,
86+
COUNT(*) AS count
87+
FROM blocks b
88+
JOIN ${satelliteTable} sat ON sat.blockhash = b.hash
89+
WHERE b.height >= ${firstInscriptionHeight}
90+
AND b.blockTimestamp >= DATE_SUB(NOW(), INTERVAL ${sqlInterval})
91+
GROUP BY ${groupByTime}
92+
ORDER BY b.blockTimestamp DESC
93+
`;
94+
try {
95+
const [rows]: any[] = await DB.query(query);
96+
return rows;
97+
} catch (error) {
98+
logger.err(`Error executing ${satelliteTable} total query: ${error}`, 'Ordpool');
99+
throw error;
100+
}
101+
}
102+
60103
private async getSatelliteBreakdown(
61104
firstInscriptionHeight: number,
62105
sqlInterval: string,

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,20 @@ describe('getSeriesData', () => {
121121
]);
122122
});
123123

124+
it('should generate a single OTS series', () => {
125+
const stats = [
126+
{ minHeight: 800000, maxHeight: 800100, minTime: 1734994800000, maxTime: 1734994800000, count: 7 },
127+
{ minHeight: 800101, maxHeight: 800200, minTime: 1734998400000, maxTime: 1734998400000, count: 12 },
128+
];
129+
const result = getSeriesData('ots', stats);
130+
expect(result).toEqual([
131+
{ name: 'OpenTimestamps batch commits', type: 'line', data: [
132+
[1734994800000, 7],
133+
[1734998400000, 12],
134+
] },
135+
]);
136+
});
137+
124138
it('should throw an error for unsupported chart type', () => {
125139
expect(() => {
126140
getSeriesData('unsupported-type' as any, []);
@@ -249,6 +263,13 @@ describe('getTooltipContent', () => {
249263
expect(result).toContain('JSON: 300');
250264
});
251265

266+
it('should generate correct tooltip content for ots', () => {
267+
const stat = { minHeight: 800000, maxHeight: 800001, minTime: 1, maxTime: 2, count: 9 };
268+
const result = getTooltipContent('ots', stat);
269+
expect(result).toContain('Block Range: 800000 – 800001');
270+
expect(result).toContain('OTS batch commits: 9');
271+
});
272+
252273
it('should throw an error for unsupported chart type', () => {
253274
expect(() => getTooltipContent('unsupported-type' as ChartType, mintStat)).toThrow(
254275
'Unsupported chart type: unsupported-type'

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
MintStatistic,
1818
NewTokenStatistic,
1919
OrdpoolStatisticResponse,
20+
OtsStatistic,
2021
ProtocolStatistic,
2122
RuneActivityStatistic,
2223
} from '../../../../../../backend/src/api/explorer/_ordpool/ordpool-statistics-interface';
@@ -48,6 +49,7 @@ type ExtractStatistic<T extends ChartType> =
4849
T extends 'rune-activity' ? RuneActivityStatistic :
4950
T extends 'atomical-ops' ? AtomicalOpsStatistic :
5051
T extends 'counterparty-messages' ? CounterpartyMessagesStatistic :
52+
T extends 'ots' ? OtsStatistic :
5153
never;
5254

5355
/**
@@ -181,6 +183,9 @@ export function getSeriesData<T extends ChartType>(
181183
'counterparty-messages': (stats: CounterpartyMessagesStatistic[]) => [
182184
{ name: 'Counterparty messages', type: 'line', data: stats.map((stat) => [stat.minTime, stat.count]) },
183185
],
186+
'ots': (stats: OtsStatistic[]) => [
187+
{ name: 'OpenTimestamps batch commits', type: 'line', data: stats.map((stat) => [stat.minTime, stat.count]) },
188+
],
184189
})(statistics);
185190
}
186191

@@ -304,6 +309,10 @@ export function getTooltipContent(
304309
const s = stat as CounterpartyMessagesStatistic;
305310
return baseContent + `${s.messageType ?? 'unknown'}: ${s.count ?? 0}`;
306311
}
312+
case 'ots': {
313+
const s = stat as OtsStatistic;
314+
return baseContent + `OTS batch commits: ${s.count ?? 0}`;
315+
}
307316
default:
308317
throw new Error(`Unsupported chart type: ${type}`);
309318
}
@@ -343,6 +352,7 @@ export function formatChartHeading(chartType: ChartType): string {
343352
'rune-activity': 'Rune Activity',
344353
'atomical-ops': 'Atomical Operations',
345354
'counterparty-messages': 'Counterparty Messages',
355+
'ots': 'OpenTimestamps',
346356
};
347357

348358
return chartTypeHeadings[chartType];
@@ -370,6 +380,7 @@ export function formatChartDescription(chartType: ChartType, interval: Interval,
370380
'rune-activity': 'Rune mint activity: distinct runes seeing mints + the top single-rune mint count. Each metric is shown twice — overall and excluding UNCOMMON•GOODS (rune 1:0, which dominates every rune mint stat).',
371381
'atomical-ops': 'Atomical operations breakdown by op type (nft / ft / dft / dmt / dat / mod / evt / sl / splat / split / custom-color).',
372382
'counterparty-messages': 'Counterparty message activity per period — sends, dispensers, fairmints, bets, sweeps, and the rest of the 22+ message types.',
383+
'ots': 'OpenTimestamps batch-commit transactions per period. Each row is one calendar publishing a Merkle root anchoring however many user submissions arrived since its last anchor — count goes up when calendars publish more often or more calendars are tracked.',
373384
};
374385

375386
const intervalDescriptions: Record<Interval, string> = {

frontend/src/app/components/_ordpool/ordpool-stats/ordpool-stats.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
<a routerLinkActive="active" class="btn btn-primary" [routerLink]="['/ordpool-stats' | relativeUrl, 'inscription-sizes', statistics.interval, statistics.aggregation]">Inscription Sizes</a>
4848
<a routerLinkActive="active" class="btn btn-primary" [routerLink]="['/ordpool-stats' | relativeUrl, 'inscription-types', statistics.interval, statistics.aggregation]">Inscription Types</a>
4949
<a routerLinkActive="active" class="btn btn-primary" [routerLink]="['/ordpool-stats' | relativeUrl, 'protocols', statistics.interval, statistics.aggregation]">Other Protocols</a>
50+
<a routerLinkActive="active" class="btn btn-primary" [routerLink]="['/ordpool-stats' | relativeUrl, 'ots', statistics.interval, statistics.aggregation]">OpenTimestamps</a>
5051

5152
</div>
5253

0 commit comments

Comments
 (0)