Skip to content

Commit

Permalink
feat: add stats endpoint for inscription counts (#70)
Browse files Browse the repository at this point in the history
* feat: add stats endpoint for inscription counts

* test: add simple stats unit test

* refactor: switch to normalize window function

* test: update inscription count tests

* refactor: add range filters

* refactor: rewrite query without defaults

* test: refactor tests to use less build()

* refactor: rename migration to match style

* refactor: add openapi info

* refactor: track minimum value directly

* refactor: make filters object mandatory

* chore: rename migration

* fix: add cache to stats endpoint

* refactor: rename total to inscription_count_accum

* fix: change schema to string for bigints

* refactor: add block hash and timestamp to stats

* fix: use min in query to ignore potential conflicts

* fix: update etag to allow for partial block ingestion

---------

Co-authored-by: janniks <janniks@users.noreply.github.com>
  • Loading branch information
janniks and janniks committed Jul 3, 2023
1 parent 2b53ff4 commit ac18e62
Show file tree
Hide file tree
Showing 12 changed files with 570 additions and 35 deletions.
29 changes: 29 additions & 0 deletions migrations/1687785552000_inscriptions-per-block.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* 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: 'bigint',
primaryKey: true,
},
block_hash: {
type: 'text',
notNull: true,
},
inscription_count: {
type: 'bigint',
notNull: true,
},
inscription_count_accum: {
type: 'bigint',
notNull: true,
},
timestamp: {
type: 'timestamptz',
notNull: true,
},
});
}
9 changes: 6 additions & 3 deletions src/api/init.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import FastifyCors from '@fastify/cors';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import Fastify, { FastifyPluginAsync } from 'fastify';
import FastifyMetrics, { IFastifyMetrics } from 'fastify-metrics';
import { Server } from 'http';
import FastifyCors from '@fastify/cors';

import { PINO_CONFIG } from '../logger';
import { InscriptionsRoutes } from './routes/inscriptions';
import { PgStore } from '../pg/pg-store';
import { InscriptionsRoutes } from './routes/inscriptions';
import { SatRoutes } from './routes/sats';
import { StatsRoutes } from './routes/stats';
import { StatusRoutes } from './routes/status';
import FastifyMetrics, { IFastifyMetrics } from 'fastify-metrics';
import { isProdEnv } from './util/helpers';

export const Api: FastifyPluginAsync<
Expand All @@ -18,6 +20,7 @@ export const Api: FastifyPluginAsync<
await fastify.register(StatusRoutes);
await fastify.register(InscriptionsRoutes);
await fastify.register(SatRoutes);
await fastify.register(StatsRoutes);
};

export async function buildApiServer(args: { db: PgStore }) {
Expand Down
45 changes: 17 additions & 28 deletions src/api/routes/inscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,42 @@ import { Value } from '@sinclair/typebox/value';
import { FastifyPluginAsync, FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import {
AddressesParam,
BlockHeightParam,
BlockInscriptionTransferSchema,
BlockParam,
InscriptionIdParamCType,
InscriptionIdentifierParam,
InscriptionIdsParam,
InscriptionLocationResponseSchema,
InscriptionNumberParam,
InscriptionNumbersParam,
InscriptionResponse,
LimitParam,
MimeTypesParam,
NotFoundResponse,
OffsetParam,
PaginatedResponse,
MimeTypesParam,
SatoshiRaritiesParam,
OutputParam,
Order,
OrderBy,
OrderByParam,
OrderParam,
OrderBy,
Order,
InscriptionIdentifierParam,
BlockHashParamCType,
BlockHeightParamCType,
InscriptionIdParamCType,
BlockHeightParam,
BlockParam,
OrdinalParam,
InscriptionNumberParam,
OutputParam,
PaginatedResponse,
SatoshiRaritiesParam,
TimestampParam,
AddressesParam,
InscriptionIdsParam,
InscriptionNumbersParam,
InscriptionLocationResponseSchema,
BlockInscriptionTransferSchema,
} from '../schemas';
import { handleInscriptionCache, handleInscriptionTransfersCache } from '../util/cache';
import {
DEFAULT_API_LIMIT,
blockParam,
hexToBuffer,
parseBlockTransfers,
parseDbInscription,
parseDbInscriptions,
parseInscriptionLocations,
} from '../util/helpers';

function blockParam(param: string | undefined, name: string) {
const out: Record<string, string> = {};
if (BlockHashParamCType.Check(param)) {
out[`${name}_hash`] = param;
} else if (BlockHeightParamCType.Check(param)) {
out[`${name}_height`] = param;
}
return out;
}

function inscriptionIdArrayParam(param: string | number) {
return InscriptionIdParamCType.Check(param) ? { genesis_id: [param] } : { number: [param] };
}
Expand Down
52 changes: 52 additions & 0 deletions src/api/routes/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
import { FastifyPluginAsync, FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import { BlockHeightParam, InscriptionsPerBlockResponse, NotFoundResponse } from '../schemas';
import { handleInscriptionsPerBlockCache } from '../util/cache';
import { blockParam } from '../util/helpers';

const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = (
fastify,
options,
done
) => {
fastify.addHook('preHandler', handleInscriptionsPerBlockCache);
fastify.get(
'/stats/inscriptions',
{
schema: {
operationId: 'getStatsInscriptionCount',
summary: 'Inscription Count per Block',
description: 'Retrieves statistics on the number of inscriptions revealed per block',
tags: ['Statistics'],
querystring: Type.Object({
from_block_height: Type.Optional(BlockHeightParam),
to_block_height: Type.Optional(BlockHeightParam),
}),
response: {
200: InscriptionsPerBlockResponse,
404: NotFoundResponse,
},
},
},
async (request, reply) => {
const inscriptions = await fastify.db.getInscriptionCountPerBlock({
...blockParam(request.query.from_block_height, 'from_block'),
...blockParam(request.query.to_block_height, 'to_block'),
});
await reply.send({
results: inscriptions,
});
}
);
done();
};

export const StatsRoutes: FastifyPluginAsync<
Record<never, never>,
Server,
TypeBoxTypeProvider
> = async fastify => {
await fastify.register(IndexRoutes);
};
18 changes: 18 additions & 0 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ export const OpenApiSchemaOptions: SwaggerOptions = {
name: 'Satoshis',
description: 'Endpoints to query Satoshi ordinal and rarity information',
},
{
name: 'Statistics',
description: 'Endpoints to query statistics on ordinal inscription data',
},
],
},
};
Expand Down Expand Up @@ -333,3 +337,17 @@ export const NotFoundResponse = Type.Object(
},
{ title: 'Not Found Response' }
);

