From baec17c51e3b5c4b6cb958b13dabb8c8c3de7a71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Fri, 30 Jun 2023 11:43:54 -0600 Subject: [PATCH] fix: skip expensive view refreshes when not streaming new blocks (#116) * feat: skip automatic predicate registration via env * fix: refresh views only when streaming --- src/chainhook/schemas.ts | 1 + src/chainhook/server.ts | 6 ++- src/env.ts | 5 +++ src/pg/pg-store.ts | 13 +++--- tests/helpers.ts | 6 +++ tests/inscriptions.test.ts | 90 ++++++++++++++++++++++++++++++++++++++ tests/server.test.ts | 16 ++++--- 7 files changed, 125 insertions(+), 12 deletions(-) diff --git a/src/chainhook/schemas.ts b/src/chainhook/schemas.ts index 93d5f2e6..ab1d95c6 100644 --- a/src/chainhook/schemas.ts +++ b/src/chainhook/schemas.ts @@ -92,6 +92,7 @@ const ChainhookPayload = Type.Object({ scope: Type.String(), operation: Type.String(), }), + is_streaming_blocks: Type.Boolean(), }), }); export type ChainhookPayload = Static; diff --git a/src/chainhook/server.ts b/src/chainhook/server.ts index 905d475c..37a2f21d 100644 --- a/src/chainhook/server.ts +++ b/src/chainhook/server.ts @@ -148,8 +148,10 @@ export async function buildChainhookServer(args: { db: PgStore }) { fastify.decorate('db', args.db); fastify.addHook('onReady', waitForChainhookNode); - fastify.addHook('onReady', registerChainhookPredicates); - fastify.addHook('onClose', removeChainhookPredicates); + if (ENV.CHAINHOOK_AUTO_PREDICATE_REGISTRATION) { + fastify.addHook('onReady', registerChainhookPredicates); + fastify.addHook('onClose', removeChainhookPredicates); + } await fastify.register(Chainhook); return fastify; diff --git a/src/env.ts b/src/env.ts index c204ec78..d1c4457d 100644 --- a/src/env.ts +++ b/src/env.ts @@ -34,6 +34,11 @@ const schema = Type.Object({ * coming from the valid instance */ CHAINHOOK_NODE_AUTH_TOKEN: Type.String(), + /** + * Register chainhook predicates automatically when the API is first launched. Set this to `false` + * if you're configuring your predicates manually for any reason. + */ + CHAINHOOK_AUTO_PREDICATE_REGISTRATION: Type.Boolean({ default: true }), PGHOST: Type.String(), PGPORT: Type.Number({ default: 5432, minimum: 0, maximum: 65535 }), diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index cc4e2d30..0a449c9d 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -209,9 +209,12 @@ export class PgStore extends BasePgStore { }); await this.normalizeInscriptionLocations({ inscription_id: Array.from(updatedInscriptionIds) }); await this.refreshMaterializedView('chain_tip'); - await this.refreshMaterializedView('inscription_count'); - await this.refreshMaterializedView('mime_type_counts'); - await this.refreshMaterializedView('sat_rarity_counts'); + // Skip expensive view refreshes if we're not streaming any live blocks yet. + if (payload.chainhook.is_streaming_blocks) { + await this.refreshMaterializedView('inscription_count'); + await this.refreshMaterializedView('mime_type_counts'); + await this.refreshMaterializedView('sat_rarity_counts'); + } } async getChainTipBlockHeight(): Promise { @@ -229,7 +232,7 @@ export class PgStore extends BasePgStore { async getMimeTypeInscriptionCount(mimeType?: string[]): Promise { if (!mimeType) return 0; const result = await this.sql<{ count: number }[]>` - SELECT SUM(count) AS count + SELECT COALESCE(SUM(count), 0) AS count FROM mime_type_counts WHERE mime_type IN ${this.sql(mimeType)} `; @@ -239,7 +242,7 @@ export class PgStore extends BasePgStore { async geSatRarityInscriptionCount(satRarity?: SatoshiRarity[]): Promise { if (!satRarity) return 0; const result = await this.sql<{ count: number }[]>` - SELECT SUM(count) AS count + SELECT COALESCE(SUM(count), 0) AS count FROM sat_rarity_counts WHERE sat_rarity IN ${this.sql(satRarity)} `; diff --git a/tests/helpers.ts b/tests/helpers.ts index 7d867622..a5a5db4f 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -28,6 +28,7 @@ export class TestChainhookPayloadBuilder { scope: 'ordinals_protocol', operation: 'inscription_feed', }, + is_streaming_blocks: true, }, }; private action: 'apply' | 'rollback' = 'apply'; @@ -38,6 +39,11 @@ export class TestChainhookPayloadBuilder { return this.lastBlock.transactions[this.lastBlock.transactions.length - 1]; } + streamingBlocks(streaming: boolean): this { + this.payload.chainhook.is_streaming_blocks = streaming; + return this; + } + apply(): this { this.action = 'apply'; return this; diff --git a/tests/inscriptions.test.ts b/tests/inscriptions.test.ts index 49880e75..9f639fa8 100644 --- a/tests/inscriptions.test.ts +++ b/tests/inscriptions.test.ts @@ -2362,6 +2362,96 @@ describe('/inscriptions', () => { expect(responseJson2.results[2].genesis_block_height).toStrictEqual(775617); }); }); + + describe('when not streaming', () => { + test('counts are returned as zero', async () => { + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .streamingBlocks(false) + .apply() + .block({ + height: 778575, + hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + timestamp: 1676913207, + }) + .transaction({ + hash: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', + }) + .inscriptionRevealed({ + content_bytes: '0x48656C6C6F', + content_type: 'text/plain;charset=utf-8', + content_length: 5, + inscription_number: 7, + inscription_fee: 705, + inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', + inscription_output_value: 10000, + inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', + ordinal_number: 0, + ordinal_block_height: 0, + ordinal_offset: 0, + satpoint_post_inscription: + '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0', + }) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .streamingBlocks(false) + .apply() + .block({ + height: 775617, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + timestamp: 1676913207, + }) + .transaction({ + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + }) + .inscriptionRevealed({ + content_bytes: '0x48656C6C6F', + content_type: 'image/png', + content_length: 5, + inscription_number: 1, + inscription_fee: 2805, + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + inscription_output_value: 10000, + inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + ordinal_number: 257418248345364, + ordinal_block_height: 650000, + ordinal_offset: 0, + satpoint_post_inscription: + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0', + }) + .build() + ); + + const response1 = await fastify.inject({ + method: 'GET', + url: '/ordinals/v1/inscriptions?mime_type=text/plain', + }); + expect(response1.statusCode).toBe(200); + const responseJson1 = response1.json(); + expect(responseJson1.total).toBe(0); + expect(responseJson1.results.length).toBeGreaterThan(0); + + const response2 = await fastify.inject({ + method: 'GET', + url: '/ordinals/v1/inscriptions?rarity=mythic', + }); + expect(response2.statusCode).toBe(200); + const responseJson2 = response2.json(); + expect(responseJson2.total).toBe(0); + expect(responseJson2.results.length).toBeGreaterThan(0); + + const response3 = await fastify.inject({ + method: 'GET', + url: '/ordinals/v1/inscriptions', + }); + expect(response3.statusCode).toBe(200); + const responseJson3 = response3.json(); + expect(responseJson3.total).toBe(0); + expect(responseJson3.results.length).toBeGreaterThan(0); + }); + }); }); test('returns not found for invalid inscriptions', async () => { diff --git a/tests/server.test.ts b/tests/server.test.ts index cca0fb9c..257aa945 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -4,6 +4,7 @@ import { ENV } from '../src/env'; import { cycleMigrations } from '../src/pg/migrations'; import { PgStore } from '../src/pg/pg-store'; import { TestChainhookPayloadBuilder, TestFastifyServer } from './helpers'; +import { ChainhookPayload } from '../src/chainhook/schemas'; describe('EventServer', () => { let db: PgStore; @@ -149,7 +150,7 @@ describe('EventServer', () => { }; // Apply - const payload1 = { + const payload1: ChainhookPayload = { apply: [reveal], rollback: [], chainhook: { @@ -158,6 +159,7 @@ describe('EventServer', () => { scope: 'ordinals_protocol', operation: 'inscription_feed', }, + is_streaming_blocks: true, }, }; const response = await fastify.inject({ @@ -208,7 +210,7 @@ describe('EventServer', () => { expect(inscr.value).toBe('10000'); // Rollback - const payload2 = { + const payload2: ChainhookPayload = { apply: [], rollback: [reveal], chainhook: { @@ -217,6 +219,7 @@ describe('EventServer', () => { scope: 'ordinals_protocol', operation: 'inscription_feed', }, + is_streaming_blocks: true, }, }; const response2 = await fastify.inject({ @@ -303,7 +306,7 @@ describe('EventServer', () => { }; // Apply - const payload1 = { + const payload1: ChainhookPayload = { apply: [transfer], rollback: [], chainhook: { @@ -312,6 +315,7 @@ describe('EventServer', () => { scope: 'ordinals_protocol', operation: 'inscription_feed', }, + is_streaming_blocks: true, }, }; const response = await fastify.inject({ @@ -362,7 +366,7 @@ describe('EventServer', () => { expect(inscr.value).toBe('10000'); // Rollback - const payload2 = { + const payload2: ChainhookPayload = { apply: [], rollback: [transfer], chainhook: { @@ -371,6 +375,7 @@ describe('EventServer', () => { scope: 'ordinals_protocol', operation: 'inscription_feed', }, + is_streaming_blocks: true, }, }; const response2 = await fastify.inject({ @@ -435,7 +440,7 @@ describe('EventServer', () => { }; // Apply - const payload1 = { + const payload1: ChainhookPayload = { apply: [reveal], rollback: [], chainhook: { @@ -444,6 +449,7 @@ describe('EventServer', () => { scope: 'ordinals_protocol', operation: 'inscription_feed', }, + is_streaming_blocks: true, }, }; const response = await fastify.inject({