Skip to content

Commit

Permalink
feat: add recursion endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Jul 18, 2023
1 parent bce62ef commit 522d7f7
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 38 deletions.
4 changes: 3 additions & 1 deletion src/api/init.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import FastifyCors from '@fastify/cors';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { PINO_LOGGER_CONFIG } from '@hirosystems/api-toolkit';
import Fastify, { FastifyPluginAsync } from 'fastify';
import FastifyMetrics, { IFastifyMetrics } from 'fastify-metrics';
import { Server } from 'http';
import { PgStore } from '../pg/pg-store';
import { InscriptionsRoutes } from './routes/inscriptions';
import { RecursionRoutes } from './routes/recursion';
import { SatRoutes } from './routes/sats';
import { StatsRoutes } from './routes/stats';
import { StatusRoutes } from './routes/status';
import { isProdEnv } from './util/helpers';
import { PINO_LOGGER_CONFIG } from '@hirosystems/api-toolkit';

export const Api: FastifyPluginAsync<
Record<never, never>,
Expand All @@ -20,6 +21,7 @@ export const Api: FastifyPluginAsync<
await fastify.register(InscriptionsRoutes);
await fastify.register(SatRoutes);
await fastify.register(StatsRoutes);
await fastify.register(RecursionRoutes);
};

