Skip to content

Commit

Permalink
refactor: add block hash and timestamp to stats
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Jun 27, 2023
1 parent 21479a0 commit 1186a8d
Show file tree
Hide file tree
Showing 8 changed files with 218 additions and 46 deletions.
8 changes: 8 additions & 0 deletions migrations/1687785552000_inscriptions-per-block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export function up(pgm: MigrationBuilder): void {
type: 'bigint',
primaryKey: true,
},
block_hash: {
type: 'text',
notNull: true,
},
inscription_count: {
type: 'bigint',
notNull: true,
Expand All @@ -17,5 +21,9 @@ export function up(pgm: MigrationBuilder): void {
type: 'bigint',
notNull: true,
},
timestamp: {
type: 'timestamptz',
notNull: true,
},
});
}
6 changes: 3 additions & 3 deletions src/api/routes/stats.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
import { FastifyPluginAsync, FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import { blockParam } from '../util/helpers';
import { Type } from '@sinclair/typebox';
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,
Expand All @@ -17,7 +17,7 @@ const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTy
{
schema: {
operationId: 'getStatsInscriptionCount',
summary: 'Inscription Count',
summary: 'Inscription Count per Block',
description: 'Retrieves statistics on the number of inscriptions revealed per block',
tags: ['Statistics'],
querystring: Type.Object({
Expand Down
4 changes: 4 additions & 0 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,12 @@ export const NotFoundResponse = Type.Object(

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),
Expand Down
63 changes: 35 additions & 28 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,10 +271,13 @@ export class PgStore extends BasePgStore {
}

async getInscriptionsPerBlockETag(): Promise<string> {
const result = await this.sql<
{ max: number }[]
>`SELECT MAX(block_height) FROM inscriptions_per_block`;
return result[0].max.toString();
const result = await this.sql<{ block_hash: string }[]>`
SELECT block_hash
FROM inscriptions_per_block
ORDER BY block_height DESC
LIMIT 1
`;
return result[0].block_hash;
}

async getInscriptionContent(
Expand Down Expand Up @@ -560,8 +563,8 @@ export class PgStore extends BasePgStore {
FROM inscriptions_per_block
${filters.from_block_height || filters.to_block_height ? where : this.sql``}
ORDER BY block_height DESC
LIMIT 10000
`; // roughly 70 days of blocks, assuming 10 minute block times on a full database
LIMIT 5000
`; // roughly 35 days of blocks, assuming 10 minute block times on a full database
}

async refreshMaterializedView(viewName: string) {
Expand Down Expand Up @@ -752,28 +755,32 @@ export class PgStore extends BasePgStore {
// - calculates new totals for all blocks >= min_block_height
// - inserts new totals
await sql`
WITH previous AS (
SELECT *
FROM inscriptions_per_block
WHERE block_height < ${args.min_block_height}
ORDER BY block_height DESC
LIMIT 1
), updated_blocks AS (
SELECT
block_height,
COUNT(*) AS inscription_count,
COALESCE((SELECT previous.inscription_count_accum FROM previous), 0) + (SUM(COUNT(*)) OVER (ORDER BY block_height ASC)) AS inscription_count_accum
FROM locations
WHERE block_height >= ${args.min_block_height} AND genesis = true
GROUP BY block_height
ORDER BY block_height ASC
)
INSERT INTO inscriptions_per_block
SELECT * FROM updated_blocks
ON CONFLICT (block_height) DO UPDATE SET
inscription_count = EXCLUDED.inscription_count,
inscription_count_accum = EXCLUDED.inscription_count_accum;
`;
WITH previous AS (
SELECT *
FROM inscriptions_per_block
WHERE block_height < ${args.min_block_height}
ORDER BY block_height DESC
LIMIT 1
), updated_blocks AS (
SELECT
block_height,
block_hash,
COUNT(*) AS inscription_count,
COALESCE((SELECT previous.inscription_count_accum FROM previous), 0) + (SUM(COUNT(*)) OVER (ORDER BY block_height ASC)) AS inscription_count_accum,
timestamp
FROM locations
WHERE block_height >= ${args.min_block_height} AND genesis = true
GROUP BY block_height, block_hash, timestamp
ORDER BY block_height ASC
)
INSERT INTO inscriptions_per_block
SELECT * FROM updated_blocks
ON CONFLICT (block_height) DO UPDATE SET
block_hash = EXCLUDED.block_hash,
inscription_count = EXCLUDED.inscription_count,
inscription_count_accum = EXCLUDED.inscription_count_accum,
timestamp = EXCLUDED.timestamp;
`;
});
}

Expand Down
2 changes: 2 additions & 0 deletions src/pg/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ export type DbInscriptionCountPerBlockFilters = {

export type DbInscriptionCountPerBlock = {
block_height: string;
block_hash: string;
inscription_count: string;
inscription_count_accum: string;
timestamp: number;
};
74 changes: 73 additions & 1 deletion tests/cache.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildApiServer } from '../src/api/init';
import { cycleMigrations } from '../src/pg/migrations';
import { PgStore } from '../src/pg/pg-store';
import { TestChainhookPayloadBuilder, TestFastifyServer } from './helpers';
import { TestChainhookPayloadBuilder, TestFastifyServer, randomHash } from './helpers';

describe('ETag cache', () => {
let db: PgStore;
Expand Down Expand Up @@ -174,4 +174,76 @@ describe('ETag cache', () => {
});
expect(cached2.statusCode).toBe(200);
});

test('inscriptions stats per block cache control', async () => {
const block1 = new TestChainhookPayloadBuilder()
.apply()
.block({ height: 778575, hash: randomHash() })
.transaction({ hash: '0x9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201' })
.inscriptionRevealed({
content_bytes: '0x48656C6C6F',
content_type: 'text/plain',
content_length: 5,
inscription_number: 7,
inscription_fee: 705,
inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0',
inscription_output_value: 10000,
inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj',
ordinal_number: 257418248345364,
ordinal_block_height: 650000,
ordinal_offset: 0,
satpoint_post_inscription:
'9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0',
})
.build();
await db.updateInscriptions(block1);

// ETag response
const response = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/stats/inscriptions',
});
expect(response.statusCode).toBe(200);
expect(response.headers.etag).not.toBeUndefined();
const etag = response.headers.etag;

// Cached
const cached = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/stats/inscriptions',
headers: { 'if-none-match': etag },
});
expect(cached.statusCode).toBe(304);

// New block
const block2 = new TestChainhookPayloadBuilder()
.apply()
.block({ height: 778577, hash: randomHash() })
.transaction({ hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d' })
.inscriptionRevealed({
content_bytes: '0x48656C6C6F',
content_type: 'image/png',
content_length: 5,
inscription_number: 2,
inscription_fee: 2805,
inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
inscription_output_value: 10000,
inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td',
ordinal_number: 1676913207,
ordinal_block_height: 650000,
ordinal_offset: 0,
satpoint_post_inscription:
'38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0',
})
.build();
await db.updateInscriptions(block2);

// Cache busted
const cacheBusted = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/stats/inscriptions',
// headers: { 'if-none-match': etag },
});
expect(cacheBusted.statusCode).toBe(200);
});
});
4 changes: 4 additions & 0 deletions tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,7 @@ export class TestChainhookPayloadBuilder {
return this.payload;
}
}

/** Generate a random hash like string for testing */
export const randomHash = () =>
[...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
103 changes: 89 additions & 14 deletions tests/stats.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { buildApiServer } from '../src/api/init';
import { cycleMigrations } from '../src/pg/migrations';
import { PgStore } from '../src/pg/pg-store';
import { TestChainhookPayloadBuilder, TestFastifyServer } from './helpers';
import { TestChainhookPayloadBuilder, TestFastifyServer, randomHash } from './helpers';

jest.setTimeout(100_000_000);

Expand All @@ -21,14 +21,47 @@ describe('/stats', () => {
});

describe('/stats/inscriptions', () => {
const bh = '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d';
const ts = 1676913207000;

describe('event processing', () => {
const EXPECTED = {
results: [
{ block_height: '778010', inscription_count: '3', inscription_count_accum: '9' },
{ block_height: '778005', inscription_count: '2', inscription_count_accum: '6' },
{ block_height: '778002', inscription_count: '1', inscription_count_accum: '4' },
{ block_height: '778001', inscription_count: '1', inscription_count_accum: '3' },
{ block_height: '778000', inscription_count: '2', inscription_count_accum: '2' },
{
block_hash: bh,
block_height: '778010',
inscription_count: '3',
inscription_count_accum: '9',
timestamp: ts,
},
{
block_hash: bh,
block_height: '778005',
inscription_count: '2',
inscription_count_accum: '6',
timestamp: ts,
},
{
block_hash: bh,
block_height: '778002',
inscription_count: '1',
inscription_count_accum: '4',
timestamp: ts,
},
{
block_hash: bh,
block_height: '778001',
inscription_count: '1',
inscription_count_accum: '3',
timestamp: ts,
},
{
block_hash: bh,
block_height: '778000',
inscription_count: '2',
inscription_count_accum: '2',
timestamp: ts,
},
],
};

Expand Down Expand Up @@ -111,8 +144,20 @@ describe('/stats', () => {
expect(responseFrom.statusCode).toBe(200);
expect(responseFrom.json()).toStrictEqual({
results: [
{ block_height: '778010', inscription_count: '1', inscription_count_accum: '6' },
{ block_height: '778005', inscription_count: '2', inscription_count_accum: '5' },
{
block_height: '778010',
block_hash: bh,
inscription_count: '1',
inscription_count_accum: '6',
timestamp: ts,
},
{
block_height: '778005',
block_hash: bh,
inscription_count: '2',
inscription_count_accum: '5',
timestamp: ts,
},
],
});

Expand All @@ -124,9 +169,27 @@ describe('/stats', () => {
expect(responseTo.statusCode).toBe(200);
expect(responseTo.json()).toStrictEqual({
results: [
{ block_height: '778002', inscription_count: '1', inscription_count_accum: '3' },
{ block_height: '778001', inscription_count: '1', inscription_count_accum: '2' },
{ block_height: '778000', inscription_count: '1', inscription_count_accum: '1' },
{
block_height: '778002',
block_hash: bh,
inscription_count: '1',
inscription_count_accum: '3',
timestamp: ts,
},
{
block_height: '778001',
block_hash: bh,
inscription_count: '1',
inscription_count_accum: '2',
timestamp: ts,
},
{
block_height: '778000',
block_hash: bh,
inscription_count: '1',
inscription_count_accum: '1',
timestamp: ts,
},
],
});

Expand All @@ -141,16 +204,28 @@ describe('/stats', () => {
expect(responseFromTo.statusCode).toBe(200);
expect(responseFromTo.json()).toStrictEqual({
results: [
{ block_height: '778005', inscription_count: '2', inscription_count_accum: '5' },
{ block_height: '778002', inscription_count: '1', inscription_count_accum: '3' },
{
block_height: '778005',
block_hash: bh,
inscription_count: '2',
inscription_count_accum: '5',
timestamp: ts,
},
{
block_height: '778002',
block_hash: bh,
inscription_count: '1',
inscription_count_accum: '3',
timestamp: ts,
},
],
});
});
});
});

function testRevealApply(blockHeight: number) {
const randomHex = [...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
const randomHex = randomHash();
return new TestChainhookPayloadBuilder()
.apply()
.block({
Expand Down

0 comments on commit 1186a8d

Please sign in to comment.