Skip to content
This repository was archived by the owner on Mar 14, 2025. It is now read-only.

Commit 522d7f7

Browse files
committed
feat: add recursion endpoints
1 parent bce62ef commit 522d7f7

File tree

7 files changed

+371
-38
lines changed

7 files changed

+371
-38
lines changed

src/api/init.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import FastifyCors from '@fastify/cors';
22
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
3+
import { PINO_LOGGER_CONFIG } from '@hirosystems/api-toolkit';
34
import Fastify, { FastifyPluginAsync } from 'fastify';
45
import FastifyMetrics, { IFastifyMetrics } from 'fastify-metrics';
56
import { Server } from 'http';
67
import { PgStore } from '../pg/pg-store';
78
import { InscriptionsRoutes } from './routes/inscriptions';
9+
import { RecursionRoutes } from './routes/recursion';
810
import { SatRoutes } from './routes/sats';
911
import { StatsRoutes } from './routes/stats';
1012
import { StatusRoutes } from './routes/status';
1113
import { isProdEnv } from './util/helpers';
12-
import { PINO_LOGGER_CONFIG } from '@hirosystems/api-toolkit';
1314

1415
export const Api: FastifyPluginAsync<
1516
Record<never, never>,
@@ -20,6 +21,7 @@ export const Api: FastifyPluginAsync<
2021
await fastify.register(InscriptionsRoutes);
2122
await fastify.register(SatRoutes);
2223
await fastify.register(StatsRoutes);
24+
await fastify.register(RecursionRoutes);
2325
};
2426

