diff --git a/migrations/1676395230930_inscriptions.ts b/migrations/1676395230930_inscriptions.ts index 9fabef9b..c7d9e927 100644 --- a/migrations/1676395230930_inscriptions.ts +++ b/migrations/1676395230930_inscriptions.ts @@ -22,11 +22,11 @@ export function up(pgm: MigrationBuilder): void { notNull: true, }, block_hash: { - type: 'text', + type: 'bytea', notNull: true, }, tx_id: { - type: 'text', + type: 'bytea', notNull: true, }, address: { diff --git a/src/api/routes/inscriptions.ts b/src/api/routes/inscriptions.ts index 68eec20d..9d950baa 100644 --- a/src/api/routes/inscriptions.ts +++ b/src/api/routes/inscriptions.ts @@ -1,5 +1,6 @@ import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'; import { Type } from '@sinclair/typebox'; +import { TypeCompiler } from '@sinclair/typebox/compiler'; import { Value } from '@sinclair/typebox/value'; import { FastifyPluginCallback } from 'fastify'; import { Server } from 'http'; @@ -12,14 +13,19 @@ import { NotFoundResponse, OffsetParam, PaginatedResponse, + BlockHashParam, } from '../types'; import { DEFAULT_API_LIMIT, parseDbInscriptions, parseDbInscription, hexToBuffer, + normalizeHashString, } from '../util/helpers'; +const BlockHashParamCType = TypeCompiler.Compile(BlockHashParam); +const BlockHeightParamCType = TypeCompiler.Compile(BlockHeightParam); + export const InscriptionRoutes: FastifyPluginCallback< Record, Server, @@ -33,7 +39,7 @@ export const InscriptionRoutes: FastifyPluginCallback< description: 'Retrieves inscriptions', tags: ['Inscriptions'], querystring: Type.Object({ - block_height: Type.Optional(BlockHeightParam), + block: Type.Optional(Type.Union([BlockHashParam, BlockHeightParam])), address: Type.Optional(BitcoinAddressParam), offset: Type.Optional(OffsetParam), limit: Type.Optional(LimitParam), @@ -47,7 +53,17 @@ export const InscriptionRoutes: FastifyPluginCallback< async (request, reply) => { const limit = request.query.limit ?? DEFAULT_API_LIMIT; const offset = request.query.offset ?? 0; - const inscriptions = await fastify.db.getInscriptions({ ...request.query, limit, offset }); + const blockArg = BlockHashParamCType.Check(request.query.block) + ? { block_hash: normalizeHashString(request.query.block) as string } + : BlockHeightParamCType.Check(request.query.block) + ? { block_height: request.query.block } + : {}; + const inscriptions = await fastify.db.getInscriptions({ + ...blockArg, + address: request.query.address, + limit, + offset, + }); await reply.send({ limit, offset, diff --git a/src/api/types.ts b/src/api/types.ts index a2f2292e..f3704ab6 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -1,18 +1,18 @@ import { Static, TSchema, Type } from '@sinclair/typebox'; import { SatoshiRarity } from './util/ordinal-satoshi'; -const BitcoinAddressRegEx = /(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}/; -export const BitcoinAddressParam = Type.RegEx(BitcoinAddressRegEx); +export const BitcoinAddressParam = Type.RegEx(/^(bc1|[13])[a-zA-HJ-NP-Z0-9]{25,39}$/); -const InscriptionIdRegEx = /[a-fA-F0-9]{64}i[0-9]+/; -export const InscriptionIdParam = Type.RegEx(InscriptionIdRegEx, { +export const InscriptionIdParam = Type.RegEx(/^[a-fA-F0-9]{64}i[0-9]+$/, { description: 'Inscription ID', examples: ['38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0'], }); export const OrdinalParam = Type.Integer(); -export const BlockHeightParam = Type.Integer(); +export const BlockHeightParam = Type.RegEx(/^[0-9]+$/); + +export const BlockHashParam = Type.RegEx(/^(0x)?[0]{8}[a-fA-F0-9]{56}$/); export const OffsetParam = Type.Integer({ minimum: 0 }); diff --git a/src/api/util/helpers.ts b/src/api/util/helpers.ts index 246dbe30..d067e77d 100644 --- a/src/api/util/helpers.ts +++ b/src/api/util/helpers.ts @@ -39,3 +39,25 @@ export function hexToBuffer(hex: string): Buffer { } return Buffer.from(hex.substring(2), 'hex'); } + +export const has0xPrefix = (id: string) => id.substr(0, 2).toLowerCase() === '0x'; + +/** + * Check if the input is a valid 32-byte hex string. If valid, returns a + * lowercase and 0x-prefixed hex string. If invalid, returns false. + */ +export function normalizeHashString(input: string): string | false { + if (typeof input !== 'string') { + return false; + } + let hashBuffer: Buffer | undefined; + if (input.length === 66 && has0xPrefix(input)) { + hashBuffer = Buffer.from(input.slice(2), 'hex'); + } else if (input.length === 64) { + hashBuffer = Buffer.from(input, 'hex'); + } + if (hashBuffer === undefined || hashBuffer.length !== 32) { + return false; + } + return `0x${hashBuffer.toString('hex')}`; +} diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 4ea48d57..ed284df3 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -71,6 +71,7 @@ export class PgStore extends BasePgStore { async getInscriptions(args: { block_height?: number; + block_hash?: string; address?: string; limit: number; offset: number; @@ -80,6 +81,7 @@ export class PgStore extends BasePgStore { FROM inscriptions WHERE true ${args.block_height ? this.sql`AND block_height = ${args.block_height}` : this.sql``} + ${args.block_hash ? this.sql`AND block_hash = ${args.block_hash}` : this.sql``} ${args.address ? this.sql`AND address = ${args.address}` : this.sql``} ORDER BY block_height DESC LIMIT ${args.limit}