export async function buildApiServer(args: { db: PgStore }) {
Expand Down
129 changes: 129 additions & 0 deletions src/api/routes/recursion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { Type } from '@sinclair/typebox';
import { FastifyPluginAsync, FastifyPluginCallback } from 'fastify';
import { Server } from 'http';
import {
BlockHashResponse,
BlockHeightParam,
BlockHeightResponse,
BlockTimestampResponse,
NotFoundResponse,
} from '../schemas';

const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = (
fastify,
options,
done
) => {
// todo: add blockheight cache? or re-use the inscriptions per block cache (since that would invalidate on gaps as well)
// fastify.addHook('preHandler', handleInscriptionTransfersCache);

fastify.get(
'/blockheight',
{
schema: {
operationId: 'getBlockHeight',
summary: 'Recursion',
description: 'Retrieves the latest block height',
tags: ['Recursion'],
response: {
200: BlockHeightResponse,
404: NotFoundResponse,
},
},
},
async (request, reply) => {
const blockHeight = (await fastify.db.getChainTipBlockHeight()) ?? 'blockheight';
// Currently, the `chain_tip` materialized view should always return a
// minimum of 767430 (inscription #0 genesis), but we'll keep the fallback
// to stay consistent with `ord`.

await reply.send(blockHeight.toString());
}
);

fastify.get(
'/blockhash',
{
schema: {
operationId: 'getBlockHash',
summary: 'Recursion',
description: 'Retrieves the latest block hash',
tags: ['Recursion'],
response: {
200: BlockHashResponse,
404: NotFoundResponse,
},
},
},
async (request, reply) => {
const blockHash = (await fastify.db.getBlockHash()) ?? 'blockhash';
await reply.send(blockHash);
}
);

fastify.get(
'/blocktime',
{
schema: {
operationId: 'getBlockTime',
summary: 'Recursion',
description: 'Retrieves the latest block time',
tags: ['Recursion'],
response: {
200: BlockTimestampResponse,
404: NotFoundResponse,
},
},
},
async (request, reply) => {
const blockTime = (await fastify.db.getBlockTimestamp()) ?? 'blocktime';
await reply.send(blockTime);
}
);

done();
};

const ShowRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = (
fastify,
options,
done
) => {
// todo: add blockheight cache? or re-use the inscriptions per block cache (since that would invalidate on gaps as well)
// fastify.addHook('preHandler', handleInscriptionCache);

fastify.get(
'/blockhash/:block_height',
{
schema: {
operationId: 'getBlockHash',
summary: 'Recursion',
description: 'Retrieves the block hash for a given block height',
tags: ['Recursion'],
params: Type.Object({
block_height: BlockHeightParam,
}),
response: {
200: BlockHashResponse,
404: NotFoundResponse,
},
},
},
async (request, reply) => {
const blockHash = (await fastify.db.getBlockHash(request.params.block_height)) ?? 'blockhash';
await reply.send(blockHash);
}
);

done();
};

export const RecursionRoutes: FastifyPluginAsync<
Record<never, never>,
Server,
TypeBoxTypeProvider
> = async fastify => {
await fastify.register(IndexRoutes);
await fastify.register(ShowRoutes);
};
11 changes: 11 additions & 0 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,3 +351,14 @@ export const InscriptionsPerBlockResponse = Type.Object({
results: Type.Array(InscriptionsPerBlock),
});
export type InscriptionsPerBlockResponse = Static<typeof InscriptionsPerBlockResponse>;

export const BlockHeightResponse = Type.String({ examples: ['778921'] });
export type BlockHeightResponse = Static<typeof BlockHeightResponse>;

export const BlockHashResponse = Type.String({
examples: ['0000000000000000000452773967cdd62297137cdaf79950c5e8bb0c62075133'],
});
export type BlockHashResponse = Static<typeof BlockHashResponse>;

export const BlockTimestampResponse = Type.String({ examples: ['1677733170000'] });
export type BlockTimestampResponse = Static<typeof BlockTimestampResponse>;
34 changes: 32 additions & 2 deletions src/pg/pg-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { BasePgStore, connectPostgres, logger, runMigrations } from '@hirosystems/api-toolkit';
import { BitcoinEvent, Payload } from '@hirosystems/chainhook-client';
import * as path from 'path';
import { Order, OrderBy } from '../api/schemas';
import { isProdEnv, isTestEnv, normalizedHexString, parseSatPoint } from '../api/util/helpers';
import { OrdinalSatoshi, SatoshiRarity } from '../api/util/ordinal-satoshi';
Expand All @@ -21,8 +23,6 @@ import {
DbPaginatedResult,
LOCATIONS_COLUMNS,
} from './types';
import { BasePgStore, connectPostgres, logger, runMigrations } from '@hirosystems/api-toolkit';
import * as path from 'path';

export const MIGRATIONS_DIR = path.join(__dirname, '../../migrations');

Expand Down Expand Up @@ -212,6 +212,36 @@ export class PgStore extends BasePgStore {
return parseInt(result[0].block_height);
}

/**
* Returns the block hash of the latest block, or the block hash of the block
* at the given height.
* @param blockHeight - optional block height (defaults to latest block)
*/
async getBlockHash(blockHeight?: string): Promise<string> {
const clause = blockHeight
? this.sql`WHERE block_height = ${blockHeight}`
: this.sql`
ORDER BY block_height DESC
LIMIT 1
`;

const result = await this.sql<{ block_hash: string }[]>`
SELECT block_hash FROM inscriptions_per_block
${clause}
`;

return result[0]?.block_hash;
}

async getBlockTimestamp(): Promise<string> {
const result = await this.sql<{ timestamp: string }[]>`
SELECT ROUND(EXTRACT(EPOCH FROM timestamp)) as timestamp FROM inscriptions_per_block
ORDER BY block_height DESC
LIMIT 1
`;
return result[0]?.timestamp;
}

async getChainTipInscriptionCount(): Promise<number> {
const result = await this.sql<{ count: number }[]>`
SELECT count FROM inscription_count
Expand Down
34 changes: 34 additions & 0 deletions tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,3 +108,37 @@ export class TestChainhookPayloadBuilder {
/** Generate a random hash like string for testing */
export const randomHash = () =>
[...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');

/** Generate a random-ish reveal apply payload for testing */
export function testRevealApply(
blockHeight: number,
args: { blockHash?: string; timestamp?: number } = {}
) {
// todo: more params could be randomized
const randomHex = randomHash();
return new TestChainhookPayloadBuilder()
.apply()
.block({
height: blockHeight,
hash: args.blockHash ?? '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d',
timestamp: args.timestamp ?? 1676913207,
})
.transaction({
hash: `0x${randomHex}`,
})
.inscriptionRevealed({
content_bytes: '0x48656C6C6F',
content_type: 'image/png',
content_length: 5,
inscription_number: Math.floor(Math.random() * 100_000),
inscription_fee: 2805,
inscription_id: `${randomHex}i0`,
inscription_output_value: 10000,
inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td',
ordinal_number: Math.floor(Math.random() * 1_000_000),
ordinal_block_height: Math.floor(Math.random() * 777_000),
ordinal_offset: 0,
satpoint_post_inscription: `${randomHex}:0:0`,
})
.build();
}
Loading

0 comments on commit 522d7f7

Please sign in to comment.