2527
export async function buildApiServer(args: { db: PgStore }) {

src/api/routes/recursion.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
2+
import { Type } from '@sinclair/typebox';
3+
import { FastifyPluginAsync, FastifyPluginCallback } from 'fastify';
4+
import { Server } from 'http';
5+
import {
6+
BlockHashResponse,
7+
BlockHeightParam,
8+
BlockHeightResponse,
9+
BlockTimestampResponse,
10+
NotFoundResponse,
11+
} from '../schemas';
12+
13+
const IndexRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = (
14+
fastify,
15+
options,
16+
done
17+
) => {
18+
// todo: add blockheight cache? or re-use the inscriptions per block cache (since that would invalidate on gaps as well)
19+
// fastify.addHook('preHandler', handleInscriptionTransfersCache);
20+
21+
fastify.get(
22+
'/blockheight',
23+
{
24+
schema: {
25+
operationId: 'getBlockHeight',
26+
summary: 'Recursion',
27+
description: 'Retrieves the latest block height',
28+
tags: ['Recursion'],
29+
response: {
30+
200: BlockHeightResponse,
31+
404: NotFoundResponse,
32+
},
33+
},
34+
},
35+
async (request, reply) => {
36+
const blockHeight = (await fastify.db.getChainTipBlockHeight()) ?? 'blockheight';
37+
// Currently, the `chain_tip` materialized view should always return a
38+
// minimum of 767430 (inscription #0 genesis), but we'll keep the fallback
39+
// to stay consistent with `ord`.
40+
41+
await reply.send(blockHeight.toString());
42+
}
43+
);
44+
45+
fastify.get(
46+
'/blockhash',
47+
{
48+
schema: {
49+
operationId: 'getBlockHash',
50+
summary: 'Recursion',
51+
description: 'Retrieves the latest block hash',
52+
tags: ['Recursion'],
53+
response: {
54+
200: BlockHashResponse,
55+
404: NotFoundResponse,
56+
},
57+
},
58+
},
59+
async (request, reply) => {
60+
const blockHash = (await fastify.db.getBlockHash()) ?? 'blockhash';
61+
await reply.send(blockHash);
62+
}
63+
);
64+
65+
fastify.get(
66+
'/blocktime',
67+
{
68+
schema: {
69+
operationId: 'getBlockTime',
70+
summary: 'Recursion',
71+
description: 'Retrieves the latest block time',
72+
tags: ['Recursion'],
73+
response: {
74+
200: BlockTimestampResponse,
75+
404: NotFoundResponse,
76+
},
77+
},
78+
},
79+
async (request, reply) => {
80+
const blockTime = (await fastify.db.getBlockTimestamp()) ?? 'blocktime';
81+
await reply.send(blockTime);
82+
}
83+
);
84+
85+
done();
86+
};
87+
88+
const ShowRoutes: FastifyPluginCallback<Record<never, never>, Server, TypeBoxTypeProvider> = (
89+
fastify,
90+
options,
91+
done
92+
) => {
93+
// todo: add blockheight cache? or re-use the inscriptions per block cache (since that would invalidate on gaps as well)
94+
// fastify.addHook('preHandler', handleInscriptionCache);
95+
96+
fastify.get(
97+
'/blockhash/:block_height',
98+
{
99+
schema: {
100+
operationId: 'getBlockHash',
101+
summary: 'Recursion',
102+
description: 'Retrieves the block hash for a given block height',
103+
tags: ['Recursion'],
104+
params: Type.Object({
105+
block_height: BlockHeightParam,
106+
}),
107+
response: {
108+
200: BlockHashResponse,
109+
404: NotFoundResponse,
110+
},
111+
},
112+
},
113+
async (request, reply) => {
114+
const blockHash = (await fastify.db.getBlockHash(request.params.block_height)) ?? 'blockhash';
115+
await reply.send(blockHash);
116+
}
117+
);
118+
119+
done();
120+
};
121+
122+
export const RecursionRoutes: FastifyPluginAsync<
123+
Record<never, never>,
124+
Server,
125+
TypeBoxTypeProvider
126+
> = async fastify => {
127+
await fastify.register(IndexRoutes);
128+
await fastify.register(ShowRoutes);
129+
};

src/api/schemas.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,14 @@ export const InscriptionsPerBlockResponse = Type.Object({
351351
results: Type.Array(InscriptionsPerBlock),
352352
});
353353
export type InscriptionsPerBlockResponse = Static<typeof InscriptionsPerBlockResponse>;
354+
355+
export const BlockHeightResponse = Type.String({ examples: ['778921'] });
356+
export type BlockHeightResponse = Static<typeof BlockHeightResponse>;
357+
358+
export const BlockHashResponse = Type.String({
359+
examples: ['0000000000000000000452773967cdd62297137cdaf79950c5e8bb0c62075133'],
360+
});
361+
export type BlockHashResponse = Static<typeof BlockHashResponse>;
362+
363+
export const BlockTimestampResponse = Type.String({ examples: ['1677733170000'] });
364+
export type BlockTimestampResponse = Static<typeof BlockTimestampResponse>;

src/pg/pg-store.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { BasePgStore, connectPostgres, logger, runMigrations } from '@hirosystems/api-toolkit';
12
import { BitcoinEvent, Payload } from '@hirosystems/chainhook-client';
3+
import * as path from 'path';
24
import { Order, OrderBy } from '../api/schemas';
35
import { isProdEnv, isTestEnv, normalizedHexString, parseSatPoint } from '../api/util/helpers';
46
import { OrdinalSatoshi, SatoshiRarity } from '../api/util/ordinal-satoshi';
@@ -21,8 +23,6 @@ import {
2123
DbPaginatedResult,
2224
LOCATIONS_COLUMNS,
2325
} from './types';
24-
import { BasePgStore, connectPostgres, logger, runMigrations } from '@hirosystems/api-toolkit';
25-
import * as path from 'path';
2626

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

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

215+
/**
216+
* Returns the block hash of the latest block, or the block hash of the block
217+
* at the given height.
218+
* @param blockHeight - optional block height (defaults to latest block)
219+
*/
220+
async getBlockHash(blockHeight?: string): Promise<string> {
221+
const clause = blockHeight
222+
? this.sql`WHERE block_height = ${blockHeight}`
223+
: this.sql`
224+
ORDER BY block_height DESC
225+
LIMIT 1
226+
`;
227+
228+
const result = await this.sql<{ block_hash: string }[]>`
229+
SELECT block_hash FROM inscriptions_per_block
230+
${clause}
231+
`;
232+
233+
return result[0]?.block_hash;
234+
}
235+
236+
async getBlockTimestamp(): Promise<string> {
237+
const result = await this.sql<{ timestamp: string }[]>`
238+
SELECT ROUND(EXTRACT(EPOCH FROM timestamp)) as timestamp FROM inscriptions_per_block
239+
ORDER BY block_height DESC
240+
LIMIT 1
241+
`;
242+
return result[0]?.timestamp;
243+
}
244+
215245
async getChainTipInscriptionCount(): Promise<number> {
216246
const result = await this.sql<{ count: number }[]>`
217247
SELECT count FROM inscription_count

tests/helpers.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,37 @@ export class TestChainhookPayloadBuilder {
108108
/** Generate a random hash like string for testing */
109109
export const randomHash = () =>
110110
[...Array(64)].map(() => Math.floor(Math.random() * 16).toString(16)).join('');
111+
112+
/** Generate a random-ish reveal apply payload for testing */
113+
export function testRevealApply(
114+
blockHeight: number,
115+
args: { blockHash?: string; timestamp?: number } = {}
116+
) {
117+
// todo: more params could be randomized
118+
const randomHex = randomHash();
119+
return new TestChainhookPayloadBuilder()
120+
.apply()
121+
.block({
122+
height: blockHeight,
123+
hash: args.blockHash ?? '0x00000000000000000002a90330a99f67e3f01eb2ce070b45930581e82fb7a91d',
124+
timestamp: args.timestamp ?? 1676913207,
125+
})
126+
.transaction({
127+
hash: `0x${randomHex}`,
128+
})
129+
.inscriptionRevealed({
130+
content_bytes: '0x48656C6C6F',
131+
content_type: 'image/png',
132+
content_length: 5,
133+
inscription_number: Math.floor(Math.random() * 100_000),
134+
inscription_fee: 2805,
135+
inscription_id: `${randomHex}i0`,
136+
inscription_output_value: 10000,
137+
inscriber_address: 'bc1p3cyx5e2hgh53w7kpxcvm8s4kkega9gv5wfw7c4qxsvxl0u8x834qf0u2td',
138+
ordinal_number: Math.floor(Math.random() * 1_000_000),
139+
ordinal_block_height: Math.floor(Math.random() * 777_000),
140+
ordinal_offset: 0,
141+
satpoint_post_inscription: `${randomHex}:0:0`,
142+
})
143+
.build();
144+
}

0 commit comments

Comments
 (0)