diff --git a/migrations/1683909884000_inscriptions_per_block.ts b/migrations/1683909884000_inscriptions_per_block.ts new file mode 100644 index 00000000..654956f7 --- /dev/null +++ b/migrations/1683909884000_inscriptions_per_block.ts @@ -0,0 +1,25 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { MigrationBuilder, ColumnDefinitions } from 'node-pg-migrate'; + +export const shorthands: ColumnDefinitions | undefined = undefined; + +export function up(pgm: MigrationBuilder): void { + pgm.createTable('inscriptions_per_block', { + block_height: { + type: 'int', + primaryKey: true, + }, + inscriptions: { + type: 'int', + notNull: true, + }, + inscriptions_total: { + type: 'int', + notNull: true, + }, + }); +} + +export function down(pgm: MigrationBuilder): void { + pgm.dropTable('inscriptions_per_block'); +} diff --git a/src/api/routes/stats.ts b/src/api/routes/stats.ts new file mode 100644 index 00000000..c68676c2 --- /dev/null +++ b/src/api/routes/stats.ts @@ -0,0 +1,25 @@ +import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; +import { FastifyPluginAsync, FastifyPluginCallback } from 'fastify'; +import { Server } from 'http'; + +const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTypeProvider> = ( + fastify, + options, + done +) => { + fastify.get('/stats/inscriptions', async (request, reply) => { + const inscriptions = await fastify.db.getInscriptionCountPerBlock(); + await reply.send({ + results: inscriptions, + }); + }); + done(); +}; + +export const StatsRoutes: FastifyPluginAsync< + Record, + Server, + TypeBoxTypeProvider +> = async fastify => { + await fastify.register(IndexRoutes); +}; diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 66d31683..77a856ae 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -11,6 +11,7 @@ import { BasePgStore } from './postgres-tools/base-pg-store'; import { DbFullyLocatedInscriptionResult, DbInscriptionContent, + DbInscriptionCountPerBlock, DbInscriptionIndexFilters, DbInscriptionIndexOrder, DbInscriptionIndexPaging, @@ -77,6 +78,9 @@ export class PgStore extends BasePgStore { } } } + const block_height = event.block_identifier.index; + await this.rollBackInscriptionCount({ block_height }); + logger.info(`PgStore rollback inscription counts for block ${block_height}`); } for (const event of payload.apply) { const block_hash = normalizedHexString(event.block_identifier.hash); @@ -130,10 +134,8 @@ export class PgStore extends BasePgStore { block_height: event.block_identifier.index, address: transfer.updated_address, output: output, - offset: offset ?? null, - value: transfer.post_transfer_output_value - ? transfer.post_transfer_output_value.toString() - : null, + offset: offset, + value: transfer.post_transfer_output_value?.toString() ?? null, timestamp: event.timestamp, sat_ordinal: transfer.ordinal_number.toString(), sat_rarity: satoshi.rarity, @@ -147,6 +149,9 @@ export class PgStore extends BasePgStore { } } } + const block_height = event.block_identifier.index; + await this.insertInscriptionCount({ block_height }); + logger.info(`PgStore apply inscription counts at block ${block_height}`); } }); await this.normalizeInscriptionLocations({ inscription_id: Array.from(updatedInscriptionIds) }); @@ -474,6 +479,10 @@ export class PgStore extends BasePgStore { } } + async getInscriptionCountPerBlock(): Promise { + return this.sql`SELECT * FROM inscriptions_per_block`; + } + async refreshMaterializedView(viewName: string) { const isProd = process.env.NODE_ENV === 'production'; await this.sql`REFRESH MATERIALIZED VIEW ${ @@ -621,6 +630,32 @@ export class PgStore extends BasePgStore { return inscription_id; } + private async insertInscriptionCount(args: { block_height: number }): Promise { + await this.sql` + WITH previous AS ( + SELECT COALESCE(p.inscriptions_total, 0) as previous_total + FROM inscriptions_per_block p + WHERE p.block_height = ${args.block_height - 1} + LIMIT 1 + ), current AS ( + SELECT block_height, COUNT(*) as inscriptions, COUNT(*) + (SELECT previous_total FROM previous) as inscriptions_total + FROM locations + WHERE block_height = ${args.block_height} AND genesis = true + GROUP BY block_height + LIMIT 1 + ) + INSERT INTO inscriptions_per_block + SELECT * FROM current + ON CONFLICT (block_height) DO UPDATE SET + inscriptions = EXCLUDED.inscriptions, + inscriptions_total = EXCLUDED.inscriptions_total; + `; + } + + private async rollBackInscriptionCount(args: { block_height: number }): Promise { + await this.sql`DELETE FROM inscriptions_per_block WHERE block_height = ${args.block_height}`; + } + private async rollBackInscriptionGenesis(args: { genesis_id: string }): Promise { // This will cascade into dependent tables. await this.sql`DELETE FROM inscriptions WHERE genesis_id = ${args.genesis_id}`; diff --git a/src/pg/types.ts b/src/pg/types.ts index eb542b89..fa1eccec 100644 --- a/src/pg/types.ts +++ b/src/pg/types.ts @@ -212,3 +212,9 @@ export enum DbInscriptionIndexResultCountType { /** Filtered by custom arguments */ custom, } + +export type DbInscriptionCountPerBlock = { + block_height: number; + inscriptions: number; + inscriptions_total: number; +};