diff --git a/src/api/routes/inscriptions.ts b/src/api/routes/inscriptions.ts index 66d779d8..0ebfe1ad 100644 --- a/src/api/routes/inscriptions.ts +++ b/src/api/routes/inscriptions.ts @@ -23,6 +23,7 @@ import { InscriptionIdParamCType, BlockParam, BlockHeightParam, + TimestampParam, } from '../types'; import { handleChainTipCache, handleInscriptionCache } from '../util/cache'; import { @@ -63,6 +64,8 @@ const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTy genesis_block: Type.Optional(BlockParam), from_genesis_block_height: Type.Optional(BlockHeightParam), to_genesis_block_height: Type.Optional(BlockHeightParam), + from_genesis_timestamp: Type.Optional(TimestampParam), + to_genesis_timestamp: Type.Optional(TimestampParam), output: Type.Optional(OutputParam), address: Type.Optional(AddressParam), mime_type: Type.Optional(MimeTypesParam), @@ -87,6 +90,8 @@ const IndexRoutes: FastifyPluginCallback, Server, TypeBoxTy ...blockParam(request.query.genesis_block, 'genesis_block'), ...blockParam(request.query.from_genesis_block_height, 'from_genesis_block'), ...blockParam(request.query.to_genesis_block_height, 'to_genesis_block'), + from_genesis_timestamp: request.query.from_genesis_timestamp, + to_genesis_timestamp: request.query.to_genesis_timestamp, output: request.query.output, address: request.query.address, mime_type: request.query.mime_type, diff --git a/src/api/types.ts b/src/api/types.ts index a51aad96..002e2904 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -79,6 +79,12 @@ export const SatoshiRaritiesParam = Type.Array( } ); +export const TimestampParam = Type.Integer({ + title: 'Timestamp', + description: 'Block UNIX epoch timestamp (milliseconds)', + examples: [1677731361], +}); + export const OutputParam = Type.RegEx(/^[a-fA-F0-9]{64}:[0-9]+$/, { title: 'Transaction Output', description: 'An UTXO for a Bitcoin transaction', diff --git a/src/pg/pg-store.ts b/src/pg/pg-store.ts index 0960222a..d11df3cf 100644 --- a/src/pg/pg-store.ts +++ b/src/pg/pg-store.ts @@ -160,6 +160,8 @@ export class PgStore extends BasePgStore { genesis_block_hash?: string; from_genesis_block_height?: number; to_genesis_block_height?: number; + from_genesis_timestamp?: number; + to_genesis_timestamp?: number; number?: number; address?: string; mime_type?: string[]; @@ -217,6 +219,16 @@ export class PgStore extends BasePgStore { ? this.sql`AND gen.block_height <= ${args.to_genesis_block_height}` : this.sql`` } + ${ + args.from_genesis_timestamp + ? this.sql`AND gen.timestamp >= to_timestamp(${args.from_genesis_timestamp})` + : this.sql`` + } + ${ + args.to_genesis_timestamp + ? this.sql`AND gen.timestamp <= to_timestamp(${args.to_genesis_timestamp})` + : this.sql`` + } ${args.address ? this.sql`AND loc.address = ${args.address}` : this.sql``} ${ args.mime_type?.length diff --git a/tests/inscriptions.test.ts b/tests/inscriptions.test.ts index 9e578240..8e37f16e 100644 --- a/tests/inscriptions.test.ts +++ b/tests/inscriptions.test.ts @@ -440,6 +440,79 @@ describe('/inscriptions', () => { ); }); + test('index filtered by timestamp range', async () => { + await db.insertInscriptionGenesis({ + inscription: { + genesis_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201i0', + mime_type: 'text/plain', + content_type: 'text/plain;charset=utf-8', + content_length: 5, + number: 7, + content: '0x48656C6C6F', + fee: 705n, + }, + location: { + inscription_id: 0, + block_height: 778575, + block_hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + tx_id: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201', + address: 'bc1pscktlmn99gyzlvymvrezh6vwd0l4kg06tg5rvssw0czg8873gz5sdkteqj', + output: '9f4a9b73b0713c5da01c0a47f97c6c001af9028d6bdd9e264dfacbc4e6790201:0', + offset: 0n, + value: 10000n, + timestamp: 1677731361, + sat_ordinal: 257418248345364n, + sat_rarity: 'common', + genesis: true, + current: true, + }, + }); + await db.insertInscriptionGenesis({ + inscription: { + genesis_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0', + mime_type: 'image/png', + content_type: 'image/png', + content_length: 5, + number: 7, + content: '0x48656C6C6F', + fee: 2805n, + }, + location: { + inscription_id: 0, + block_height: 775617, + block_hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d', + tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc', + address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td', + output: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc:0', + offset: 0n, + value: 10000n, + timestamp: 1675312161, + sat_ordinal: 257418248345364n, + sat_rarity: 'epic', + genesis: true, + current: true, + }, + }); + + const response2 = await fastify.inject({ + method: 'GET', + url: '/ordinals/v1/inscriptions?from_genesis_timestamp=1675571361', + }); + expect(response2.statusCode).toBe(200); + const responseJson2 = response2.json(); + expect(responseJson2.total).toBe(1); + expect(responseJson2.results[0].genesis_timestamp).toBe(1677731361000); + + const response3 = await fastify.inject({ + method: 'GET', + url: '/ordinals/v1/inscriptions?to_genesis_timestamp=1675571361', + }); + expect(response3.statusCode).toBe(200); + const responseJson3 = response3.json(); + expect(responseJson3.total).toBe(1); + expect(responseJson3.results[0].genesis_timestamp).toBe(1675312161000); + }); + test('index filtered by output', async () => { await db.insertInscriptionGenesis({ inscription: {