diff --git a/CHANGELOG.md b/CHANGELOG.md index feac7f23..2c5eddfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## [2.0.1-beta.4](https://github.com/hirosystems/ordinals-api/compare/v2.0.1-beta.3...v2.0.1-beta.4) (2023-12-05) + + +### Bug Fixes + +* optimize block rollback logic for re-orgs ([#279](https://github.com/hirosystems/ordinals-api/issues/279)) ([293c323](https://github.com/hirosystems/ordinals-api/commit/293c32391d5e71a3d6570526be3565a4e75572b7)) + +## [2.0.1-beta.3](https://github.com/hirosystems/ordinals-api/compare/v2.0.1-beta.2...v2.0.1-beta.3) (2023-11-29) + + +### Bug Fixes + +* upgrade chainhook client to v1.4.2 ([#277](https://github.com/hirosystems/ordinals-api/issues/277)) ([67ba3d4](https://github.com/hirosystems/ordinals-api/commit/67ba3d4f48e6ce8e955c08fd75cbf009786c0160)) + +## [2.0.1-beta.2](https://github.com/hirosystems/ordinals-api/compare/v2.0.1-beta.1...v2.0.1-beta.2) (2023-11-23) + + +### Bug Fixes + +* select only the first inscription id match ([106368e](https://github.com/hirosystems/ordinals-api/commit/106368e9fa415db0657988de28a541906f74d28a)) + +## [2.0.1-beta.1](https://github.com/hirosystems/ordinals-api/compare/v2.0.0...v2.0.1-beta.1) (2023-11-23) + + +### Bug Fixes + +* add ENV to toggle inscription gap detection ([56ab283](https://github.com/hirosystems/ordinals-api/commit/56ab283cdfaf95efe058e5f057fb5559b9dfede0)) + ## [2.0.0](https://github.com/hirosystems/ordinals-api/compare/v1.2.6...v2.0.0) (2023-11-21) diff --git a/README.md b/README.md index 30105141..10eea4dc 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ information. The Ordinals API is a microservice that has hard dependencies on other systems. Before you start, you'll need to have access to: -1. A [Chainhook node](https://github.com/hirosystems/chainhook) with a fully - indexed Ordinals `.redb` database. +1. An [Ordhook node](https://github.com/hirosystems/ordhook) with a fully + indexed Ordinals database. 1. A local writeable Postgres database for data storage ### Running the API @@ -47,7 +47,7 @@ Before you start, you'll need to have access to: Clone the repo. Create an `.env` file and specify the appropriate values to configure the local -API server, postgres DB and Chainhook node reachability. See +API server, postgres DB and Ordhook node reachability. See [`env.ts`](https://github.com/hirosystems/ordinals-api/blob/develop/src/env.ts) for all available configuration options. diff --git a/migrations/1701486147464_chain-tip-table.ts b/migrations/1701486147464_chain-tip-table.ts new file mode 100644 index 00000000..1f9b30b2 --- /dev/null +++ b/migrations/1701486147464_chain-tip-table.ts @@ -0,0 +1,38 @@ +/* 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.dropMaterializedView('chain_tip'); + pgm.createTable('chain_tip', { + id: { + type: 'bool', + primaryKey: true, + default: true, + }, + block_height: { + type: 'bigint', + notNull: true, + // Set block height 767430 (inscription #0 genesis) as default. + default: 767430, + }, + }); + pgm.addConstraint('chain_tip', 'chain_tip_one_row', 'CHECK(id)'); + pgm.sql(` + INSERT INTO chain_tip (block_height) ( + SELECT GREATEST(MAX(block_height), 767430) AS block_height FROM locations + ) + `); +} + +export function down(pgm: MigrationBuilder): void { + pgm.dropTable('chain_tip'); + pgm.createMaterializedView( + 'chain_tip', + { data: true }, + // Set block height 767430 (inscription #0 genesis) as default. + `SELECT GREATEST(MAX(block_height), 767430) AS block_height FROM locations` + ); + pgm.createIndex('chain_tip', ['block_height'], { unique: true }); +} diff --git a/package-lock.json b/package-lock.json index 479f269c..60ed95e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@fastify/swagger": "^8.3.1", "@fastify/type-provider-typebox": "^3.2.0", "@hirosystems/api-toolkit": "^1.3.1", - "@hirosystems/chainhook-client": "^1.4.1", + "@hirosystems/chainhook-client": "^1.4.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^10.0.4", "@semantic-release/git": "^10.0.1", @@ -95,6 +95,35 @@ "node": ">=18" } }, + "../chainhook/components/client/typescript": { + "name": "@hirosystems/chainhook-client", + "version": "1.4.2", + "extraneous": true, + "license": "Apache 2.0", + "dependencies": { + "@fastify/type-provider-typebox": "^3.2.0", + "fastify": "^4.15.0", + "pino": "^8.11.0", + "undici": "^5.21.2" + }, + "devDependencies": { + "@stacks/eslint-config": "^1.2.0", + "@types/jest": "^29.5.0", + "@types/node": "^18.15.7", + "@typescript-eslint/eslint-plugin": "^5.56.0", + "@typescript-eslint/parser": "^5.56.0", + "babel-jest": "^29.5.0", + "eslint": "^8.36.0", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-tsdoc": "^0.2.17", + "jest": "^29.5.0", + "prettier": "^2.8.7", + "rimraf": "^4.4.1", + "ts-jest": "^29.0.5", + "ts-node": "^10.9.1", + "typescript": "^5.0.2" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -1270,9 +1299,9 @@ } }, "node_modules/@hirosystems/chainhook-client": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@hirosystems/chainhook-client/-/chainhook-client-1.4.1.tgz", - "integrity": "sha512-ZiOk8RlLgtyDc9oRDkWHXGbzhRIMyxRIAi0OHZG/mt5yu3qNzYZ7OmK4txShxAytwDb3okMZEwPK/CrsoLPLXA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@hirosystems/chainhook-client/-/chainhook-client-1.4.2.tgz", + "integrity": "sha512-cz92ikNHwWi/ypwnitk4anOvuL7KoHLKRSQidDYyM8Pnar0yinir3GZm4q7/QCoDij0PIMg50aE7KCCzLrEhjA==", "dependencies": { "@fastify/type-provider-typebox": "^3.2.0", "fastify": "^4.15.0", @@ -19714,9 +19743,9 @@ } }, "@hirosystems/chainhook-client": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@hirosystems/chainhook-client/-/chainhook-client-1.4.1.tgz", - "integrity": "sha512-ZiOk8RlLgtyDc9oRDkWHXGbzhRIMyxRIAi0OHZG/mt5yu3qNzYZ7OmK4txShxAytwDb3okMZEwPK/CrsoLPLXA==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@hirosystems/chainhook-client/-/chainhook-client-1.4.2.tgz", + "integrity": "sha512-cz92ikNHwWi/ypwnitk4anOvuL7KoHLKRSQidDYyM8Pnar0yinir3GZm4q7/QCoDij0PIMg50aE7KCCzLrEhjA==", "requires": { "@fastify/type-provider-typebox": "^3.2.0", "fastify": "^4.15.0", diff --git a/package.json b/package.json index 723f2e3d..e9abf85d 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,7 @@ "@fastify/swagger": "^8.3.1", "@fastify/type-provider-typebox": "^3.2.0", "@hirosystems/api-toolkit": "^1.3.1", - "@hirosystems/chainhook-client": "^1.4.1", + "@hirosystems/chainhook-client": "^1.4.2", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^10.0.4", "@semantic-release/git": "^10.0.1", diff --git a/src/admin-rpc/init.ts b/src/admin-rpc/init.ts deleted file mode 100644 index 2fbc0c4e..00000000 --- a/src/admin-rpc/init.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Fastify, { FastifyPluginCallback } from 'fastify'; -import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; -import { PgStore } from '../pg/pg-store'; -import { Server } from 'http'; -import { Type } from '@sinclair/typebox'; -import { PINO_LOGGER_CONFIG, logger } from '@hirosystems/api-toolkit'; - -export const AdminApi: FastifyPluginCallback, Server, TypeBoxTypeProvider> = ( - fastify, - options, - done -) => { - fastify.post( - '/brc-20/scan', - { - schema: { - description: 'Scan for BRC-20 operations within a block range', - querystring: Type.Object({ - // TIP: The first BRC-20 token was deployed at height `779832`. This should be a good - // place to start. - start_block: Type.Integer(), - end_block: Type.Integer(), - }), - }, - }, - async (request, reply) => { - const startBlock = request.query.start_block; - const endBlock = request.query.end_block; - logger.info( - `AdminRPC scanning for BRC-20 operations from block ${startBlock} to block ${endBlock}` - ); - // TODO: Provide a way to stop this scan without restarting. - fastify.db.brc20 - .scanBlocks(startBlock, endBlock) - .then(() => logger.info(`AdminRPC finished scanning for BRC-20 operations`)) - .catch(error => logger.error(error, `AdminRPC failed to scan for BRC-20`)); - await reply.code(200).send(); - } - ); - - done(); -}; - -export async function buildAdminRpcServer(args: { db: PgStore }) { - const fastify = Fastify({ - trustProxy: true, - logger: PINO_LOGGER_CONFIG, - }).withTypeProvider(); - - fastify.decorate('db', args.db); - await fastify.register(AdminApi, { prefix: '/ordinals/admin' }); - - return fastify; -} diff --git a/src/env.ts b/src/env.ts index 9f83eed3..88f64f65 100644 --- a/src/env.ts +++ b/src/env.ts @@ -5,8 +5,8 @@ const schema = Type.Object({ /** * Run mode for this service. Allows you to control how the API runs, typically in an auto-scaled * environment. Available values are: - * * `default`: Runs the chainhook server and the REST API server (this is the default) - * * `writeonly`: Runs only the chainhook server + * * `default`: Runs the ordhook server and the REST API server (this is the default) + * * `writeonly`: Runs only the ordhook server * * `readonly`: Runs only the REST API server */ RUN_MODE: Type.Enum( @@ -20,24 +20,24 @@ const schema = Type.Object({ API_PORT: Type.Number({ default: 3000, minimum: 0, maximum: 65535 }), /** Port in which to serve the Admin RPC interface */ ADMIN_RPC_PORT: Type.Number({ default: 3001, minimum: 0, maximum: 65535 }), - /** Port in which to receive chainhook events */ + /** Port in which to receive ordhook events */ EVENT_PORT: Type.Number({ default: 3099, minimum: 0, maximum: 65535 }), /** Event server body limit (bytes) */ EVENT_SERVER_BODY_LIMIT: Type.Integer({ default: 20971520 }), - /** Hostname that will be reported to the chainhook node so it can call us back with events */ + /** Hostname that will be reported to the ordhook node so it can call us back with events */ EXTERNAL_HOSTNAME: Type.String({ default: '127.0.0.1' }), - /** Hostname of the chainhook node we'll use to register predicates */ + /** Hostname of the ordhook node we'll use to register predicates */ CHAINHOOK_NODE_RPC_HOST: Type.String({ default: '127.0.0.1' }), - /** Control port of the chainhook node */ + /** Control port of the ordhook node */ CHAINHOOK_NODE_RPC_PORT: Type.Number({ default: 20456, minimum: 0, maximum: 65535 }), /** - * Authorization token that the chainhook node must send with every event to make sure it's + * Authorization token that the ordhook node must send with every event to make sure it's * 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` + * Register ordhook 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 }), @@ -55,6 +55,8 @@ const schema = Type.Object({ /** Enables BRC-20 processing in write mode APIs */ BRC20_BLOCK_SCAN_ENABLED: Type.Boolean({ default: true }), + /** Enables inscription gap detection to prevent ingesting unordered blocks */ + INSCRIPTION_GAP_DETECTION_ENABLED: Type.Boolean({ default: true }), }); type Env = Static; diff --git a/src/index.ts b/src/index.ts index 3add9360..464a69d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,31 +1,20 @@ import { isProdEnv, logger, registerShutdownConfig } from '@hirosystems/api-toolkit'; import { buildApiServer, buildPromServer } from './api/init'; -import { startChainhookServer } from './chainhook/server'; +import { startOrdhookServer } from './ordhook/server'; import { ENV } from './env'; import { ApiMetrics } from './metrics/metrics'; import { PgStore } from './pg/pg-store'; -import { buildAdminRpcServer } from './admin-rpc/init'; async function initBackgroundServices(db: PgStore) { logger.info('Initializing background services...'); - const server = await startChainhookServer({ db }); + const server = await startOrdhookServer({ db }); registerShutdownConfig({ - name: 'Chainhook Server', + name: 'Ordhook Server', forceKillable: false, handler: async () => { await server.close(); }, }); - - const adminRpcServer = await buildAdminRpcServer({ db }); - registerShutdownConfig({ - name: 'Admin RPC Server', - forceKillable: false, - handler: async () => { - await adminRpcServer.close(); - }, - }); - await adminRpcServer.listen({ host: ENV.API_HOST, port: ENV.ADMIN_RPC_PORT }); } async function initApiService(db: PgStore) { diff --git a/src/chainhook/server.ts b/src/ordhook/server.ts similarity index 71% rename from src/chainhook/server.ts rename to src/ordhook/server.ts index d5e6dec2..b7fae7b0 100644 --- a/src/chainhook/server.ts +++ b/src/ordhook/server.ts @@ -10,15 +10,15 @@ import { } from '@hirosystems/chainhook-client'; import { logger } from '@hirosystems/api-toolkit'; -export const CHAINHOOK_BASE_PATH = `http://${ENV.CHAINHOOK_NODE_RPC_HOST}:${ENV.CHAINHOOK_NODE_RPC_PORT}`; +export const ORDHOOK_BASE_PATH = `http://${ENV.CHAINHOOK_NODE_RPC_HOST}:${ENV.CHAINHOOK_NODE_RPC_PORT}`; export const PREDICATE_UUID = randomUUID(); /** - * Starts the chainhooks event server. + * Starts the Ordhook event observer. * @param args - DB * @returns ChainhookEventObserver instance */ -export async function startChainhookServer(args: { db: PgStore }): Promise { +export async function startOrdhookServer(args: { db: PgStore }): Promise { const predicates: ServerPredicate[] = []; if (ENV.CHAINHOOK_AUTO_PREDICATE_REGISTRATION) { const blockHeight = await args.db.getChainTipBlockHeight(); @@ -48,12 +48,14 @@ export async function startChainhookServer(args: { db: PgStore }): Promise { + const server = new ChainhookEventObserver(serverOpts, ordhookOpts); + await server.start(predicates, async (uuid: string, payload: Payload) => { + logger.info(`OrdhookServer received payload from predicate ${uuid}`); await args.db.updateInscriptions(payload); }); return server; diff --git a/src/pg/brc20/brc20-pg-store.ts b/src/pg/brc20/brc20-pg-store.ts index dd0ee33a..c2ba0969 100644 --- a/src/pg/brc20/brc20-pg-store.ts +++ b/src/pg/brc20/brc20-pg-store.ts @@ -2,11 +2,13 @@ import { BasePgStoreModule, logger } from '@hirosystems/api-toolkit'; import * as postgres from 'postgres'; import { hexToBuffer } from '../../api/util/helpers'; import { - DbInscription, DbInscriptionIndexPaging, - DbLocation, + DbInscriptionInsert, + DbLocationInsert, + DbLocationPointerInsert, DbLocationTransferType, DbPaginatedResult, + DbRevealInsert, } from '../types'; import { BRC20_DEPLOYS_COLUMNS, @@ -19,9 +21,7 @@ import { DbBrc20Event, DbBrc20EventOperation, DbBrc20Holder, - DbBrc20Location, DbBrc20MintEvent, - DbBrc20ScannedInscription, DbBrc20Token, DbBrc20TokenWithSupply, DbBrc20TransferEvent, @@ -38,93 +38,59 @@ export class Brc20PgStore extends BasePgStoreModule { return partials?.reduce((acc, curr) => this.sql`${acc} OR ${curr}`); } - /** - * Perform a scan of all inscriptions stored in the DB divided by block in order to look for - * BRC-20 operations. - * @param startBlock - Start at block height - * @param endBlock - End at block height - */ - async scanBlocks(startBlock: number, endBlock: number): Promise { - for (let blockHeight = startBlock; blockHeight <= endBlock; blockHeight++) { - if (blockHeight < BRC20_GENESIS_BLOCK) continue; - logger.info(`Brc20PgStore scanning block ${blockHeight}`); - await this.sqlWriteTransaction(async sql => { - const limit = 100_000; - let offset = 0; - do { - const block = await sql` - SELECT - EXISTS(SELECT location_id FROM genesis_locations WHERE location_id = l.id) AS genesis, - l.id, l.inscription_id, l.block_height, l.tx_id, l.tx_index, l.address, - l.transfer_type - FROM locations AS l - INNER JOIN inscriptions AS i ON l.inscription_id = i.id - WHERE l.block_height = ${blockHeight} - AND i.number >= 0 - AND i.mime_type IN ('application/json', 'text/plain') - ORDER BY tx_index ASC - LIMIT ${limit} - OFFSET ${offset} - `; - if (block.count === 0) break; - await this.insertOperations(block); - if (block.count < limit) break; - offset += limit; - } while (true); - }); - } - } - - async insertOperations(writes: DbBrc20ScannedInscription[]): Promise { - if (writes.length === 0) return; - for (const write of writes) { - if (write.genesis) { - if (write.transfer_type != DbLocationTransferType.transferred) continue; - const content = await this.sql<{ content: string }[]>` - SELECT content FROM inscriptions WHERE id = ${write.inscription_id} - `; + async insertOperations(args: { + reveals: DbRevealInsert[]; + pointers: DbLocationPointerInsert[]; + }): Promise { + for (const [i, reveal] of args.reveals.entries()) { + const pointer = args.pointers[i]; + if (parseInt(pointer.block_height) < BRC20_GENESIS_BLOCK) continue; + if (reveal.inscription) { + if (reveal.location.transfer_type != DbLocationTransferType.transferred) continue; const brc20 = brc20FromInscriptionContent( - hexToBuffer(content[0].content).toString('utf-8') + hexToBuffer(reveal.inscription.content as string).toString('utf-8') ); if (brc20) { switch (brc20.op) { case 'deploy': - await this.insertDeploy({ op: brc20, location: write }); + await this.insertDeploy({ brc20, reveal, pointer }); break; case 'mint': - await this.insertMint({ op: brc20, location: write }); + await this.insertMint({ brc20, reveal, pointer }); break; case 'transfer': - await this.insertTransfer({ op: brc20, location: write }); + await this.insertTransfer({ brc20, reveal, pointer }); break; } } } else { - await this.applyTransfer(write); + await this.applyTransfer({ reveal, pointer }); } } } - async applyTransfer(location: DbBrc20ScannedInscription): Promise { + async applyTransfer(args: { + reveal: DbRevealInsert; + pointer: DbLocationPointerInsert; + }): Promise { await this.sqlWriteTransaction(async sql => { - if (!location.inscription_id) return; // Get the sender address for this transfer. We need to get this in a separate query to know // if we should alter the write query to accomodate a "return to sender" scenario. const fromAddressRes = await sql<{ from_address: string }[]>` - SELECT from_address FROM brc20_transfers WHERE inscription_id = ${location.inscription_id} + SELECT from_address FROM brc20_transfers WHERE inscription_id = ${args.pointer.inscription_id} `; if (fromAddressRes.count === 0) return; const fromAddress = fromAddressRes[0].from_address; // Is this transfer sent as fee or from the same sender? If so, we'll return the balance. // Is it burnt? Mark as empty owner. const returnToSender = - location.transfer_type == DbLocationTransferType.spentInFees || - fromAddress == location.address; + args.reveal.location.transfer_type == DbLocationTransferType.spentInFees || + fromAddress == args.pointer.address; const toAddress = returnToSender ? fromAddress - : location.transfer_type == DbLocationTransferType.burnt + : args.reveal.location.transfer_type == DbLocationTransferType.burnt ? '' - : location.address; + : args.pointer.address; // Check if we have a valid transfer inscription emitted by this address that hasn't been sent // to another address before. Use `LIMIT 3` as a quick way of checking if we have just inserted // the first transfer for this inscription (genesis + transfer). @@ -133,10 +99,11 @@ export class Brc20PgStore extends BasePgStoreModule { SELECT t.id, t.amount, t.brc20_deploy_id, t.from_address, ROW_NUMBER() OVER() FROM locations AS l INNER JOIN brc20_transfers AS t ON t.inscription_id = l.inscription_id - WHERE l.inscription_id = ${location.inscription_id} + WHERE l.inscription_id = ${args.pointer.inscription_id} AND ( - l.block_height < ${location.block_height} - OR (l.block_height = ${location.block_height} AND l.tx_index <= ${location.tx_index}) + l.block_height < ${args.pointer.block_height} + OR (l.block_height = ${args.pointer.block_height} + AND l.tx_index <= ${args.pointer.tx_index}) ) LIMIT 3 ), @@ -152,16 +119,16 @@ export class Brc20PgStore extends BasePgStoreModule { ), balance_insert_from AS ( INSERT INTO brc20_balances (inscription_id, location_id, brc20_deploy_id, address, avail_balance, trans_balance, type) ( - SELECT ${location.inscription_id}, ${location.id}, brc20_deploy_id, from_address, 0, - -1 * amount, ${DbBrc20BalanceTypeId.transferFrom} + SELECT ${args.pointer.inscription_id}, ${args.pointer.location_id}, brc20_deploy_id, + from_address, 0, -1 * amount, ${DbBrc20BalanceTypeId.transferFrom} FROM validated_transfer ) ON CONFLICT ON CONSTRAINT brc20_balances_inscription_id_type_unique DO NOTHING ), balance_insert_to AS ( INSERT INTO brc20_balances (inscription_id, location_id, brc20_deploy_id, address, avail_balance, trans_balance, type) ( - SELECT ${location.inscription_id}, ${location.id}, brc20_deploy_id, ${toAddress}, - amount, 0, ${DbBrc20BalanceTypeId.transferTo} + SELECT ${args.pointer.inscription_id}, ${args.pointer.location_id}, brc20_deploy_id, + ${toAddress}, amount, 0, ${DbBrc20BalanceTypeId.transferTo} FROM validated_transfer ) ON CONFLICT ON CONSTRAINT brc20_balances_inscription_id_type_unique DO NOTHING @@ -221,34 +188,33 @@ export class Brc20PgStore extends BasePgStoreModule { ON CONFLICT (event_type) DO UPDATE SET count = brc20_counts_by_event_type.count + EXCLUDED.count ) INSERT INTO brc20_events (operation, inscription_id, genesis_location_id, brc20_deploy_id, transfer_id, address, from_address) ( - SELECT 'transfer_send', ${location.inscription_id}, ${location.id}, brc20_deploy_id, id, - ${toAddress}, from_address + SELECT 'transfer_send', ${args.pointer.inscription_id}, ${args.pointer.location_id}, + brc20_deploy_id, id, ${toAddress}, from_address FROM validated_transfer ) `; if (sendRes.count) - logger.info(`Brc20PgStore send transfer to ${toAddress} at block ${location.block_height}`); + logger.info( + `Brc20PgStore send transfer to ${toAddress} at block ${args.pointer.block_height}` + ); }); } private async insertDeploy(deploy: { - op: Brc20Deploy; - location: DbBrc20Location; + brc20: Brc20Deploy; + reveal: DbRevealInsert; + pointer: DbLocationPointerInsert; }): Promise { - if ( - deploy.location.transfer_type != DbLocationTransferType.transferred || - !deploy.location.inscription_id - ) - return; + if (deploy.reveal.location.transfer_type != DbLocationTransferType.transferred) return; const insert: DbBrc20DeployInsert = { - inscription_id: deploy.location.inscription_id, - block_height: deploy.location.block_height, - tx_id: deploy.location.tx_id, - address: deploy.location.address as string, - ticker: deploy.op.tick, - max: deploy.op.max, - limit: deploy.op.lim ?? null, - decimals: deploy.op.dec ?? '18', + inscription_id: deploy.pointer.inscription_id, + block_height: deploy.pointer.block_height, + tx_id: deploy.reveal.location.tx_id, + address: deploy.pointer.address as string, + ticker: deploy.brc20.tick, + max: deploy.brc20.max, + limit: deploy.brc20.lim ?? null, + decimals: deploy.brc20.dec ?? '18', tx_count: 1, }; const deployRes = await this.sql` @@ -264,7 +230,7 @@ export class Brc20PgStore extends BasePgStoreModule { ), address_event_type_count_increase AS ( INSERT INTO brc20_counts_by_address_event_type (address, deploy) - (SELECT ${deploy.location.address}, COALESCE(COUNT(*), 0) FROM deploy_insert) + (SELECT ${deploy.pointer.address}, COALESCE(COUNT(*), 0) FROM deploy_insert) ON CONFLICT (address) DO UPDATE SET deploy = brc20_counts_by_address_event_type.deploy + EXCLUDED.deploy ), token_count_increase AS ( @@ -273,23 +239,23 @@ export class Brc20PgStore extends BasePgStoreModule { ON CONFLICT (token_type) DO UPDATE SET count = brc20_counts_by_tokens.count + EXCLUDED.count ) INSERT INTO brc20_events (operation, inscription_id, genesis_location_id, brc20_deploy_id, deploy_id, address) ( - SELECT 'deploy', ${deploy.location.inscription_id}, ${deploy.location.id}, id, id, - ${deploy.location.address} + SELECT 'deploy', ${deploy.pointer.inscription_id}, ${deploy.pointer.location_id}, id, id, + ${deploy.pointer.address} FROM deploy_insert ) `; if (deployRes.count) logger.info( - `Brc20PgStore deploy ${deploy.op.tick} by ${deploy.location.address} at block ${deploy.location.block_height}` + `Brc20PgStore deploy ${deploy.brc20.tick} by ${deploy.pointer.address} at block ${deploy.pointer.block_height}` ); } - private async insertMint(mint: { op: Brc20Mint; location: DbBrc20Location }): Promise { - if ( - mint.location.transfer_type != DbLocationTransferType.transferred || - !mint.location.inscription_id - ) - return; + private async insertMint(mint: { + brc20: Brc20Mint; + reveal: DbRevealInsert; + pointer: DbLocationPointerInsert; + }): Promise { + if (mint.reveal.location.transfer_type != DbLocationTransferType.transferred) return; // Check the following conditions: // * Is the mint amount within the allowed token limits? // * Is the number of decimals correct? @@ -298,20 +264,20 @@ export class Brc20PgStore extends BasePgStoreModule { WITH mint_data AS ( SELECT id, decimals, "limit", max, minted_supply FROM brc20_deploys - WHERE ticker_lower = LOWER(${mint.op.tick}) AND minted_supply < max + WHERE ticker_lower = LOWER(${mint.brc20.tick}) AND minted_supply < max ), validated_mint AS ( SELECT id AS brc20_deploy_id, - LEAST(${mint.op.amt}::numeric, max - minted_supply) AS real_mint_amount + LEAST(${mint.brc20.amt}::numeric, max - minted_supply) AS real_mint_amount FROM mint_data - WHERE ("limit" IS NULL OR ${mint.op.amt}::numeric <= "limit") - AND (SCALE(${mint.op.amt}::numeric) <= decimals) + WHERE ("limit" IS NULL OR ${mint.brc20.amt}::numeric <= "limit") + AND (SCALE(${mint.brc20.amt}::numeric) <= decimals) ), mint_insert AS ( INSERT INTO brc20_mints (inscription_id, brc20_deploy_id, block_height, tx_id, address, amount) ( - SELECT ${mint.location.inscription_id}, brc20_deploy_id, ${mint.location.block_height}, - ${mint.location.tx_id}, ${mint.location.address}, ${mint.op.amt}::numeric + SELECT ${mint.pointer.inscription_id}, brc20_deploy_id, ${mint.pointer.block_height}, + ${mint.reveal.location.tx_id}, ${mint.pointer.address}, ${mint.brc20.amt}::numeric FROM validated_mint ) ON CONFLICT (inscription_id) DO NOTHING @@ -326,15 +292,15 @@ export class Brc20PgStore extends BasePgStoreModule { ), balance_insert AS ( INSERT INTO brc20_balances (inscription_id, location_id, brc20_deploy_id, address, avail_balance, trans_balance, type) ( - SELECT ${mint.location.inscription_id}, ${mint.location.id}, brc20_deploy_id, - ${mint.location.address}, real_mint_amount, 0, ${DbBrc20BalanceTypeId.mint} + SELECT ${mint.pointer.inscription_id}, ${mint.pointer.location_id}, brc20_deploy_id, + ${mint.pointer.address}, real_mint_amount, 0, ${DbBrc20BalanceTypeId.mint} FROM validated_mint ) ON CONFLICT ON CONSTRAINT brc20_balances_inscription_id_type_unique DO NOTHING ), total_balance_insert AS ( INSERT INTO brc20_total_balances (brc20_deploy_id, address, avail_balance, trans_balance, total_balance) ( - SELECT brc20_deploy_id, ${mint.location.address}, real_mint_amount, 0, real_mint_amount + SELECT brc20_deploy_id, ${mint.pointer.address}, real_mint_amount, 0, real_mint_amount FROM validated_mint ) ON CONFLICT ON CONSTRAINT brc20_total_balances_unique DO UPDATE SET @@ -348,29 +314,26 @@ export class Brc20PgStore extends BasePgStoreModule { ), address_event_type_count_increase AS ( INSERT INTO brc20_counts_by_address_event_type (address, mint) - (SELECT ${mint.location.address}, COALESCE(COUNT(*), 0) FROM validated_mint) + (SELECT ${mint.pointer.address}, COALESCE(COUNT(*), 0) FROM validated_mint) ON CONFLICT (address) DO UPDATE SET mint = brc20_counts_by_address_event_type.mint + EXCLUDED.mint ) INSERT INTO brc20_events (operation, inscription_id, genesis_location_id, brc20_deploy_id, mint_id, address) ( - SELECT 'mint', ${mint.location.inscription_id}, ${mint.location.id}, brc20_deploy_id, id, ${mint.location.address} + SELECT 'mint', ${mint.pointer.inscription_id}, ${mint.pointer.location_id}, brc20_deploy_id, id, ${mint.pointer.address} FROM mint_insert ) `; if (mintRes.count) logger.info( - `Brc20PgStore mint ${mint.op.tick} (${mint.op.amt}) by ${mint.location.address} at block ${mint.location.block_height}` + `Brc20PgStore mint ${mint.brc20.tick} (${mint.brc20.amt}) by ${mint.pointer.address} at block ${mint.pointer.block_height}` ); } private async insertTransfer(transfer: { - op: Brc20Transfer; - location: DbBrc20Location; + brc20: Brc20Transfer; + reveal: DbRevealInsert; + pointer: DbLocationPointerInsert; }): Promise { - if ( - transfer.location.transfer_type != DbLocationTransferType.transferred || - !transfer.location.inscription_id - ) - return; + if (transfer.reveal.location.transfer_type != DbLocationTransferType.transferred) return; // Check the following conditions: // * Do we have enough available balance to do this transfer? const transferRes = await this.sql` @@ -378,19 +341,19 @@ export class Brc20PgStore extends BasePgStoreModule { SELECT b.brc20_deploy_id, COALESCE(SUM(b.avail_balance), 0) AS avail_balance FROM brc20_balances AS b INNER JOIN brc20_deploys AS d ON b.brc20_deploy_id = d.id - WHERE d.ticker_lower = LOWER(${transfer.op.tick}) - AND b.address = ${transfer.location.address} + WHERE d.ticker_lower = LOWER(${transfer.brc20.tick}) + AND b.address = ${transfer.pointer.address} GROUP BY b.brc20_deploy_id ), validated_transfer AS ( SELECT * FROM balance_data - WHERE avail_balance >= ${transfer.op.amt}::numeric + WHERE avail_balance >= ${transfer.brc20.amt}::numeric ), transfer_insert AS ( INSERT INTO brc20_transfers (inscription_id, brc20_deploy_id, block_height, tx_id, from_address, to_address, amount) ( - SELECT ${transfer.location.inscription_id}, brc20_deploy_id, - ${transfer.location.block_height}, ${transfer.location.tx_id}, - ${transfer.location.address}, NULL, ${transfer.op.amt}::numeric + SELECT ${transfer.pointer.inscription_id}, brc20_deploy_id, + ${transfer.pointer.block_height}, ${transfer.reveal.location.tx_id}, + ${transfer.pointer.address}, NULL, ${transfer.brc20.amt}::numeric FROM validated_transfer ) ON CONFLICT (inscription_id) DO NOTHING @@ -398,19 +361,19 @@ export class Brc20PgStore extends BasePgStoreModule { ), balance_insert AS ( INSERT INTO brc20_balances (inscription_id, location_id, brc20_deploy_id, address, avail_balance, trans_balance, type) ( - SELECT ${transfer.location.inscription_id}, ${transfer.location.id}, brc20_deploy_id, - ${transfer.location.address}, -1 * ${transfer.op.amt}::numeric, - ${transfer.op.amt}::numeric, ${DbBrc20BalanceTypeId.transferIntent} + SELECT ${transfer.pointer.inscription_id}, ${transfer.pointer.location_id}, brc20_deploy_id, + ${transfer.pointer.address}, -1 * ${transfer.brc20.amt}::numeric, + ${transfer.brc20.amt}::numeric, ${DbBrc20BalanceTypeId.transferIntent} FROM validated_transfer ) ON CONFLICT ON CONSTRAINT brc20_balances_inscription_id_type_unique DO NOTHING ), total_balance_update AS ( UPDATE brc20_total_balances SET - avail_balance = avail_balance - ${transfer.op.amt}::numeric, - trans_balance = trans_balance + ${transfer.op.amt}::numeric + avail_balance = avail_balance - ${transfer.brc20.amt}::numeric, + trans_balance = trans_balance + ${transfer.brc20.amt}::numeric WHERE brc20_deploy_id = (SELECT brc20_deploy_id FROM validated_transfer) - AND address = ${transfer.location.address} + AND address = ${transfer.pointer.address} ), deploy_update AS ( UPDATE brc20_deploys @@ -424,23 +387,25 @@ export class Brc20PgStore extends BasePgStoreModule { ), address_event_type_count_increase AS ( INSERT INTO brc20_counts_by_address_event_type (address, transfer) - (SELECT ${transfer.location.address}, COALESCE(COUNT(*), 0) FROM validated_transfer) + (SELECT ${transfer.pointer.address}, COALESCE(COUNT(*), 0) FROM validated_transfer) ON CONFLICT (address) DO UPDATE SET transfer = brc20_counts_by_address_event_type.transfer + EXCLUDED.transfer ) INSERT INTO brc20_events (operation, inscription_id, genesis_location_id, brc20_deploy_id, transfer_id, address) ( - SELECT 'transfer', ${transfer.location.inscription_id}, ${transfer.location.id}, brc20_deploy_id, id, ${transfer.location.address} + SELECT 'transfer', ${transfer.pointer.inscription_id}, ${transfer.pointer.location_id}, brc20_deploy_id, id, ${transfer.pointer.address} FROM transfer_insert ) `; if (transferRes.count) logger.info( - `Brc20PgStore transfer ${transfer.op.tick} (${transfer.op.amt}) by ${transfer.location.address} at block ${transfer.location.block_height}` + `Brc20PgStore transfer ${transfer.brc20.tick} (${transfer.brc20.amt}) by ${transfer.pointer.address} at block ${transfer.pointer.block_height}` ); } - async rollBackInscription(args: { inscription: DbInscription }): Promise { + async rollBackInscription(args: { inscription: DbInscriptionInsert }): Promise { const events = await this.sql` - SELECT * FROM brc20_events WHERE inscription_id = ${args.inscription.id} + SELECT e.* FROM brc20_events AS e + INNER JOIN inscriptions AS i ON i.id = e.inscription_id + WHERE i.genesis_id = ${args.inscription.genesis_id} `; if (events.count === 0) return; // Traverse all activities generated by this inscription and roll back actions that are NOT @@ -460,9 +425,11 @@ export class Brc20PgStore extends BasePgStoreModule { } } - async rollBackLocation(args: { location: DbLocation }): Promise { + async rollBackLocation(args: { location: DbLocationInsert }): Promise { const events = await this.sql` - SELECT * FROM brc20_events WHERE genesis_location_id = ${args.location.id} + SELECT e.* FROM brc20_events AS e + INNER JOIN locations AS l ON l.id = e.genesis_location_id + WHERE output = ${args.location.output} AND "offset" = ${args.location.offset} `; if (events.count === 0) return; // Traverse all activities generated by this location and roll back actions that are NOT diff --git a/src/pg/brc20/types.ts b/src/pg/brc20/types.ts index 0bb72f33..269397da 100644 --- a/src/pg/brc20/types.ts +++ b/src/pg/brc20/types.ts @@ -10,10 +10,6 @@ export type DbBrc20Location = { transfer_type: DbLocationTransferType; }; -export type DbBrc20ScannedInscription = DbBrc20Location & { - genesis: boolean; -}; - export type DbBrc20DeployInsert = { inscription_id: string; block_height: string; diff --git a/src/pg/counts/counts-pg-store.ts b/src/pg/counts/counts-pg-store.ts index 8d983719..719b1b58 100644 --- a/src/pg/counts/counts-pg-store.ts +++ b/src/pg/counts/counts-pg-store.ts @@ -5,6 +5,7 @@ import { DbInscriptionIndexFilters, DbInscriptionInsert, DbInscriptionType, + DbLocationInsert, DbLocationPointer, } from '../types'; import { DbInscriptionIndexResultCountType } from './types'; @@ -103,7 +104,10 @@ export class CountsPgStore extends BasePgStoreModule { `; } - async rollBackInscription(args: { inscription: DbInscription }): Promise { + async rollBackInscription(args: { + inscription: DbInscriptionInsert; + location: DbLocationInsert; + }): Promise { await this.sql` WITH decrease_mime_type AS ( UPDATE counts_by_mime_type SET count = count - 1 @@ -119,19 +123,14 @@ export class CountsPgStore extends BasePgStoreModule { ), decrease_type AS ( UPDATE counts_by_type SET count = count - 1 WHERE type = ${ - parseInt(args.inscription.number) < 0 - ? DbInscriptionType.cursed - : DbInscriptionType.blessed + args.inscription.number < 0 ? DbInscriptionType.cursed : DbInscriptionType.blessed } ), decrease_genesis AS ( - UPDATE counts_by_genesis_address SET count = count - 1 WHERE address = ( - SELECT address FROM current_locations WHERE inscription_id = ${args.inscription.id} - ) - ) - UPDATE counts_by_address SET count = count - 1 WHERE address = ( - SELECT address FROM current_locations WHERE inscription_id = ${args.inscription.id} + UPDATE counts_by_genesis_address SET count = count - 1 + WHERE address = ${args.location.address} ) + UPDATE counts_by_address SET count = count - 1 WHERE address = ${args.location.address} `; } diff --git a/src/pg/helpers.ts b/src/pg/helpers.ts index 443b45d2..6bd678fe 100644 --- a/src/pg/helpers.ts +++ b/src/pg/helpers.ts @@ -1,6 +1,14 @@ -import { PgBytea } from '@hirosystems/api-toolkit'; -import { hexToBuffer } from '../api/util/helpers'; -import { BadPayloadRequestError } from '@hirosystems/chainhook-client'; +import { PgBytea, toEnumValue } from '@hirosystems/api-toolkit'; +import { hexToBuffer, normalizedHexString, parseSatPoint } from '../api/util/helpers'; +import { + BadPayloadRequestError, + BitcoinEvent, + BitcoinInscriptionRevealed, + BitcoinInscriptionTransferred, +} from '@hirosystems/chainhook-client'; +import { ENV } from '../env'; +import { DbLocationTransferType, DbRevealInsert } from './types'; +import { OrdinalSatoshi } from '../api/util/ordinal-satoshi'; /** * Check if writing a block would create an inscription number gap @@ -13,6 +21,7 @@ export function assertNoBlockInscriptionGap(args: { currentBlockHeight: number; newBlockHeight: number; }) { + if (!ENV.INSCRIPTION_GAP_DETECTION_ENABLED) return; args.newNumbers.sort((a, b) => a - b); for (let n = 0; n < args.newNumbers.length; n++) { const curr = args.currentNumber + n; @@ -65,7 +74,126 @@ export function objRemoveUndefinedValues(obj: object) { Object.keys(obj).forEach(key => (obj as any)[key] === undefined && delete (obj as any)[key]); } +/** + * Replace null bytes on a string with an empty string + * @param input - String + * @returns Sanitized string + */ export function removeNullBytes(input: string): string { - // Replace null byte with an empty string return input.replace(/\x00/g, ''); } + +function revealInsertFromOrdhookInscriptionRevealed(args: { + block_height: number; + block_hash: string; + tx_id: string; + timestamp: number; + reveal: BitcoinInscriptionRevealed; +}): DbRevealInsert { + const satoshi = new OrdinalSatoshi(args.reveal.ordinal_number); + const satpoint = parseSatPoint(args.reveal.satpoint_post_inscription); + const recursive_refs = getInscriptionRecursion(args.reveal.content_bytes); + const contentType = removeNullBytes(args.reveal.content_type); + return { + inscription: { + genesis_id: args.reveal.inscription_id, + mime_type: contentType.split(';')[0], + content_type: contentType, + content_length: args.reveal.content_length, + number: args.reveal.inscription_number, + content: removeNullBytes(args.reveal.content_bytes), + fee: args.reveal.inscription_fee.toString(), + curse_type: args.reveal.curse_type ? JSON.stringify(args.reveal.curse_type) : null, + sat_ordinal: args.reveal.ordinal_number.toString(), + sat_rarity: satoshi.rarity, + sat_coinbase_height: satoshi.blockHeight, + recursive: recursive_refs.length > 0, + }, + location: { + block_hash: args.block_hash, + block_height: args.block_height, + tx_id: args.tx_id, + tx_index: args.reveal.tx_index, + block_transfer_index: null, + genesis_id: args.reveal.inscription_id, + address: args.reveal.inscriber_address, + output: `${satpoint.tx_id}:${satpoint.vout}`, + offset: satpoint.offset ?? null, + prev_output: null, + prev_offset: null, + value: args.reveal.inscription_output_value.toString(), + timestamp: args.timestamp, + transfer_type: DbLocationTransferType.transferred, + }, + recursive_refs, + }; +} + +function revealInsertFromOrdhookInscriptionTransferred(args: { + block_height: number; + block_hash: string; + tx_id: string; + timestamp: number; + blockTransferIndex: number; + transfer: BitcoinInscriptionTransferred; +}): DbRevealInsert { + const satpoint = parseSatPoint(args.transfer.satpoint_post_transfer); + const prevSatpoint = parseSatPoint(args.transfer.satpoint_pre_transfer); + return { + location: { + block_hash: args.block_hash, + block_height: args.block_height, + tx_id: args.tx_id, + tx_index: args.transfer.tx_index, + block_transfer_index: args.blockTransferIndex, + genesis_id: args.transfer.inscription_id, + address: args.transfer.destination.value ?? null, + output: `${satpoint.tx_id}:${satpoint.vout}`, + offset: satpoint.offset ?? null, + prev_output: `${prevSatpoint.tx_id}:${prevSatpoint.vout}`, + prev_offset: prevSatpoint.offset ?? null, + value: args.transfer.post_transfer_output_value + ? args.transfer.post_transfer_output_value.toString() + : null, + timestamp: args.timestamp, + transfer_type: + toEnumValue(DbLocationTransferType, args.transfer.destination.type) ?? + DbLocationTransferType.transferred, + }, + }; +} + +export function revealInsertsFromOrdhookEvent(event: BitcoinEvent): DbRevealInsert[] { + // Keep the relative ordering of a transfer within a block for faster future reads. + let blockTransferIndex = 0; + const block_height = event.block_identifier.index; + const block_hash = normalizedHexString(event.block_identifier.hash); + const writes: DbRevealInsert[] = []; + for (const tx of event.transactions) { + const tx_id = normalizedHexString(tx.transaction_identifier.hash); + for (const operation of tx.metadata.ordinal_operations) { + if (operation.inscription_revealed) + writes.push( + revealInsertFromOrdhookInscriptionRevealed({ + block_hash, + block_height, + tx_id, + timestamp: event.timestamp, + reveal: operation.inscription_revealed, + }) + ); + if (operation.inscription_transferred) + writes.push( + revealInsertFromOrdhookInscriptionTransferred({ + block_hash, + block_height, + tx_id, + timestamp: event.timestamp, + blockTransferIndex: blockTransferIndex++, + transfer: operation.inscription_transferred, + }) + ); + } + } + return writes; +} diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 25fdbc52..ab8a6b6b 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -7,21 +7,19 @@ import { connectPostgres, logger, runMigrations, + stopwatch, } from '@hirosystems/api-toolkit'; import { BitcoinEvent, Payload } from '@hirosystems/chainhook-client'; import * as path from 'path'; import * as postgres from 'postgres'; import { Order, OrderBy } from '../api/schemas'; -import { normalizedHexString, parseSatPoint } from '../api/util/helpers'; -import { OrdinalSatoshi } from '../api/util/ordinal-satoshi'; import { ENV } from '../env'; import { Brc20PgStore } from './brc20/brc20-pg-store'; import { CountsPgStore } from './counts/counts-pg-store'; import { getIndexResultCountType } from './counts/helpers'; -import { assertNoBlockInscriptionGap, getInscriptionRecursion, removeNullBytes } from './helpers'; +import { assertNoBlockInscriptionGap, revealInsertsFromOrdhookEvent } from './helpers'; import { DbFullyLocatedInscriptionResult, - DbInscription, DbInscriptionContent, DbInscriptionCountPerBlock, DbInscriptionCountPerBlockFilters, @@ -31,15 +29,13 @@ import { DbInscriptionInsert, DbInscriptionLocationChange, DbLocation, + DbLocationInsert, DbLocationPointer, DbLocationPointerInsert, - DbLocationTransferType, DbPaginatedResult, DbRevealInsert, - INSCRIPTIONS_COLUMNS, LOCATIONS_COLUMNS, } from './types'; -import { toEnumValue } from '@hirosystems/api-toolkit'; export const MIGRATIONS_DIR = path.join(__dirname, '../../migrations'); export const ORDINALS_GENESIS_BLOCK = 767430; @@ -81,195 +77,77 @@ export class PgStore extends BasePgStore { } /** - * Inserts inscription genesis and transfers from Chainhook events. Also handles rollbacks from - * chain re-orgs and materialized view refreshes. - * @param args - Apply/Rollback Chainhook events + * Inserts inscription genesis and transfers from Ordhook events. Also handles rollbacks from + * chain re-orgs. + * @param args - Apply/Rollback Ordhook events */ async updateInscriptions(payload: Payload): Promise { let updatedBlockHeightMin = Infinity; await this.sqlWriteTransaction(async sql => { - // Check where we're at in terms of ingestion, e.g. block height and max blessed inscription - // number. This will let us determine if we should skip ingesting this block or throw an error - // if a gap is detected. - const currentBlessedNumber = (await this.getMaxInscriptionNumber()) ?? -1; - const currentBlockHeight = await this.getChainTipBlockHeight(); - const newBlessedNumbers: number[] = []; - + // ROLLBACK for (const rollbackEvent of payload.rollback) { - // TODO: Optimize rollbacks just as we optimized applys. const event = rollbackEvent as BitcoinEvent; - const block_height = event.block_identifier.index; - for (const tx of event.transactions) { - for (const operation of tx.metadata.ordinal_operations) { - if (operation.inscription_revealed) { - const number = operation.inscription_revealed.inscription_number; - const genesis_id = operation.inscription_revealed.inscription_id; - await this.rollBackInscription({ genesis_id, number, block_height }); - } - if (operation.cursed_inscription_revealed) { - const number = operation.cursed_inscription_revealed.inscription_number; - const genesis_id = operation.cursed_inscription_revealed.inscription_id; - await this.rollBackInscription({ genesis_id, number, block_height }); - } - if (operation.inscription_transferred) { - const genesis_id = operation.inscription_transferred.inscription_id; - const satpoint = parseSatPoint( - operation.inscription_transferred.satpoint_post_transfer - ); - const output = `${satpoint.tx_id}:${satpoint.vout}`; - await this.rollBackLocation({ genesis_id, output, block_height }); - } - } - } + logger.info(`PgStore rolling back block ${event.block_identifier.index}`); + const time = stopwatch(); + const rollbacks = revealInsertsFromOrdhookEvent(event); + for (const writeChunk of batchIterate(rollbacks, 1000)) + await this.rollBackInscriptions(writeChunk); updatedBlockHeightMin = Math.min(updatedBlockHeightMin, event.block_identifier.index); + logger.info( + `PgStore rolled back block ${ + event.block_identifier.index + } in ${time.getElapsedSeconds()}s` + ); + await this.updateChainTipBlockHeight(event.block_identifier.index - 1); } - let blockTransferIndex = 0; + // APPLY for (const applyEvent of payload.apply) { + // Check where we're at in terms of ingestion, e.g. block height and max blessed inscription + // number. This will let us determine if we should skip ingesting this block or throw an + // error if a gap is detected. + const currentBlessedNumber = (await this.getMaxInscriptionNumber()) ?? -1; + const currentBlockHeight = await this.getChainTipBlockHeight(); const event = applyEvent as BitcoinEvent; - const block_height = event.block_identifier.index; - if (block_height <= currentBlockHeight && block_height !== ORDINALS_GENESIS_BLOCK) { + if ( + ENV.INSCRIPTION_GAP_DETECTION_ENABLED && + event.block_identifier.index <= currentBlockHeight && + event.block_identifier.index !== ORDINALS_GENESIS_BLOCK + ) { logger.info( - `PgStore skipping ingestion for previously seen block ${block_height}, current chain tip is at ${currentBlockHeight}` + `PgStore skipping ingestion for previously seen block ${event.block_identifier.index}, current chain tip is at ${currentBlockHeight}` ); - return; - } - const block_hash = normalizedHexString(event.block_identifier.hash); - const writes: DbRevealInsert[] = []; - for (const tx of event.transactions) { - const tx_id = normalizedHexString(tx.transaction_identifier.hash); - for (const operation of tx.metadata.ordinal_operations) { - if (operation.inscription_revealed) { - const reveal = operation.inscription_revealed; - if (reveal.inscription_number >= 0) - newBlessedNumbers.push(parseInt(`${reveal.inscription_number}`)); - const satoshi = new OrdinalSatoshi(reveal.ordinal_number); - const satpoint = parseSatPoint(reveal.satpoint_post_inscription); - const recursive_refs = getInscriptionRecursion(reveal.content_bytes); - const contentType = removeNullBytes(reveal.content_type); - writes.push({ - inscription: { - genesis_id: reveal.inscription_id, - mime_type: contentType.split(';')[0], - content_type: contentType, - content_length: reveal.content_length, - number: reveal.inscription_number, - content: removeNullBytes(reveal.content_bytes), - fee: reveal.inscription_fee.toString(), - curse_type: null, - sat_ordinal: reveal.ordinal_number.toString(), - sat_rarity: satoshi.rarity, - sat_coinbase_height: satoshi.blockHeight, - recursive: recursive_refs.length > 0, - }, - location: { - block_hash, - block_height, - tx_id, - tx_index: reveal.tx_index, - block_transfer_index: null, - genesis_id: reveal.inscription_id, - address: reveal.inscriber_address, - output: `${satpoint.tx_id}:${satpoint.vout}`, - offset: satpoint.offset ?? null, - prev_output: null, - prev_offset: null, - value: reveal.inscription_output_value.toString(), - timestamp: event.timestamp, - transfer_type: DbLocationTransferType.transferred, - }, - recursive_refs, - }); - } - if (operation.cursed_inscription_revealed) { - const reveal = operation.cursed_inscription_revealed; - const satoshi = new OrdinalSatoshi(reveal.ordinal_number); - const satpoint = parseSatPoint(reveal.satpoint_post_inscription); - const recursive_refs = getInscriptionRecursion(reveal.content_bytes); - const contentType = removeNullBytes(reveal.content_type); - writes.push({ - inscription: { - genesis_id: reveal.inscription_id, - mime_type: contentType.split(';')[0], - content_type: contentType, - content_length: reveal.content_length, - number: reveal.inscription_number, - content: removeNullBytes(reveal.content_bytes), - fee: reveal.inscription_fee.toString(), - curse_type: JSON.stringify(reveal.curse_type), - sat_ordinal: reveal.ordinal_number.toString(), - sat_rarity: satoshi.rarity, - sat_coinbase_height: satoshi.blockHeight, - recursive: recursive_refs.length > 0, - }, - location: { - block_hash, - block_height, - tx_id, - tx_index: reveal.tx_index, - block_transfer_index: null, - genesis_id: reveal.inscription_id, - address: reveal.inscriber_address, - output: `${satpoint.tx_id}:${satpoint.vout}`, - offset: satpoint.offset ?? null, - prev_output: null, - prev_offset: null, - value: reveal.inscription_output_value.toString(), - timestamp: event.timestamp, - transfer_type: DbLocationTransferType.transferred, - }, - recursive_refs, - }); - } - if (operation.inscription_transferred) { - const transfer = operation.inscription_transferred; - const satpoint = parseSatPoint(transfer.satpoint_post_transfer); - const prevSatpoint = parseSatPoint(transfer.satpoint_pre_transfer); - writes.push({ - location: { - block_hash, - block_height, - tx_id, - tx_index: transfer.tx_index, - block_transfer_index: blockTransferIndex++, - genesis_id: transfer.inscription_id, - address: transfer.destination.value ?? null, - output: `${satpoint.tx_id}:${satpoint.vout}`, - offset: satpoint.offset ?? null, - prev_output: `${prevSatpoint.tx_id}:${prevSatpoint.vout}`, - prev_offset: prevSatpoint.offset ?? null, - value: transfer.post_transfer_output_value - ? transfer.post_transfer_output_value.toString() - : null, - timestamp: event.timestamp, - transfer_type: - toEnumValue(DbLocationTransferType, transfer.destination.type) ?? - DbLocationTransferType.transferred, - }, - }); - } - } + continue; } + logger.info(`PgStore ingesting block ${event.block_identifier.index}`); + const time = stopwatch(); + const writes = revealInsertsFromOrdhookEvent(event); + const newBlessedNumbers = writes + .filter(w => w.inscription !== undefined && w.inscription.number >= 0) + .map(w => w.inscription?.number ?? 0); assertNoBlockInscriptionGap({ currentNumber: currentBlessedNumber, newNumbers: newBlessedNumbers, currentBlockHeight: currentBlockHeight, - newBlockHeight: block_height, + newBlockHeight: event.block_identifier.index, }); - // Divide insertion array into chunks of 4000 in order to avoid the postgres limit of 65534 - // query params. for (const writeChunk of batchIterate(writes, 4000)) await this.insertInscriptions(writeChunk); updatedBlockHeightMin = Math.min(updatedBlockHeightMin, event.block_identifier.index); - if (ENV.BRC20_BLOCK_SCAN_ENABLED) - await this.brc20.scanBlocks(event.block_identifier.index, event.block_identifier.index); + logger.info( + `PgStore ingested block ${event.block_identifier.index} in ${time.getElapsedSeconds()}s` + ); + await this.updateChainTipBlockHeight(event.block_identifier.index); } }); - await this.refreshMaterializedView('chain_tip'); if (updatedBlockHeightMin !== Infinity) await this.normalizeInscriptionCount({ min_block_height: updatedBlockHeightMin }); } + private async updateChainTipBlockHeight(block_height: number): Promise { + await this.sql`UPDATE chain_tip SET block_height = ${block_height}`; + } + async getChainTipBlockHeight(): Promise { const result = await this.sql<{ block_height: string }[]>`SELECT block_height FROM chain_tip`; return parseInt(result[0].block_height); @@ -606,35 +484,12 @@ export class PgStore extends BasePgStore { `; // roughly 35 days of blocks, assuming 10 minute block times on a full database } - private async getInscription(args: { genesis_id: string }): Promise { - const query = await this.sql` - SELECT ${this.sql(INSCRIPTIONS_COLUMNS)} - FROM inscriptions - WHERE genesis_id = ${args.genesis_id} - `; - if (query.count === 0) return; - return query[0]; - } - - private async getLocation(args: { - genesis_id: string; - output: string; - }): Promise { - const query = await this.sql` - SELECT ${this.sql(LOCATIONS_COLUMNS)} - FROM locations - WHERE genesis_id = ${args.genesis_id} AND output = ${args.output} - `; - if (query.count === 0) return; - return query[0]; - } - - private async insertInscriptions(writes: DbRevealInsert[]): Promise { - if (writes.length === 0) return; + private async insertInscriptions(reveals: DbRevealInsert[]): Promise { + if (reveals.length === 0) return; await this.sqlWriteTransaction(async sql => { const inscriptions: DbInscriptionInsert[] = []; const transferGenesisIds = new Set(); - for (const r of writes) + for (const r of reveals) if (r.inscription) inscriptions.push(r.inscription); else transferGenesisIds.add(r.location.genesis_id); if (inscriptions.length) @@ -652,12 +507,12 @@ export class PgStore extends BasePgStore { sat_coinbase_height = EXCLUDED.sat_coinbase_height, updated_at = NOW() `; - const locationData = writes.map(i => ({ + const locationData = reveals.map(i => ({ ...i.location, - inscription_id: sql`(SELECT id FROM inscriptions WHERE genesis_id = ${i.location.genesis_id})`, + inscription_id: sql`(SELECT id FROM inscriptions WHERE genesis_id = ${i.location.genesis_id} LIMIT 1)`, timestamp: sql`TO_TIMESTAMP(${i.location.timestamp})`, })); - const locations = await sql` + const pointers = await sql` INSERT INTO locations ${sql(locationData)} ON CONFLICT ON CONSTRAINT locations_inscription_id_block_height_tx_index_unique DO UPDATE SET genesis_id = EXCLUDED.genesis_id, @@ -670,21 +525,22 @@ export class PgStore extends BasePgStore { timestamp = EXCLUDED.timestamp RETURNING inscription_id, id AS location_id, block_height, tx_index, address `; - await this.updateInscriptionRecursions(writes); + await this.updateInscriptionRecursions(reveals); if (transferGenesisIds.size) await sql` UPDATE inscriptions SET updated_at = NOW() WHERE genesis_id IN ${sql([...transferGenesisIds])} `; - await this.updateInscriptionLocationPointers(locations); - await this.counts.applyInscriptions(inscriptions); - for (const reveal of writes) { + await this.updateInscriptionLocationPointers(pointers); + for (const reveal of reveals) { const action = reveal.inscription ? `reveal #${reveal.inscription.number}` : `transfer`; logger.info( `PgStore ${action} (${reveal.location.genesis_id}) at block ${reveal.location.block_height}` ); } + await this.counts.applyInscriptions(inscriptions); + if (ENV.BRC20_BLOCK_SCAN_ENABLED) await this.brc20.insertOperations({ reveals, pointers }); }); } @@ -728,37 +584,35 @@ export class PgStore extends BasePgStore { }); } - private async rollBackInscription(args: { - genesis_id: string; - number: number; - block_height: number; - }): Promise { - const inscription = await this.getInscription({ genesis_id: args.genesis_id }); - if (!inscription) return; - await this.sqlWriteTransaction(async sql => { - await this.counts.rollBackInscription({ inscription }); - await this.brc20.rollBackInscription({ inscription }); - await sql`DELETE FROM inscriptions WHERE id = ${inscription.id}`; - logger.info( - `PgStore rollback reveal #${args.number} (${args.genesis_id}) at block ${args.block_height}` - ); - }); - } - - private async rollBackLocation(args: { - genesis_id: string; - output: string; - block_height: number; - }): Promise { - const location = await this.getLocation({ genesis_id: args.genesis_id, output: args.output }); - if (!location) return; + private async rollBackInscriptions(rollbacks: DbRevealInsert[]): Promise { + if (rollbacks.length === 0) return; await this.sqlWriteTransaction(async sql => { - await this.brc20.rollBackLocation({ location }); - await this.recalculateCurrentLocationPointerFromLocationRollBack({ location }); - await sql`DELETE FROM locations WHERE id = ${location.id}`; - logger.info( - `PgStore rollback transfer (${args.genesis_id}) on output ${args.output} at block ${args.block_height}` - ); + // Roll back events in reverse so BRC-20 keeps a sane order. + for (const rollback of rollbacks.reverse()) { + if (rollback.inscription) { + await this.brc20.rollBackInscription({ inscription: rollback.inscription }); + await this.counts.rollBackInscription({ + inscription: rollback.inscription, + location: rollback.location, + }); + await sql`DELETE FROM inscriptions WHERE genesis_id = ${rollback.inscription.genesis_id}`; + logger.info( + `PgStore rollback reveal #${rollback.inscription.number} (${rollback.inscription.genesis_id}) at block ${rollback.location.block_height}` + ); + } else { + await this.brc20.rollBackLocation({ location: rollback.location }); + await this.recalculateCurrentLocationPointerFromLocationRollBack({ + location: rollback.location, + }); + await sql` + DELETE FROM locations + WHERE output = ${rollback.location.output} AND "offset" = ${rollback.location.offset} + `; + logger.info( + `PgStore rollback transfer for ${rollback.location.genesis_id} at block ${rollback.location.block_height}` + ); + } + } }); } @@ -853,19 +707,22 @@ export class PgStore extends BasePgStore { } private async recalculateCurrentLocationPointerFromLocationRollBack(args: { - location: DbLocation; + location: DbLocationInsert; }): Promise { await this.sqlWriteTransaction(async sql => { // Is the location we're rolling back *the* current location? const current = await sql` - SELECT * FROM current_locations WHERE location_id = ${args.location.id} + SELECT * + FROM current_locations AS c + INNER JOIN locations AS l ON l.id = c.location_id + WHERE l.output = ${args.location.output} AND l."offset" = ${args.location.offset} `; if (current.count > 0) { const update = await sql` WITH prev AS ( SELECT id, block_height, tx_index, address FROM locations - WHERE inscription_id = ${args.location.inscription_id} AND id <> ${args.location.id} + WHERE inscription_id = ${current[0].inscription_id} AND id <> ${current[0].location_id} ORDER BY block_height DESC, tx_index DESC LIMIT 1 ) @@ -875,7 +732,7 @@ export class PgStore extends BasePgStore { tx_index = prev.tx_index, address = prev.address FROM prev - WHERE c.inscription_id = ${args.location.inscription_id} + WHERE c.inscription_id = ${current[0].inscription_id} RETURNING * `; await this.counts.rollBackCurrentLocation({ curr: current[0], prev: update[0] }); @@ -896,8 +753,9 @@ export class PgStore extends BasePgStore { for (const ref of refSet) inserts.push({ inscription_id: this - .sql`(SELECT id FROM inscriptions WHERE genesis_id = ${i.inscription.genesis_id})`, - ref_inscription_id: this.sql`(SELECT id FROM inscriptions WHERE genesis_id = ${ref})`, + .sql`(SELECT id FROM inscriptions WHERE genesis_id = ${i.inscription.genesis_id} LIMIT 1)`, + ref_inscription_id: this + .sql`(SELECT id FROM inscriptions WHERE genesis_id = ${ref} LIMIT 1)`, ref_inscription_genesis_id: ref, }); } diff --git a/tests/cache.test.ts b/tests/cache.test.ts index 29c890eb..c188683f 100644 --- a/tests/cache.test.ts +++ b/tests/cache.test.ts @@ -41,6 +41,7 @@ describe('ETag cache', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(block); @@ -172,6 +173,7 @@ describe('ETag cache', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(block1); @@ -196,6 +198,7 @@ describe('ETag cache', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(block2); @@ -267,6 +270,7 @@ describe('ETag cache', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(block1); @@ -310,6 +314,7 @@ describe('ETag cache', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(block2); @@ -345,6 +350,7 @@ describe('ETag cache', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(block1); @@ -388,6 +394,7 @@ describe('ETag cache', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(block2); diff --git a/tests/helpers.ts b/tests/helpers.ts index e57966e6..fe26f3e9 100644 --- a/tests/helpers.ts +++ b/tests/helpers.ts @@ -1,6 +1,5 @@ import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; import { - BitcoinCursedInscriptionRevealed, BitcoinEvent, BitcoinInscriptionRevealed, BitcoinInscriptionTransferred, @@ -91,11 +90,6 @@ export class TestChainhookPayloadBuilder { return this; } - cursedInscriptionRevealed(args: BitcoinCursedInscriptionRevealed): this { - this.lastBlockTx.metadata.ordinal_operations.push({ cursed_inscription_revealed: args }); - return this; - } - inscriptionTransferred(args: BitcoinInscriptionTransferred): this { this.lastBlockTx.metadata.ordinal_operations.push({ inscription_transferred: args }); return this; @@ -137,6 +131,7 @@ export function brc20Reveal(args: { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }; return reveal; } diff --git a/tests/inscriptions.test.ts b/tests/inscriptions.test.ts index 4fbf4255..4e0cc04c 100644 --- a/tests/inscriptions.test.ts +++ b/tests/inscriptions.test.ts @@ -53,6 +53,7 @@ describe('/inscriptions', () => { tx_index: 0, inscription_input_index: 0, transfers_pre_inscription: 0, + curse_type: null, }) .build() ); @@ -84,6 +85,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -160,6 +162,7 @@ describe('/inscriptions', () => { tx_index: 0, inscription_input_index: 0, transfers_pre_inscription: 0, + curse_type: null, }) .transaction({ hash: '0xf351d86c6e6cae3c64e297e7463095732f216875bcc1f3c03f950a492bb25421', @@ -181,6 +184,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -218,6 +222,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -297,6 +302,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -356,7 +362,7 @@ describe('/inscriptions', () => { .transaction({ hash: '0x38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .cursedInscriptionRevealed({ + .inscriptionRevealed({ content_bytes: '0x48656C6C6F', content_type: 'image/png', content_length: 5, @@ -450,6 +456,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -594,6 +601,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -681,7 +689,7 @@ describe('/inscriptions', () => { .transaction({ hash: '0x38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .cursedInscriptionRevealed({ + .inscriptionRevealed({ content_bytes: '0x48656C6C6F', content_type: 'image/png', content_length: 5, @@ -845,6 +853,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1008,6 +1017,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .transaction({ hash: '7ac73ecd01b9da4a7eab904655416dbfe8e03f193e091761b5a63ad0963570cd', @@ -1029,6 +1039,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 1, + curse_type: null, }) .build() ); @@ -1358,6 +1369,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1389,6 +1401,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1486,6 +1499,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1517,6 +1531,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1631,6 +1646,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1662,6 +1678,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1722,6 +1739,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1753,6 +1771,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1817,6 +1836,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1848,6 +1868,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1908,6 +1929,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -1939,6 +1961,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2001,6 +2024,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2045,6 +2069,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2076,6 +2101,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2122,6 +2148,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2153,6 +2180,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2207,6 +2235,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2238,6 +2267,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2292,6 +2322,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2323,6 +2354,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2377,6 +2409,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2408,6 +2441,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2461,6 +2495,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2492,6 +2527,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2526,6 +2562,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }; await db.updateInscriptions( new TestChainhookPayloadBuilder() @@ -2569,6 +2606,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2727,6 +2765,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2760,6 +2799,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2818,6 +2858,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2832,7 +2873,7 @@ describe('/inscriptions', () => { .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) - .cursedInscriptionRevealed({ + .inscriptionRevealed({ content_bytes: `0x${Buffer.from( 'Hello /content/9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0' ).toString('hex')}`, @@ -2910,6 +2951,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -2940,6 +2982,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(genesis2); @@ -3008,6 +3051,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3039,6 +3083,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3070,6 +3115,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3126,6 +3172,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3157,6 +3204,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3188,6 +3236,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3244,6 +3293,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3275,6 +3325,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3306,6 +3357,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3362,6 +3414,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3393,6 +3446,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -3424,6 +3478,7 @@ describe('/inscriptions', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); diff --git a/tests/sats.test.ts b/tests/sats.test.ts index 723d23c7..4d422c76 100644 --- a/tests/sats.test.ts +++ b/tests/sats.test.ts @@ -62,6 +62,7 @@ describe('/sats', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -81,7 +82,7 @@ describe('/sats', () => { .apply() .block({ height: 775617 }) .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc' }) - .cursedInscriptionRevealed({ + .inscriptionRevealed({ content_bytes: '0x48656C6C6F', content_type: 'image/png', content_length: 5, @@ -113,7 +114,7 @@ describe('/sats', () => { .transaction({ hash: 'b9cd9489fe30b81d007f753663d12766f1368721a87f4c69056c8215caa57993', }) - .cursedInscriptionRevealed({ + .inscriptionRevealed({ content_bytes: '0x48656C6C6F', content_type: 'image/png', content_length: 5, diff --git a/tests/server.test.ts b/tests/server.test.ts index b375d741..9ac5dc6d 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -1,4 +1,4 @@ -import { PREDICATE_UUID, startChainhookServer } from '../src/chainhook/server'; +import { PREDICATE_UUID, startOrdhookServer } from '../src/ordhook/server'; import { ENV } from '../src/env'; import { MIGRATIONS_DIR, PgStore } from '../src/pg/pg-store'; import { TestChainhookPayloadBuilder, TestFastifyServer } from './helpers'; @@ -20,7 +20,7 @@ describe('EventServer', () => { await runMigrations(MIGRATIONS_DIR, 'up'); ENV.CHAINHOOK_AUTO_PREDICATE_REGISTRATION = false; db = await PgStore.connect({ skipMigrations: true }); - server = await startChainhookServer({ db }); + server = await startOrdhookServer({ db }); fastify = await buildApiServer({ db }); }); @@ -50,6 +50,7 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }; // Apply @@ -67,7 +68,7 @@ describe('EventServer', () => { .build(); const response = await server['fastify'].inject({ method: 'POST', - url: `/chainhook/${PREDICATE_UUID}`, + url: `/payload`, headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: payload1, }); @@ -127,7 +128,7 @@ describe('EventServer', () => { .build(); const response2 = await server['fastify'].inject({ method: 'POST', - url: `/chainhook/${PREDICATE_UUID}`, + url: `/payload`, headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: payload2, }); @@ -167,9 +168,11 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); + await expect(db.getChainTipBlockHeight()).resolves.toBe(775617); const transfer: BitcoinInscriptionTransferred = { inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', @@ -200,12 +203,12 @@ describe('EventServer', () => { .build(); const response = await server['fastify'].inject({ method: 'POST', - url: `/chainhook/${PREDICATE_UUID}`, + url: `/payload`, headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: payload1, }); expect(response.statusCode).toBe(200); - + await expect(db.getChainTipBlockHeight()).resolves.toBe(775618); const query = await db.getInscriptions( { limit: 1, @@ -260,7 +263,7 @@ describe('EventServer', () => { .build(); const response2 = await server['fastify'].inject({ method: 'POST', - url: `/chainhook/${PREDICATE_UUID}`, + url: `/payload`, headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: payload2, }); @@ -269,6 +272,7 @@ describe('EventServer', () => { expect(c1[0].count).toBe(1); const c2 = await db.sql<{ count: number }[]>`SELECT COUNT(*)::int FROM locations`; expect(c2[0].count).toBe(1); + await expect(db.getChainTipBlockHeight()).resolves.toBe(775617); }); test('multiple inscription pointers on the same block are compared correctly', async () => { @@ -301,6 +305,7 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 995, + curse_type: null, }) .transaction({ hash: '7edaa48337a94da327b6262830505f116775a32db5ad4ad46e87ecea33f21bac', @@ -357,6 +362,7 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -387,12 +393,13 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await expect(db.updateInscriptions(errorPayload)).rejects.toThrow(BadPayloadRequestError); const response = await server['fastify'].inject({ method: 'POST', - url: `/chainhook/${PREDICATE_UUID}`, + url: `/payload`, headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: errorPayload, }); @@ -428,6 +435,7 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -458,6 +466,7 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .transaction({ hash: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5', @@ -479,12 +488,13 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await expect(db.updateInscriptions(errorPayload)).rejects.toThrow(BadPayloadRequestError); const response = await server['fastify'].inject({ method: 'POST', - url: `/chainhook/${PREDICATE_UUID}`, + url: `/payload`, headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: errorPayload, }); @@ -520,6 +530,7 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -550,6 +561,7 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .transaction({ hash: '6891d374a17ba85f6b5514f2f7edc301c1c860284dff5a5c6e88ab3a20fcd8a5', @@ -571,6 +583,7 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await expect(db.updateInscriptions(unboundPayload)).resolves.not.toThrow( @@ -606,13 +619,14 @@ describe('EventServer', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build(); await db.updateInscriptions(payload); const response = await server['fastify'].inject({ method: 'POST', - url: `/chainhook/${PREDICATE_UUID}`, + url: `/payload`, headers: { authorization: `Bearer ${ENV.CHAINHOOK_NODE_AUTH_TOKEN}` }, payload: payload, }); diff --git a/tests/stats.test.ts b/tests/stats.test.ts index bc7119b1..f4850c17 100644 --- a/tests/stats.test.ts +++ b/tests/stats.test.ts @@ -223,6 +223,7 @@ function testRevealApply(blockHeight: number, numbers: number[]) { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }); } return block.build(); diff --git a/tests/status.test.ts b/tests/status.test.ts index 50596a48..18d5fcb0 100644 --- a/tests/status.test.ts +++ b/tests/status.test.ts @@ -55,6 +55,7 @@ describe('Status', () => { inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, + curse_type: null, }) .build() ); @@ -63,7 +64,7 @@ describe('Status', () => { .apply() .block({ height: 791975 }) .transaction({ hash: 'a98d7055a77fa0b96cc31e30bb8bacf777382d1b67f1b7eca6f2014e961591c8' }) - .cursedInscriptionRevealed({ + .inscriptionRevealed({ content_bytes: '0x48656C6C6F', content_type: 'text/plain;charset=utf-8', content_length: 5,