Skip to content

Commit

Permalink
feat: accept inscription number on endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelcr committed Mar 1, 2023
1 parent 38d6c6b commit 952bf8e
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 34 deletions.
29 changes: 16 additions & 13 deletions src/api/routes/inscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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 { FastifyPluginAsync, FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
Expand All @@ -20,7 +19,10 @@ import {
OrderParam,
OrderBy,
Order,
InscriptionIdParam,
InscriptionIdentifierParam,
BlockHashParamCType,
BlockHeightParamCType,
InscriptionIdParamCType,
} from '../types';
import { handleChainTipCache, handleInscriptionCache } from '../util/cache';
import {
Expand All @@ -30,9 +32,6 @@ import {
parseDbInscriptions,
} from '../util/helpers';

const BlockHashParamCType = TypeCompiler.Compile(BlockHashParam);
const BlockHeightParamCType = TypeCompiler.Compile(BlockHeightParam);

const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = (
fastify,
options,
Expand Down Expand Up @@ -103,14 +102,14 @@ const ShowRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTyp
) => {
fastify.addHook('preHandler', handleInscriptionCache);
fastify.get(
'/inscriptions/:inscription_id',
'/inscriptions/:id',
{
schema: {
summary: 'Inscription',
description: 'Retrieves a single inscription',
tags: ['Inscriptions'],
params: Type.Object({
inscription_id: InscriptionIdParam,
id: InscriptionIdentifierParam,
}),
response: {
200: InscriptionResponse,
Expand All @@ -119,8 +118,11 @@ const ShowRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTyp
},
},
async (request, reply) => {
const idArg = InscriptionIdParamCType.Check(request.params.id)
? { genesis_id: request.params.id }
: { number: request.params.id };
const inscription = await fastify.db.getInscriptions({
genesis_id: request.params.inscription_id,
...idArg,
limit: 1,
offset: 0,
});
Expand All @@ -133,14 +135,14 @@ const ShowRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTyp
);

fastify.get(
'/inscriptions/:inscription_id/content',
'/inscriptions/:id/content',
{
schema: {
summary: 'Inscription content',
description: 'Retrieves the contents of a single inscription',
tags: ['Inscriptions'],
params: Type.Object({
inscription_id: InscriptionIdParam,
id: InscriptionIdentifierParam,
}),
response: {
200: Type.Uint8Array(),
Expand All @@ -149,9 +151,10 @@ const ShowRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTyp
},
},
async (request, reply) => {
const inscription = await fastify.db.getInscriptionContent({
inscription_id: request.params.inscription_id,
});
const idArg = InscriptionIdParamCType.Check(request.params.id)
? { genesis_id: request.params.id }
: { number: request.params.id };
const inscription = await fastify.db.getInscriptionContent(idArg);
if (inscription) {
const bytes = hexToBuffer(inscription.content);
await reply
Expand Down
17 changes: 15 additions & 2 deletions src/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Static, TSchema, Type } from '@sinclair/typebox';
import { TypeCompiler } from '@sinclair/typebox/compiler';
import { SatoshiRarity, SAT_SUPPLY } from './util/ordinal-satoshi';

export const AddressParam = Type.String({
Expand All @@ -7,12 +8,22 @@ export const AddressParam = Type.String({
examples: ['bc1p8aq8s3z9xl87e74twfk93mljxq6alv4a79yheadx33t9np4g2wkqqt8kc5'],
});

export 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]+$/, {
title: 'Inscription ID',
description: 'Inscription unique identifier',
examples: ['38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0'],
});
export const InscriptionIdParamCType = TypeCompiler.Compile(InscriptionIdParam);

export const InscriptionNumberParam = Type.Integer({
minimum: 0,
title: 'Inscription Number',
description: 'Number of the inscription',
examples: ['10500'],
});
export const InscriptionNumberParamCType = TypeCompiler.Compile(InscriptionNumberParam);

export const InscriptionIdentifierParam = Type.Union([InscriptionIdParam, InscriptionNumberParam]);

export const OrdinalParam = Type.Integer({
title: 'Ordinal Number',
Expand All @@ -27,12 +38,14 @@ export const BlockHeightParam = Type.RegEx(/^[0-9]+$/, {
description: 'Bitcoin block height',
examples: [777678],
});
export const BlockHeightParamCType = TypeCompiler.Compile(BlockHeightParam);

export const BlockHashParam = Type.RegEx(/^[0]{8}[a-fA-F0-9]{56}$/, {
title: 'Block Hash',
description: 'Bitcoin block hash',
examples: ['0000000000000000000452773967cdd62297137cdaf79950c5e8bb0c62075133'],
});
export const BlockHashParamCType = TypeCompiler.Compile(BlockHashParam);

export const MimeTypeParam = Type.RegEx(/^\w+\/[-.\w]+(?:\+[-.\w]+)?$/, {
title: 'MIME Type',
Expand Down
12 changes: 5 additions & 7 deletions src/api/util/cache.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FastifyReply, FastifyRequest } from 'fastify';
import { logger } from '../../logger';
import { InscriptionIdRegEx } from '../types';
import { InscriptionIdParamCType, InscriptionNumberParamCType } from '../types';

export enum ETagType {
chainTip,
Expand Down Expand Up @@ -57,18 +57,16 @@ export function setReplyNonCacheable(reply: FastifyReply) {
async function getInscriptionLocationEtag(request: FastifyRequest): Promise<string | undefined> {
try {
const components = request.url.split('/');
let inscription_id: string | undefined;
do {
const lastElement = components.pop();
if (lastElement && lastElement.length) {
if (InscriptionIdRegEx.test(lastElement)) {
inscription_id = lastElement;
break;
if (InscriptionIdParamCType.Check(lastElement)) {
return await request.server.db.getInscriptionETag({ genesis_id: lastElement });
} else if (InscriptionNumberParamCType.Check(parseInt(lastElement))) {
return await request.server.db.getInscriptionETag({ number: lastElement });
}
}
} while (components.length);
if (!inscription_id) return;
return await request.server.db.getInscriptionETag({ inscription_id });
} catch (error) {
return;
}
Expand Down
23 changes: 17 additions & 6 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
LOCATIONS_COLUMNS,
} from './types';

type InscriptionIdentifier = { genesis_id: string } | { number: number };

export class PgStore extends BasePgStore {
static async connect(opts?: { skipMigrations: boolean }): Promise<PgStore> {
const pgConfig = {
Expand Down Expand Up @@ -118,25 +120,33 @@ export class PgStore extends BasePgStore {
return result[0];
}

async getInscriptionContent(args: {
inscription_id: string;
}): Promise<DbInscriptionContent | undefined> {
async getInscriptionContent(
args: InscriptionIdentifier
): Promise<DbInscriptionContent | undefined> {
const result = await this.sql<DbInscriptionContent[]>`
SELECT content, content_type, content_length
FROM inscriptions
WHERE genesis_id = ${args.inscription_id}
WHERE ${
'genesis_id' in args
? this.sql`genesis_id = ${args.genesis_id}`
: this.sql`number = ${args.number}`
}
`;
if (result.count > 0) {
return result[0];
}
}

async getInscriptionETag(args: { inscription_id: string }): Promise<string | undefined> {
async getInscriptionETag(args: InscriptionIdentifier): Promise<string | undefined> {
const result = await this.sql<{ etag: string }[]>`
SELECT date_part('epoch', l.timestamp)::text AS etag
FROM locations AS l
INNER JOIN inscriptions AS i ON l.inscription_id = i.id
WHERE i.genesis_id = ${args.inscription_id}
WHERE ${
'genesis_id' in args
? this.sql`i.genesis_id = ${args.genesis_id}`
: this.sql`i.number = ${args.number}`
}
AND l.current = TRUE
`;
if (result.count > 0) {
Expand All @@ -148,6 +158,7 @@ export class PgStore extends BasePgStore {
genesis_id?: string;
genesis_block_height?: number;
genesis_block_hash?: string;
number?: number;
address?: string;
mime_type?: string[];
output?: string;
Expand Down
22 changes: 22 additions & 0 deletions tests/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,29 @@ describe('ETag cache', () => {
expect(response.headers.etag).not.toBeUndefined();
const etag = response.headers.etag;

// Check on numbered id too
const nResponse = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/7',
});
expect(nResponse.statusCode).toBe(200);
expect(nResponse.headers.etag).not.toBeUndefined();
const nEtag = nResponse.headers.etag;
expect(nEtag).toBe(etag);

// Cached response
const cached = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
headers: { 'if-none-match': etag },
});
expect(cached.statusCode).toBe(304);
const nCached = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/7',
headers: { 'if-none-match': etag },
});
expect(nCached.statusCode).toBe(304);

// Simulate modified location and check status code
await db.sql`UPDATE locations SET timestamp = NOW() WHERE true`;
Expand All @@ -71,6 +87,12 @@ describe('ETag cache', () => {
headers: { 'if-none-match': etag },
});
expect(cached2.statusCode).toBe(200);
const nCached2 = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/7',
headers: { 'if-none-match': etag },
});
expect(nCached2.statusCode).toBe(200);
});

test('inscriptions index cache control', async () => {
Expand Down
23 changes: 17 additions & 6 deletions tests/inscriptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,7 @@ describe('/inscriptions', () => {
current: true,
},
});
const response = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
});
expect(response.statusCode).toBe(200);
expect(response.json()).toStrictEqual({
const expected = {
address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td',
genesis_block_hash: '00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d',
genesis_block_height: 775617,
Expand All @@ -69,7 +64,23 @@ describe('/inscriptions', () => {
sat_rarity: 'common',
timestamp: 1676913207000,
genesis_tx_id: '38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dc',
};

// By inscription id
const response = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/38c46a8bf7ec90bc7f6b797e7dc84baa97f4e5fd4286b92fe1b50176d03b18dci0',
});
expect(response.statusCode).toBe(200);
expect(response.json()).toStrictEqual(expected);

// By inscription number
const response2 = await fastify.inject({
method: 'GET',
url: '/ordinals/v1/inscriptions/7',
});
expect(response2.statusCode).toBe(200);
expect(response2.json()).toStrictEqual(expected);
});

test('index filtered by mime type', async () => {
Expand Down

0 comments on commit 952bf8e

Please sign in to comment.