From 31498bdb57203bd6c28eccac4446a9d169a3fe18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20C=C3=A1rdenas?= Date: Tue, 1 Aug 2023 15:16:38 -0600 Subject: [PATCH] fix: optimize COUNT calculations via the use of count tables (#175) * feat: first iteration * fix: rollbacks * fix: block height ranges * fix: block hash count * fix: add custom count support * feat: add cursed filter * feat: genesis address filter --- .../1690476164909_count-views-to-tables.ts | 149 +++++++++ src/api/routes/inscriptions.ts | 5 + src/api/schemas.ts | 6 + src/pg/counts/counts-pg-store.ts | 236 +++++++++++++ src/pg/counts/helpers.ts | 41 +++ src/pg/counts/types.ts | 25 ++ src/pg/helpers.ts | 32 -- src/pg/pg-store.ts | 226 +++++++------ src/pg/types.ts | 21 +- tests/inscriptions.test.ts | 315 ++++++++++++------ 10 files changed, 805 insertions(+), 251 deletions(-) create mode 100644 migrations/1690476164909_count-views-to-tables.ts create mode 100644 src/pg/counts/counts-pg-store.ts create mode 100644 src/pg/counts/helpers.ts create mode 100644 src/pg/counts/types.ts diff --git a/migrations/1690476164909_count-views-to-tables.ts b/migrations/1690476164909_count-views-to-tables.ts new file mode 100644 index 00000000..b6a167ca --- /dev/null +++ b/migrations/1690476164909_count-views-to-tables.ts @@ -0,0 +1,149 @@ +/* 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('mime_type_counts'); + pgm.createTable('counts_by_mime_type', { + mime_type: { + type: 'text', + notNull: true, + primaryKey: true, + }, + count: { + type: 'bigint', + notNull: true, + default: 1, + }, + }); + pgm.sql(` + INSERT INTO counts_by_mime_type ( + SELECT mime_type, COUNT(*) AS count FROM inscriptions GROUP BY mime_type + ) + `); + + pgm.dropMaterializedView('sat_rarity_counts'); + pgm.createTable('counts_by_sat_rarity', { + sat_rarity: { + type: 'text', + notNull: true, + primaryKey: true, + }, + count: { + type: 'bigint', + notNull: true, + default: 1, + }, + }); + pgm.sql(` + INSERT INTO counts_by_sat_rarity ( + SELECT sat_rarity, COUNT(*) AS count FROM inscriptions GROUP BY sat_rarity + ) + `); + + pgm.dropMaterializedView('address_counts'); + pgm.createTable('counts_by_address', { + address: { + type: 'text', + notNull: true, + primaryKey: true, + }, + count: { + type: 'bigint', + notNull: true, + default: 1, + }, + }); + pgm.sql(` + INSERT INTO counts_by_address ( + SELECT address, COUNT(*) AS count FROM current_locations GROUP BY address + ) + `); + + pgm.createTable('counts_by_genesis_address', { + address: { + type: 'text', + notNull: true, + primaryKey: true, + }, + count: { + type: 'bigint', + notNull: true, + default: 1, + }, + }); + pgm.sql(` + INSERT INTO counts_by_genesis_address ( + SELECT address, COUNT(*) AS count FROM genesis_locations GROUP BY address + ) + `); + + pgm.dropMaterializedView('inscription_count'); + pgm.createTable('counts_by_type', { + type: { + type: 'text', + notNull: true, + primaryKey: true, + }, + count: { + type: 'bigint', + notNull: true, + default: 1, + }, + }); + pgm.sql(` + INSERT INTO counts_by_type ( + SELECT 'blessed' AS type, COUNT(*) AS count FROM inscriptions WHERE number >= 0 + ) + `); + pgm.sql(` + INSERT INTO counts_by_type ( + SELECT 'cursed' AS type, COUNT(*) AS count FROM inscriptions WHERE number < 0 + ) + `); + + pgm.createIndex('inscriptions_per_block', ['block_hash']); +} + +export function down(pgm: MigrationBuilder): void { + pgm.dropTable('counts_by_mime_type'); + pgm.createMaterializedView( + 'mime_type_counts', + { data: true }, + `SELECT mime_type, COUNT(*) AS count FROM inscriptions GROUP BY mime_type` + ); + pgm.createIndex('mime_type_counts', ['mime_type'], { unique: true }); + + pgm.dropTable('counts_by_sat_rarity'); + pgm.createMaterializedView( + 'sat_rarity_counts', + { data: true }, + ` + SELECT sat_rarity, COUNT(*) AS count + FROM inscriptions AS i + GROUP BY sat_rarity + ` + ); + pgm.createIndex('sat_rarity_counts', ['sat_rarity'], { unique: true }); + + pgm.dropTable('counts_by_address'); + pgm.createMaterializedView( + 'address_counts', + { data: true }, + `SELECT address, COUNT(*) AS count FROM current_locations GROUP BY address` + ); + pgm.createIndex('address_counts', ['address'], { unique: true }); + + pgm.dropTable('counts_by_type'); + pgm.createMaterializedView( + 'inscription_count', + { data: true }, + `SELECT COUNT(*) AS count FROM inscriptions` + ); + pgm.createIndex('inscription_count', ['count'], { unique: true }); + + pgm.dropIndex('inscriptions_per_block', ['block_hash']); + + pgm.dropTable('counts_by_genesis_address'); +} diff --git a/src/api/routes/inscriptions.ts b/src/api/routes/inscriptions.ts index 9a5c0ec6..2b868003 100644 --- a/src/api/routes/inscriptions.ts +++ b/src/api/routes/inscriptions.ts @@ -8,6 +8,7 @@ import { BlockHeightParam, BlockInscriptionTransferSchema, BlockParam, + CursedParam, InscriptionIdParamCType, InscriptionIdentifierParam, InscriptionIdsParam, @@ -83,9 +84,11 @@ const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTy number: Type.Optional(InscriptionNumbersParam), output: Type.Optional(OutputParam), address: Type.Optional(AddressesParam), + genesis_address: Type.Optional(AddressesParam), mime_type: Type.Optional(MimeTypesParam), rarity: Type.Optional(SatoshiRaritiesParam), recursive: Type.Optional(RecursiveParam), + cursed: Type.Optional(CursedParam), // Pagination offset: Type.Optional(OffsetParam), limit: Type.Optional(LimitParam), @@ -120,9 +123,11 @@ const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTy number: request.query.number, output: request.query.output, address: request.query.address, + genesis_address: request.query.genesis_address, mime_type: request.query.mime_type, sat_rarity: request.query.rarity, recursive: request.query.recursive, + cursed: request.query.cursed, }, { order_by: request.query.order_by ?? OrderBy.genesis_block_height, diff --git a/src/api/schemas.ts b/src/api/schemas.ts index 1e057e3a..ba63371f 100644 --- a/src/api/schemas.ts +++ b/src/api/schemas.ts @@ -171,6 +171,12 @@ export const RecursiveParam = Type.Boolean({ examples: [false], }); +export const CursedParam = Type.Boolean({ + title: 'Cursed', + description: 'Whether or not the inscription is cursed', + examples: [false], +}); + export const OffsetParam = Type.Integer({ minimum: 0, title: 'Offset', diff --git a/src/pg/counts/counts-pg-store.ts b/src/pg/counts/counts-pg-store.ts new file mode 100644 index 00000000..1d4397bc --- /dev/null +++ b/src/pg/counts/counts-pg-store.ts @@ -0,0 +1,236 @@ +import { PgSqlClient } from '@hirosystems/api-toolkit'; +import { PgStore } from '../pg-store'; +import { SatoshiRarity } from '../../api/util/ordinal-satoshi'; +import { + DbInscription, + DbInscriptionIndexFilters, + DbInscriptionInsert, + DbInscriptionType, + DbLocationPointer, +} from '../types'; +import { DbInscriptionIndexResultCountType } from './types'; + +/** + * This class affects all the different tables that track inscription counts according to different + * parameters (sat rarity, mime type, cursed, blessed, current owner, etc.) + */ +export class CountsPgStore { + // TODO: Move this to the api-toolkit so we can have pg submodules. + private readonly parent: PgStore; + private get sql(): PgSqlClient { + return this.parent.sql; + } + + constructor(db: PgStore) { + this.parent = db; + } + + async fromResults( + countType: DbInscriptionIndexResultCountType, + filters?: DbInscriptionIndexFilters + ): Promise { + switch (countType) { + case DbInscriptionIndexResultCountType.all: + return await this.getInscriptionCount(); + case DbInscriptionIndexResultCountType.cursed: + return await this.getInscriptionCount( + filters?.cursed === true ? DbInscriptionType.cursed : DbInscriptionType.blessed + ); + case DbInscriptionIndexResultCountType.mimeType: + return await this.getMimeTypeCount(filters?.mime_type); + case DbInscriptionIndexResultCountType.satRarity: + return await this.getSatRarityCount(filters?.sat_rarity); + case DbInscriptionIndexResultCountType.address: + return await this.getAddressCount(filters?.address); + case DbInscriptionIndexResultCountType.genesisAddress: + return await this.getGenesisAddressCount(filters?.genesis_address); + case DbInscriptionIndexResultCountType.blockHeight: + return await this.getBlockCount( + filters?.genesis_block_height, + filters?.genesis_block_height + ); + case DbInscriptionIndexResultCountType.fromblockHeight: + return await this.getBlockCount(filters?.from_genesis_block_height); + case DbInscriptionIndexResultCountType.toblockHeight: + return await this.getBlockCount(undefined, filters?.to_genesis_block_height); + case DbInscriptionIndexResultCountType.blockHeightRange: + return await this.getBlockCount( + filters?.from_genesis_block_height, + filters?.to_genesis_block_height + ); + case DbInscriptionIndexResultCountType.blockHash: + return await this.getBlockHashCount(filters?.genesis_block_hash); + } + } + + async applyInscription(args: { inscription: DbInscriptionInsert }): Promise { + await this.parent.sqlWriteTransaction(async sql => { + await sql` + INSERT INTO counts_by_mime_type ${sql({ mime_type: args.inscription.mime_type })} + ON CONFLICT (mime_type) DO UPDATE SET count = counts_by_mime_type.count + 1 + `; + await sql` + INSERT INTO counts_by_sat_rarity ${sql({ sat_rarity: args.inscription.sat_rarity })} + ON CONFLICT (sat_rarity) DO UPDATE SET count = counts_by_sat_rarity.count + 1 + `; + await sql` + INSERT INTO counts_by_type ${sql({ + type: args.inscription.number < 0 ? DbInscriptionType.cursed : DbInscriptionType.blessed, + })} + ON CONFLICT (type) DO UPDATE SET count = counts_by_type.count + 1 + `; + }); + } + + async rollBackInscription(args: { inscription: DbInscription }): Promise { + await this.parent.sqlWriteTransaction(async sql => { + await sql` + UPDATE counts_by_mime_type SET count = count - 1 WHERE mime_type = ${args.inscription.mime_type} + `; + await sql` + UPDATE counts_by_sat_rarity SET count = count - 1 WHERE sat_rarity = ${args.inscription.sat_rarity} + `; + await sql` + UPDATE counts_by_type SET count = count - 1 WHERE type = ${ + parseInt(args.inscription.number) < 0 + ? DbInscriptionType.cursed + : DbInscriptionType.blessed + } + `; + await sql` + UPDATE counts_by_address SET count = count - 1 WHERE address = ( + SELECT address FROM current_locations WHERE inscription_id = ${args.inscription.id} + ) + `; + }); + } + + async applyGenesisLocation(args: { + old?: DbLocationPointer; + new: DbLocationPointer; + }): Promise { + await this.parent.sqlWriteTransaction(async sql => { + if (args.old && args.old.address) { + await sql` + UPDATE counts_by_genesis_address SET count = count - 1 WHERE address = ${args.old.address} + `; + } + if (args.new.address) { + await sql` + INSERT INTO counts_by_genesis_address ${sql({ address: args.new.address })} + ON CONFLICT (address) DO UPDATE SET count = counts_by_genesis_address.count + 1 + `; + } + }); + } + + async applyCurrentLocation(args: { + old?: DbLocationPointer; + new: DbLocationPointer; + }): Promise { + await this.parent.sqlWriteTransaction(async sql => { + if (args.old && args.old.address) { + await sql` + UPDATE counts_by_address SET count = count - 1 WHERE address = ${args.old.address} + `; + } + if (args.new.address) { + await sql` + INSERT INTO counts_by_address ${sql({ address: args.new.address })} + ON CONFLICT (address) DO UPDATE SET count = counts_by_address.count + 1 + `; + } + }); + } + + async rollBackCurrentLocation(args: { + curr: DbLocationPointer; + prev: DbLocationPointer; + }): Promise { + await this.parent.sqlWriteTransaction(async sql => { + if (args.curr.address) { + await sql` + UPDATE counts_by_address SET count = count - 1 WHERE address = ${args.curr.address} + `; + } + if (args.prev.address) { + await sql` + UPDATE counts_by_address SET count = count + 1 WHERE address = ${args.prev.address} + `; + } + }); + } + + private async getBlockCount(from?: number, to?: number): Promise { + if (from === undefined && to === undefined) return 0; + const result = await this.sql<{ count: number }[]>` + SELECT COALESCE(SUM(inscription_count), 0) AS count + FROM inscriptions_per_block + WHERE TRUE + ${from !== undefined ? this.sql`AND block_height >= ${from}` : this.sql``} + ${to !== undefined ? this.sql`AND block_height <= ${to}` : this.sql``} + `; + return result[0].count; + } + + private async getBlockHashCount(hash?: string): Promise { + if (!hash) return 0; + const result = await this.sql<{ count: number }[]>` + SELECT COALESCE(SUM(inscription_count), 0) AS count + FROM inscriptions_per_block + WHERE block_hash = ${hash} + `; + return result[0].count; + } + + private async getInscriptionCount(type?: DbInscriptionType): Promise { + const types = + type !== undefined ? [type] : [DbInscriptionType.blessed, DbInscriptionType.cursed]; + const result = await this.sql<{ count: number }[]>` + SELECT COALESCE(SUM(count), 0) AS count + FROM counts_by_type + WHERE type IN ${this.sql(types)} + `; + return result[0].count; + } + + private async getMimeTypeCount(mimeType?: string[]): Promise { + if (!mimeType) return 0; + const result = await this.sql<{ count: number }[]>` + SELECT COALESCE(SUM(count), 0) AS count + FROM counts_by_mime_type + WHERE mime_type IN ${this.sql(mimeType)} + `; + return result[0].count; + } + + private async getSatRarityCount(satRarity?: SatoshiRarity[]): Promise { + if (!satRarity) return 0; + const result = await this.sql<{ count: number }[]>` + SELECT COALESCE(SUM(count), 0) AS count + FROM counts_by_sat_rarity + WHERE sat_rarity IN ${this.sql(satRarity)} + `; + return result[0].count; + } + + private async getAddressCount(address?: string[]): Promise { + if (!address) return 0; + const result = await this.sql<{ count: number }[]>` + SELECT COALESCE(SUM(count), 0) AS count + FROM counts_by_address + WHERE address IN ${this.sql(address)} + `; + return result[0].count; + } + + private async getGenesisAddressCount(genesisAddress?: string[]): Promise { + if (!genesisAddress) return 0; + const result = await this.sql<{ count: number }[]>` + SELECT COALESCE(SUM(count), 0) AS count + FROM counts_by_genesis_address + WHERE address IN ${this.sql(genesisAddress)} + `; + return result[0].count; + } +} diff --git a/src/pg/counts/helpers.ts b/src/pg/counts/helpers.ts new file mode 100644 index 00000000..be56aa2b --- /dev/null +++ b/src/pg/counts/helpers.ts @@ -0,0 +1,41 @@ +import { DbInscriptionIndexFilters } from '../types'; +import { DbInscriptionIndexResultCountType } from './types'; + +/** + * Returns which inscription count is required based on filters sent to the index endpoint. + * @param filters - DbInscriptionIndexFilters + * @returns DbInscriptionIndexResultCountType + */ +export function getIndexResultCountType( + filters?: DbInscriptionIndexFilters +): DbInscriptionIndexResultCountType { + if (!filters) return DbInscriptionIndexResultCountType.all; + // Remove undefined values. + Object.keys(filters).forEach( + key => + filters[key as keyof DbInscriptionIndexFilters] === undefined && + delete filters[key as keyof DbInscriptionIndexFilters] + ); + // How many filters do we have? + switch (Object.keys(filters).length) { + case 0: + return DbInscriptionIndexResultCountType.all; + case 1: + if (filters.mime_type) return DbInscriptionIndexResultCountType.mimeType; + if (filters.sat_rarity) return DbInscriptionIndexResultCountType.satRarity; + if (filters.address) return DbInscriptionIndexResultCountType.address; + if (filters.genesis_address) return DbInscriptionIndexResultCountType.genesisAddress; + if (filters.genesis_block_height) return DbInscriptionIndexResultCountType.blockHeight; + if (filters.from_genesis_block_height) + return DbInscriptionIndexResultCountType.fromblockHeight; + if (filters.to_genesis_block_height) return DbInscriptionIndexResultCountType.toblockHeight; + if (filters.genesis_block_hash) return DbInscriptionIndexResultCountType.blockHash; + if (filters.cursed !== undefined) return DbInscriptionIndexResultCountType.cursed; + if (filters.number || filters.genesis_id || filters.output || filters.sat_ordinal) + return DbInscriptionIndexResultCountType.singleResult; + case 2: + if (filters.from_genesis_block_height && filters.to_genesis_block_height) + return DbInscriptionIndexResultCountType.blockHeightRange; + } + return DbInscriptionIndexResultCountType.custom; +} diff --git a/src/pg/counts/types.ts b/src/pg/counts/types.ts new file mode 100644 index 00000000..a3ba8a0d --- /dev/null +++ b/src/pg/counts/types.ts @@ -0,0 +1,25 @@ +/** Type of row count required for an inscription index endpoint call */ +export enum DbInscriptionIndexResultCountType { + /** All inscriptions */ + all, + /** Filtered by cursed or blessed */ + cursed, + /** Filtered by mime type */ + mimeType, + /** Filtered by sat rarity */ + satRarity, + /** Filtered by address */ + address, + genesisAddress, + /** Filtered by block height */ + blockHeight, + fromblockHeight, + toblockHeight, + blockHeightRange, + /** Filtered by block hash */ + blockHash, + /** Filtered by some other param that yields a single result (easy to count) */ + singleResult, + /** Filtered by custom arguments (tough to count) */ + custom, +} diff --git a/src/pg/helpers.ts b/src/pg/helpers.ts index a8fcfb42..b0d967c2 100644 --- a/src/pg/helpers.ts +++ b/src/pg/helpers.ts @@ -1,38 +1,6 @@ import { PgBytea } from '@hirosystems/api-toolkit'; -import { DbInscriptionIndexFilters, DbInscriptionIndexResultCountType } from './types'; import { hexToBuffer } from '../api/util/helpers'; -/** - * Returns which inscription count is required based on filters sent to the index endpoint. - * @param filters - DbInscriptionIndexFilters - * @returns DbInscriptionIndexResultCountType - */ -export function getIndexResultCountType( - filters?: DbInscriptionIndexFilters -): DbInscriptionIndexResultCountType { - if (!filters) return DbInscriptionIndexResultCountType.all; - // Remove undefined values. - Object.keys(filters).forEach( - key => - filters[key as keyof DbInscriptionIndexFilters] === undefined && - delete filters[key as keyof DbInscriptionIndexFilters] - ); - // How many filters do we have? - switch (Object.keys(filters).length) { - case 0: - return DbInscriptionIndexResultCountType.all; - case 1: - if (filters.mime_type) return DbInscriptionIndexResultCountType.mimeType; - if (filters.sat_rarity) return DbInscriptionIndexResultCountType.satRarity; - if (filters.address) return DbInscriptionIndexResultCountType.address; - if (filters.number || filters.genesis_id || filters.output || filters.sat_ordinal) - return DbInscriptionIndexResultCountType.singleResult; - return DbInscriptionIndexResultCountType.intractable; - default: - return DbInscriptionIndexResultCountType.intractable; - } -} - /** * Returns a list of referenced inscription ids from inscription content. * @param content - Inscription content diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index cfcd5254..a804ad23 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -1,18 +1,18 @@ import { BitcoinEvent, Payload } from '@hirosystems/chainhook-client'; import { Order, OrderBy } from '../api/schemas'; import { isProdEnv, isTestEnv, normalizedHexString, parseSatPoint } from '../api/util/helpers'; -import { OrdinalSatoshi, SatoshiRarity } from '../api/util/ordinal-satoshi'; +import { OrdinalSatoshi } from '../api/util/ordinal-satoshi'; import { ENV } from '../env'; -import { getIndexResultCountType, getInscriptionRecursion } from './helpers'; +import { getInscriptionRecursion } from './helpers'; import { DbFullyLocatedInscriptionResult, + DbInscription, DbInscriptionContent, DbInscriptionCountPerBlock, DbInscriptionCountPerBlockFilters, DbInscriptionIndexFilters, DbInscriptionIndexOrder, DbInscriptionIndexPaging, - DbInscriptionIndexResultCountType, DbInscriptionInsert, DbInscriptionLocationChange, DbLocation, @@ -20,16 +20,28 @@ import { DbLocationPointer, DbLocationPointerInsert, DbPaginatedResult, + INSCRIPTIONS_COLUMNS, LOCATIONS_COLUMNS, } from './types'; -import { BasePgStore, connectPostgres, logger, runMigrations } from '@hirosystems/api-toolkit'; +import { + BasePgStore, + PgSqlClient, + connectPostgres, + logger, + runMigrations, +} from '@hirosystems/api-toolkit'; import * as path from 'path'; +import { CountsPgStore } from './counts/counts-pg-store'; +import { getIndexResultCountType } from './counts/helpers'; +import * as postgres from 'postgres'; export const MIGRATIONS_DIR = path.join(__dirname, '../../migrations'); type InscriptionIdentifier = { genesis_id: string } | { number: number }; export class PgStore extends BasePgStore { + readonly counts: CountsPgStore; + static async connect(opts?: { skipMigrations: boolean }): Promise { const pgConfig = { host: ENV.PGHOST, @@ -53,6 +65,11 @@ export class PgStore extends BasePgStore { return new PgStore(sql); } + constructor(sql: PgSqlClient) { + super(sql); + this.counts = new CountsPgStore(this); + } + /** * Inserts inscription genesis and transfers from Chainhook events. Also handles rollbacks from * chain re-orgs and materialized view refreshes. @@ -198,10 +215,6 @@ export class PgStore extends BasePgStore { // we can respond to the chainhook node with a `200` HTTP code as soon as possible. const viewRefresh = Promise.allSettled([ this.normalizeInscriptionCount({ min_block_height: updatedBlockHeightMin }), - this.refreshMaterializedView('inscription_count'), - this.refreshMaterializedView('address_counts'), - this.refreshMaterializedView('mime_type_counts'), - this.refreshMaterializedView('sat_rarity_counts'), ]); // Only wait for these on tests. if (isTestEnv) await viewRefresh; @@ -213,43 +226,6 @@ export class PgStore extends BasePgStore { return parseInt(result[0].block_height); } - async getChainTipInscriptionCount(): Promise { - const result = await this.sql<{ count: number }[]>` - SELECT count FROM inscription_count - `; - return result[0].count; - } - - async getMimeTypeInscriptionCount(mimeType?: string[]): Promise { - if (!mimeType) return 0; - const result = await this.sql<{ count: number }[]>` - SELECT COALESCE(SUM(count), 0) AS count - FROM mime_type_counts - WHERE mime_type IN ${this.sql(mimeType)} - `; - return result[0].count; - } - - async getSatRarityInscriptionCount(satRarity?: SatoshiRarity[]): Promise { - if (!satRarity) return 0; - const result = await this.sql<{ count: number }[]>` - SELECT COALESCE(SUM(count), 0) AS count - FROM sat_rarity_counts - WHERE sat_rarity IN ${this.sql(satRarity)} - `; - return result[0].count; - } - - async getAddressInscriptionCount(address?: string[]): Promise { - if (!address) return 0; - const result = await this.sql<{ count: number }[]>` - SELECT COALESCE(SUM(count), 0) AS count - FROM address_counts - WHERE address IN ${this.sql(address)} - `; - return result[0].count; - } - async getMaxInscriptionNumber(): Promise { const result = await this.sql<{ max: string }[]>` SELECT MAX(number) FROM inscriptions WHERE number >= 0 @@ -323,9 +299,6 @@ export class PgStore extends BasePgStore { sort?: DbInscriptionIndexOrder ): Promise> { return await this.sqlTransaction(async sql => { - // Do we need a filtered `COUNT(*)`? If so, try to use the pre-calculated counts we have in - // materialized views to speed up these queries. - const countType = getIndexResultCountType(filters); // `ORDER BY` statement let orderBy = sql`i.number`; switch (sort?.order_by) { @@ -341,7 +314,11 @@ export class PgStore extends BasePgStore { } // `ORDER` statement const order = sort?.order === Order.asc ? sql`ASC` : sql`DESC`; - const results = await sql<({ total: number } & DbFullyLocatedInscriptionResult)[]>` + // This function will generate a query to be used for getting results or total counts. + const query = ( + columns: postgres.PendingQuery, + sorting: postgres.PendingQuery + ) => sql` WITH gen_locations AS ( SELECT l.* FROM locations AS l INNER JOIN genesis_locations AS g ON l.id = g.location_id @@ -350,40 +327,7 @@ export class PgStore extends BasePgStore { SELECT l.* FROM locations AS l INNER JOIN current_locations AS c ON l.id = c.location_id ) - SELECT - i.genesis_id, - i.number, - i.mime_type, - i.content_type, - i.content_length, - i.fee AS genesis_fee, - i.curse_type, - i.sat_ordinal, - i.sat_rarity, - i.sat_coinbase_height, - i.recursive, - ( - SELECT STRING_AGG(ii.genesis_id, ',') - FROM inscription_recursions AS ir - INNER JOIN inscriptions AS ii ON ii.id = ir.ref_inscription_id - WHERE ir.inscription_id = i.id - ) AS recursion_refs, - gen.block_height AS genesis_block_height, - gen.block_hash AS genesis_block_hash, - gen.tx_id AS genesis_tx_id, - gen.timestamp AS genesis_timestamp, - gen.address AS genesis_address, - loc.tx_id, - loc.address, - loc.output, - loc.offset, - loc.timestamp, - loc.value, - ${ - countType === DbInscriptionIndexResultCountType.singleResult - ? sql`COUNT(*) OVER() as total` - : sql`0 as total` - } + SELECT ${columns} FROM inscriptions AS i INNER JOIN cur_locations AS loc ON loc.inscription_id = i.id INNER JOIN gen_locations AS gen ON gen.inscription_id = i.id @@ -452,24 +396,56 @@ export class PgStore extends BasePgStore { } ${filters?.sat_ordinal ? sql`AND i.sat_ordinal = ${filters.sat_ordinal}` : sql``} ${filters?.recursive !== undefined ? sql`AND i.recursive = ${filters.recursive}` : sql``} - ORDER BY ${orderBy} ${order} - LIMIT ${page.limit} - OFFSET ${page.offset} + ${filters?.cursed === true ? sql`AND i.number < 0` : sql``} + ${filters?.cursed === false ? sql`AND i.number >= 0` : sql``} + ${ + filters?.genesis_address?.length + ? sql`AND gen.address IN ${sql(filters.genesis_address)}` + : sql`` + } + ${sorting} `; - let total = results[0]?.total ?? 0; - switch (countType) { - case DbInscriptionIndexResultCountType.all: - total = await this.getChainTipInscriptionCount(); - break; - case DbInscriptionIndexResultCountType.mimeType: - total = await this.getMimeTypeInscriptionCount(filters?.mime_type); - break; - case DbInscriptionIndexResultCountType.satRarity: - total = await this.getSatRarityInscriptionCount(filters?.sat_rarity); - break; - case DbInscriptionIndexResultCountType.address: - total = await this.getAddressInscriptionCount(filters?.address); - break; + const results = await sql`${query( + sql` + i.genesis_id, + i.number, + i.mime_type, + i.content_type, + i.content_length, + i.fee AS genesis_fee, + i.curse_type, + i.sat_ordinal, + i.sat_rarity, + i.sat_coinbase_height, + i.recursive, + ( + SELECT STRING_AGG(ii.genesis_id, ',') + FROM inscription_recursions AS ir + INNER JOIN inscriptions AS ii ON ii.id = ir.ref_inscription_id + WHERE ir.inscription_id = i.id + ) AS recursion_refs, + gen.block_height AS genesis_block_height, + gen.block_hash AS genesis_block_hash, + gen.tx_id AS genesis_tx_id, + gen.timestamp AS genesis_timestamp, + gen.address AS genesis_address, + loc.tx_id, + loc.address, + loc.output, + loc.offset, + loc.timestamp, + loc.value + `, + sql`ORDER BY ${orderBy} ${order} LIMIT ${page.limit} OFFSET ${page.offset}` + )}`; + // Do we need a filtered `COUNT(*)`? If so, try to use the pre-calculated counts we have in + // cached tables to speed up these queries. + const countType = getIndexResultCountType(filters); + let total = await this.counts.fromResults(countType, filters); + if (total === undefined) { + // If the count is more complex, attempt it with a separate query. + const count = await sql<{ total: number }[]>`${query(sql`COUNT(*) AS total`, sql``)}`; + total = count[0].total; } return { total, @@ -583,6 +559,16 @@ export class PgStore extends BasePgStore { } ${this.sql(viewName)}`; } + 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; @@ -654,6 +640,7 @@ export class PgStore extends BasePgStore { address: args.location.address, }); await this.updateInscriptionRecursion({ inscription_id, ref_genesis_ids: recursion }); + await this.counts.applyInscription({ inscription: args.inscription }); logger.info( `PgStore${upsert.count > 0 ? ' upsert ' : ' '}reveal #${args.inscription.number} (${ args.location.genesis_id @@ -784,11 +771,15 @@ export class PgStore extends BasePgStore { number: number; block_height: number; }): Promise { - // This will cascade into dependent tables. - await this.sql`DELETE FROM inscriptions WHERE genesis_id = ${args.genesis_id}`; - logger.info( - `PgStore rollback reveal #${args.number} (${args.genesis_id}) at block ${args.block_height}` - ); + const inscription = await this.getInscription({ genesis_id: args.genesis_id }); + if (!inscription) return; + await this.sqlWriteTransaction(async sql => { + await this.counts.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: { @@ -819,7 +810,11 @@ export class PgStore extends BasePgStore { tx_index: args.tx_index, address: args.address, }; - await sql` + + const genesis = await sql` + SELECT * FROM genesis_locations WHERE inscription_id = ${args.inscription_id} + `; + const genesisRes = await sql` INSERT INTO genesis_locations ${sql(pointer)} ON CONFLICT (inscription_id) DO UPDATE SET location_id = EXCLUDED.location_id, @@ -831,7 +826,15 @@ export class PgStore extends BasePgStore { (EXCLUDED.block_height = genesis_locations.block_height AND EXCLUDED.tx_index < genesis_locations.tx_index) `; - await sql` + // Affect genesis counts only if we have an update. + if (genesisRes.count > 0) { + await this.counts.applyGenesisLocation({ old: genesis[0], new: pointer }); + } + + const current = await sql` + SELECT * FROM current_locations WHERE inscription_id = ${args.inscription_id} + `; + const currentRes = await sql` INSERT INTO current_locations ${sql(pointer)} ON CONFLICT (inscription_id) DO UPDATE SET location_id = EXCLUDED.location_id, @@ -843,6 +846,11 @@ export class PgStore extends BasePgStore { (EXCLUDED.block_height = current_locations.block_height AND EXCLUDED.tx_index > current_locations.tx_index) `; + // Affect current location counts only if we have an update. + if (currentRes.count > 0) { + await this.counts.applyCurrentLocation({ old: current[0], new: pointer }); + } + // Backfill orphan locations for this inscription, if any. await sql` UPDATE locations @@ -865,7 +873,7 @@ export class PgStore extends BasePgStore { SELECT * FROM current_locations WHERE location_id = ${args.location.id} `; if (current.count > 0) { - await sql` + const update = await sql` WITH prev AS ( SELECT id, block_height, tx_index, address FROM locations @@ -880,7 +888,9 @@ export class PgStore extends BasePgStore { address = prev.address FROM prev WHERE c.inscription_id = ${args.location.inscription_id} + RETURNING * `; + await this.counts.rollBackCurrentLocation({ curr: current[0], prev: update[0] }); } }); } diff --git a/src/pg/types.ts b/src/pg/types.ts index b4226f90..c35c9cae 100644 --- a/src/pg/types.ts +++ b/src/pg/types.ts @@ -181,9 +181,9 @@ export type DbInscriptionIndexPaging = { export type DbInscriptionIndexFilters = { genesis_id?: string[]; genesis_block_height?: number; - genesis_block_hash?: string; from_genesis_block_height?: number; to_genesis_block_height?: number; + genesis_block_hash?: string; from_genesis_timestamp?: number; to_genesis_timestamp?: number; from_sat_coinbase_height?: number; @@ -192,6 +192,7 @@ export type DbInscriptionIndexFilters = { from_number?: number; to_number?: number; address?: string[]; + genesis_address?: string[]; mime_type?: string[]; output?: string; sat_rarity?: SatoshiRarity[]; @@ -199,6 +200,7 @@ export type DbInscriptionIndexFilters = { from_sat_ordinal?: bigint; to_sat_ordinal?: bigint; recursive?: boolean; + cursed?: boolean; }; export type DbInscriptionIndexOrder = { @@ -206,20 +208,9 @@ export type DbInscriptionIndexOrder = { order?: Order; }; -/** Type of row count required for an inscription index endpoint call */ -export enum DbInscriptionIndexResultCountType { - /** All inscriptions */ - all, - /** Filtered by mime type */ - mimeType, - /** Filtered by sat rarity */ - satRarity, - /** Filtered by address */ - address, - /** Filtered by some param that yields a single result (easy to count) */ - singleResult, - /** Filtered by custom arguments (very hard to count) */ - intractable, +export enum DbInscriptionType { + blessed = 'blessed', + cursed = 'cursed', } export type DbInscriptionCountPerBlockFilters = { diff --git a/tests/inscriptions.test.ts b/tests/inscriptions.test.ts index 6b484661..3212dd87 100644 --- a/tests/inscriptions.test.ts +++ b/tests/inscriptions.test.ts @@ -1890,7 +1890,7 @@ describe('/inscriptions', () => { }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); - // expect(responseJson1.total).toBe(1); + expect(responseJson1.total).toBe(1); expect(responseJson1.results.length).toBe(1); expect(responseJson1.results[0].genesis_block_height).toBe(775617); @@ -1900,7 +1900,7 @@ describe('/inscriptions', () => { }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); - // expect(responseJson2.total).toBe(1); + expect(responseJson2.total).toBe(1); expect(responseJson2.results.length).toBe(1); expect(responseJson2.results[0].genesis_block_height).toBe(778575); @@ -1910,9 +1910,51 @@ describe('/inscriptions', () => { }); expect(response3.statusCode).toBe(200); const responseJson3 = response3.json(); - // expect(responseJson3.total).toBe(1); + expect(responseJson3.total).toBe(1); expect(responseJson3.results.length).toBe(1); expect(responseJson3.results[0].genesis_block_height).toBe(775617); + + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: 778580, + hash: '000000000000000000003ac2d5b588bc97a5479d25e403cffd90bd60c9680cfc', + timestamp: 1676913207, + }) + .transaction({ + hash: '25b372de3de0cb6fcc52c89a8bc3fb78eec596521ba20de16e53c1585be7c3fc', + }) + .inscriptionRevealed({ + content_bytes: '0x48656C6C6F', + content_type: 'text/plain;charset=utf-8', + content_length: 5, + inscription_number: 70, + inscription_fee: 705, + inscription_id: '25b372de3de0cb6fcc52c89a8bc3fb78eec596521ba20de16e53c1585be7c3fci0', + inscription_output_value: 10000, + inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', + ordinal_number: 257418248345364, + ordinal_block_height: 650000, + ordinal_offset: 0, + satpoint_post_inscription: + '25b372de3de0cb6fcc52c89a8bc3fb78eec596521ba20de16e53c1585be7c3fc:0:0', + inscription_input_index: 0, + transfers_pre_inscription: 0, + tx_index: 0, + }) + .build() + ); + const response4 = await fastify.inject({ + method: 'GET', + url: '/ordinals/v1/inscriptions?from_genesis_block_height=778000&to_genesis_block_height=779000', + }); + expect(response4.statusCode).toBe(200); + const responseJson4 = response4.json(); + expect(responseJson4.total).toBe(2); + expect(responseJson4.results.length).toBe(2); + expect(responseJson4.results[0].genesis_block_height).toBe(778580); + expect(responseJson4.results[1].genesis_block_height).toBe(778575); }); test('index filtered by block hash', async () => { @@ -1985,7 +2027,7 @@ describe('/inscriptions', () => { }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); - // expect(responseJson1.total).toBe(1); + expect(responseJson1.total).toBe(1); expect(responseJson1.results.length).toBe(1); expect(responseJson1.results[0].genesis_block_hash).toBe( '000000000000000000039b3051705a16fcf310a70dee55742339e6da70181bf7' @@ -2062,7 +2104,7 @@ describe('/inscriptions', () => { }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); - // expect(responseJson2.total).toBe(1); + expect(responseJson2.total).toBe(1); expect(responseJson2.results.length).toBe(1); expect(responseJson2.results[0].genesis_timestamp).toBe(1677731361000); @@ -2072,7 +2114,7 @@ describe('/inscriptions', () => { }); expect(response3.statusCode).toBe(200); const responseJson3 = response3.json(); - // expect(responseJson3.total).toBe(1); + expect(responseJson3.total).toBe(1); expect(responseJson3.results.length).toBe(1); expect(responseJson3.results[0].genesis_timestamp).toBe(1675312161000); }); @@ -2147,7 +2189,7 @@ describe('/inscriptions', () => { }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); - // expect(responseJson2.total).toBe(1); + expect(responseJson2.total).toBe(1); expect(responseJson2.results.length).toBe(1); expect(responseJson2.results[0].sat_ordinal).toBe('257418248345364'); @@ -2157,7 +2199,7 @@ describe('/inscriptions', () => { }); expect(response3.statusCode).toBe(200); const responseJson3 = response3.json(); - // expect(responseJson3.total).toBe(1); + expect(responseJson3.total).toBe(1); expect(responseJson3.results.length).toBe(1); expect(responseJson3.results[0].sat_ordinal).toBe('1000000000000'); }); @@ -2232,7 +2274,7 @@ describe('/inscriptions', () => { }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); - // expect(responseJson2.total).toBe(1); + expect(responseJson2.total).toBe(1); expect(responseJson2.results.length).toBe(1); expect(responseJson2.results[0].sat_coinbase_height).toBe(51483); @@ -2242,7 +2284,7 @@ describe('/inscriptions', () => { }); expect(response3.statusCode).toBe(200); const responseJson3 = response3.json(); - // expect(responseJson3.total).toBe(1); + expect(responseJson3.total).toBe(1); expect(responseJson3.results.length).toBe(1); expect(responseJson3.results[0].sat_coinbase_height).toBe(200); }); @@ -2317,7 +2359,7 @@ describe('/inscriptions', () => { }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); - // expect(responseJson2.total).toBe(1); + expect(responseJson2.total).toBe(1); expect(responseJson2.results[0].number).toBe(50); const response3 = await fastify.inject({ @@ -2326,7 +2368,7 @@ describe('/inscriptions', () => { }); expect(response3.statusCode).toBe(200); const responseJson3 = response3.json(); - // expect(responseJson3.total).toBe(1); + expect(responseJson3.total).toBe(1); expect(responseJson3.results.length).toBe(1); expect(responseJson3.results[0].number).toBe(7); }); @@ -2666,7 +2708,7 @@ describe('/inscriptions', () => { }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); - // expect(responseJson1.total).toBe(1); + expect(responseJson1.total).toBe(1); expect(responseJson1.results.length).toBe(1); expect(responseJson1.results[0].id).toBe( '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0' @@ -2678,23 +2720,21 @@ describe('/inscriptions', () => { }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); - // expect(responseJson2.total).toBe(1); + expect(responseJson2.total).toBe(1); expect(responseJson2.results.length).toBe(1); expect(responseJson2.results[0].id).toBe( '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0' ); }); - }); - describe('ordering', () => { - test('index ordered by number', async () => { + test('index filtered by cursed', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ height: 778575, hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, + timestamp: 1677731361, }) .transaction({ hash: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', @@ -2725,21 +2765,23 @@ describe('/inscriptions', () => { .block({ height: 775617, hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, + timestamp: 1675312161, }) .transaction({ hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) .inscriptionRevealed({ - content_bytes: '0x48656C6C6F', - content_type: 'text/plain;charset=utf-8', + content_bytes: `0x${Buffer.from( + 'Hello /content/9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0' + ).toString('hex')}`, + content_type: 'image/png', content_length: 5, - inscription_number: 8, - inscription_fee: 705, + inscription_number: -100, + inscription_fee: 2805, inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', inscription_output_value: 10000, inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 1050000000000000, + ordinal_number: 1000000000000, ordinal_block_height: 650000, ordinal_offset: 0, satpoint_post_inscription: @@ -2750,31 +2792,89 @@ describe('/inscriptions', () => { }) .build() ); + + const response1 = await fastify.inject({ + method: 'GET', + url: '/ordinals/v1/inscriptions?cursed=true', + }); + expect(response1.statusCode).toBe(200); + const responseJson1 = response1.json(); + expect(responseJson1.total).toBe(1); + expect(responseJson1.results.length).toBe(1); + expect(responseJson1.results[0].id).toBe( + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0' + ); + + const response2 = await fastify.inject({ + method: 'GET', + url: '/ordinals/v1/inscriptions?cursed=false', + }); + expect(response2.statusCode).toBe(200); + const responseJson2 = response2.json(); + expect(responseJson2.total).toBe(1); + expect(responseJson2.results.length).toBe(1); + expect(responseJson2.results[0].id).toBe( + '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0' + ); + }); + + test('index filtered by genesis address', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() .block({ - height: 778583, + height: 778575, + hash: '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + timestamp: 1677731361, + }) + .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: 257418248345364, + ordinal_block_height: 650000, + ordinal_offset: 0, + satpoint_post_inscription: + '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0', + inscription_input_index: 0, + transfers_pre_inscription: 0, + tx_index: 0, + }) + .build() + ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: 775617, hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', - timestamp: 1676913207, + timestamp: 1675312161, }) .transaction({ - hash: '567c7605439dfdc3a289d13fd2132237852f4a56e784b9364ba94499d5f9baf1', + hash: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', }) .inscriptionRevealed({ content_bytes: '0x48656C6C6F', content_type: 'image/png', content_length: 5, - inscription_number: 9, + inscription_number: 1, inscription_fee: 2805, - inscription_id: '567c7605439dfdc3a289d13fd2132237852f4a56e784b9364ba94499d5f9baf1i0', + inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', inscription_output_value: 10000, - inscriber_address: 'bc1pxq6t85qp57aw8yf8eh9t7vsgd9zm5a8372rdll5jzrmc3cxqdpmqfucdry', - ordinal_number: 0, - ordinal_block_height: 0, + inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + ordinal_number: 1000000000000, + ordinal_block_height: 650000, ordinal_offset: 0, satpoint_post_inscription: - '567c7605439dfdc3a289d13fd2132237852f4a56e784b9364ba94499d5f9baf1:0:0', + '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0:0', inscription_input_index: 0, transfers_pre_inscription: 0, tx_index: 0, @@ -2784,28 +2884,29 @@ describe('/inscriptions', () => { const response1 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?order_by=number&order=asc', + url: '/ordinals/v1/inscriptions?genesis_address=bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); - expect(responseJson1.total).toBe(3); - expect(responseJson1.results[0].number).toStrictEqual(7); - expect(responseJson1.results[1].number).toStrictEqual(8); - expect(responseJson1.results[2].number).toStrictEqual(9); + expect(responseJson1.total).toBe(1); + expect(responseJson1.results.length).toBe(1); + expect(responseJson1.results[0].genesis_address).toBe( + 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj' + ); const response2 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?order_by=number&order=desc', + url: '/ordinals/v1/inscriptions?genesis_address=bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj&genesis_address=bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); - expect(responseJson2.total).toBe(3); - expect(responseJson2.results[0].number).toStrictEqual(9); - expect(responseJson2.results[1].number).toStrictEqual(8); - expect(responseJson2.results[2].number).toStrictEqual(7); + expect(responseJson2.total).toBe(2); + expect(responseJson2.results.length).toBe(2); }); + }); - test('index ordered by sat rarity', async () => { + describe('ordering', () => { + test('index ordered by number', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() @@ -2902,28 +3003,28 @@ describe('/inscriptions', () => { const response1 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?order_by=rarity&order=asc', + url: '/ordinals/v1/inscriptions?order_by=number&order=asc', }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); expect(responseJson1.total).toBe(3); - expect(responseJson1.results[0].sat_rarity).toStrictEqual('common'); - expect(responseJson1.results[1].sat_rarity).toStrictEqual('epic'); - expect(responseJson1.results[2].sat_rarity).toStrictEqual('mythic'); + expect(responseJson1.results[0].number).toStrictEqual(7); + expect(responseJson1.results[1].number).toStrictEqual(8); + expect(responseJson1.results[2].number).toStrictEqual(9); const response2 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?order_by=rarity&order=desc', + url: '/ordinals/v1/inscriptions?order_by=number&order=desc', }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); expect(responseJson2.total).toBe(3); - expect(responseJson2.results[0].sat_rarity).toStrictEqual('mythic'); - expect(responseJson2.results[1].sat_rarity).toStrictEqual('epic'); - expect(responseJson2.results[2].sat_rarity).toStrictEqual('common'); + expect(responseJson2.results[0].number).toStrictEqual(9); + expect(responseJson2.results[1].number).toStrictEqual(8); + expect(responseJson2.results[2].number).toStrictEqual(7); }); - test('index ordered by sat ordinal', async () => { + test('index ordered by sat rarity', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() @@ -3020,28 +3121,28 @@ describe('/inscriptions', () => { const response1 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?order_by=ordinal&order=asc', + url: '/ordinals/v1/inscriptions?order_by=rarity&order=asc', }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); expect(responseJson1.total).toBe(3); - expect(responseJson1.results[0].sat_ordinal).toStrictEqual('0'); - expect(responseJson1.results[1].sat_ordinal).toStrictEqual('257418248345364'); - expect(responseJson1.results[2].sat_ordinal).toStrictEqual('1050000000000000'); + expect(responseJson1.results[0].sat_rarity).toStrictEqual('common'); + expect(responseJson1.results[1].sat_rarity).toStrictEqual('epic'); + expect(responseJson1.results[2].sat_rarity).toStrictEqual('mythic'); const response2 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?order_by=ordinal&order=desc', + url: '/ordinals/v1/inscriptions?order_by=rarity&order=desc', }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); expect(responseJson2.total).toBe(3); - expect(responseJson2.results[0].sat_ordinal).toStrictEqual('1050000000000000'); - expect(responseJson2.results[1].sat_ordinal).toStrictEqual('257418248345364'); - expect(responseJson2.results[2].sat_ordinal).toStrictEqual('0'); + expect(responseJson2.results[0].sat_rarity).toStrictEqual('mythic'); + expect(responseJson2.results[1].sat_rarity).toStrictEqual('epic'); + expect(responseJson2.results[2].sat_rarity).toStrictEqual('common'); }); - test('index ordered by genesis block height', async () => { + test('index ordered by sat ordinal', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() .apply() @@ -3138,33 +3239,30 @@ describe('/inscriptions', () => { const response1 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?order_by=genesis_block_height&order=asc', + url: '/ordinals/v1/inscriptions?order_by=ordinal&order=asc', }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); expect(responseJson1.total).toBe(3); - expect(responseJson1.results[0].genesis_block_height).toStrictEqual(775617); - expect(responseJson1.results[1].genesis_block_height).toStrictEqual(778575); - expect(responseJson1.results[2].genesis_block_height).toStrictEqual(778583); + expect(responseJson1.results[0].sat_ordinal).toStrictEqual('0'); + expect(responseJson1.results[1].sat_ordinal).toStrictEqual('257418248345364'); + expect(responseJson1.results[2].sat_ordinal).toStrictEqual('1050000000000000'); const response2 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?order_by=genesis_block_height&order=desc', + url: '/ordinals/v1/inscriptions?order_by=ordinal&order=desc', }); expect(response2.statusCode).toBe(200); const responseJson2 = response2.json(); expect(responseJson2.total).toBe(3); - expect(responseJson2.results[0].genesis_block_height).toStrictEqual(778583); - expect(responseJson2.results[1].genesis_block_height).toStrictEqual(778575); - expect(responseJson2.results[2].genesis_block_height).toStrictEqual(775617); + expect(responseJson2.results[0].sat_ordinal).toStrictEqual('1050000000000000'); + expect(responseJson2.results[1].sat_ordinal).toStrictEqual('257418248345364'); + expect(responseJson2.results[2].sat_ordinal).toStrictEqual('0'); }); - }); - describe('when not streaming', () => { - test('counts are returned as zero', async () => { + test('index ordered by genesis block height', async () => { await db.updateInscriptions( new TestChainhookPayloadBuilder() - .streamingBlocks(false) .apply() .block({ height: 778575, @@ -3183,8 +3281,8 @@ describe('/inscriptions', () => { inscription_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', inscription_output_value: 10000, inscriber_address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', - ordinal_number: 0, - ordinal_block_height: 0, + ordinal_number: 257418248345364, + ordinal_block_height: 650000, ordinal_offset: 0, satpoint_post_inscription: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0:0', @@ -3196,7 +3294,6 @@ describe('/inscriptions', () => { ); await db.updateInscriptions( new TestChainhookPayloadBuilder() - .streamingBlocks(false) .apply() .block({ height: 775617, @@ -3208,14 +3305,14 @@ describe('/inscriptions', () => { }) .inscriptionRevealed({ content_bytes: '0x48656C6C6F', - content_type: 'image/png', + content_type: 'text/plain;charset=utf-8', content_length: 5, - inscription_number: 1, - inscription_fee: 2805, + inscription_number: 8, + inscription_fee: 705, inscription_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', inscription_output_value: 10000, inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', - ordinal_number: 257418248345364, + ordinal_number: 1050000000000000, ordinal_block_height: 650000, ordinal_offset: 0, satpoint_post_inscription: @@ -3226,33 +3323,59 @@ describe('/inscriptions', () => { }) .build() ); + await db.updateInscriptions( + new TestChainhookPayloadBuilder() + .apply() + .block({ + height: 778583, + hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + timestamp: 1676913207, + }) + .transaction({ + hash: '567c7605439dfdc3a289d13fd2132237852f4a56e784b9364ba94499d5f9baf1', + }) + .inscriptionRevealed({ + content_bytes: '0x48656C6C6F', + content_type: 'image/png', + content_length: 5, + inscription_number: 9, + inscription_fee: 2805, + inscription_id: '567c7605439dfdc3a289d13fd2132237852f4a56e784b9364ba94499d5f9baf1i0', + inscription_output_value: 10000, + inscriber_address: 'bc1pxq6t85qp57aw8yf8eh9t7vsgd9zm5a8372rdll5jzrmc3cxqdpmqfucdry', + ordinal_number: 0, + ordinal_block_height: 0, + ordinal_offset: 0, + satpoint_post_inscription: + '567c7605439dfdc3a289d13fd2132237852f4a56e784b9364ba94499d5f9baf1:0:0', + inscription_input_index: 0, + transfers_pre_inscription: 0, + tx_index: 0, + }) + .build() + ); const response1 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?mime_type=text/plain', + url: '/ordinals/v1/inscriptions?order_by=genesis_block_height&order=asc', }); expect(response1.statusCode).toBe(200); const responseJson1 = response1.json(); - expect(responseJson1.total).toBe(0); - expect(responseJson1.results.length).toBeGreaterThan(0); + expect(responseJson1.total).toBe(3); + expect(responseJson1.results[0].genesis_block_height).toStrictEqual(775617); + expect(responseJson1.results[1].genesis_block_height).toStrictEqual(778575); + expect(responseJson1.results[2].genesis_block_height).toStrictEqual(778583); const response2 = await fastify.inject({ method: 'GET', - url: '/ordinals/v1/inscriptions?rarity=mythic', + url: '/ordinals/v1/inscriptions?order_by=genesis_block_height&order=desc', }); 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); + expect(responseJson2.total).toBe(3); + expect(responseJson2.results[0].genesis_block_height).toStrictEqual(778583); + expect(responseJson2.results[1].genesis_block_height).toStrictEqual(778575); + expect(responseJson2.results[2].genesis_block_height).toStrictEqual(775617); }); }); });