export const InscriptionsPerBlock = Type.Object({
block_height: Type.String({ examples: ['778921'] }),
block_hash: Type.String({
examples: ['0000000000000000000452773967cdd62297137cdaf79950c5e8bb0c62075133'],
}),
inscription_count: Type.String({ examples: ['100'] }),
inscription_count_accum: Type.String({ examples: ['3100'] }),
timestamp: Type.Integer({ examples: [1677733170000] }),
});
export const InscriptionsPerBlockResponse = Type.Object({
results: Type.Array(InscriptionsPerBlock),
});
export type InscriptionsPerBlockResponse = Static<typeof InscriptionsPerBlockResponse>;
11 changes: 11 additions & 0 deletions src/api/util/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { InscriptionIdParamCType, InscriptionNumberParamCType } from '../schemas
export enum ETagType {
inscriptionTransfers,
inscription,
inscriptionsPerBlock,
}

/**
Expand All @@ -26,6 +27,13 @@ export async function handleInscriptionTransfersCache(
return handleCache(ETagType.inscriptionTransfers, request, reply);
}

export async function handleInscriptionsPerBlockCache(
request: FastifyRequest,
reply: FastifyReply
) {
return handleCache(ETagType.inscriptionsPerBlock, request, reply);
}

async function handleCache(type: ETagType, request: FastifyRequest, reply: FastifyReply) {
const ifNoneMatch = parseIfNoneMatchHeader(request.headers['if-none-match']);
let etag: string | undefined;
Expand All @@ -36,6 +44,9 @@ async function handleCache(type: ETagType, request: FastifyRequest, reply: Fasti
case ETagType.inscriptionTransfers:
etag = await getInscriptionTransfersEtag(request);
break;
case ETagType.inscriptionsPerBlock:
etag = await request.server.db.getInscriptionsPerBlockETag();
break;
}
if (etag) {
if (ifNoneMatch && ifNoneMatch.includes(etag)) {
Expand Down
12 changes: 12 additions & 0 deletions src/api/util/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {
DbLocation,
} from '../../pg/types';
import {
BlockHashParamCType,
BlockHeightParamCType,
BlockInscriptionTransfer,
InscriptionLocationResponse,
InscriptionResponseType,
Expand Down Expand Up @@ -127,3 +129,13 @@ export const has0xPrefix = (id: string) => id.substr(0, 2).toLowerCase() === '0x
export function normalizedHexString(hex: string): string {
return has0xPrefix(hex) ? hex.substring(2) : hex;
}

export function blockParam(param: string | undefined, name: string) {
const out: Record<string, string> = {};
if (BlockHashParamCType.Check(param)) {
out[`${name}_hash`] = param;
} else if (BlockHeightParamCType.Check(param)) {
out[`${name}_height`] = param;
}
return out;
}
Loading

0 comments on commit ac18e62

Please sign in to comment.