Skip to content

Commit f5c2e01

Browse files
authored
feat: add /extended/v2/block-tenures/:height/blocks endpoint (#2285)
1 parent 3418863 commit f5c2e01

File tree

6 files changed

+281
-33
lines changed

6 files changed

+281
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/* eslint-disable camelcase */
2+
3+
exports.shorthands = undefined;
4+
5+
exports.up = pgm => {
6+
pgm.createIndex('blocks', ['tenure_height', { name: 'block_height', sort: 'DESC' }]);
7+
};
8+
9+
exports.down = pgm => {
10+
pgm.dropIndex('blocks', ['tenure_height', { name: 'block_height', sort: 'DESC' }]);
11+
};

src/api/init.ts

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import FastifyCors from '@fastify/cors';
5858
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
5959
import * as promClient from 'prom-client';
6060
import DeprecationPlugin from './deprecation-plugin';
61+
import { BlockTenureRoutes } from './routes/v2/block-tenures';
6162

6263
export interface ApiServer {
6364
fastifyApp: FastifyInstance;
@@ -100,6 +101,7 @@ export const StacksApiRoutes: FastifyPluginAsync<
100101
async fastify => {
101102
await fastify.register(BlockRoutesV2, { prefix: '/blocks' });
102103
await fastify.register(BurnBlockRoutesV2, { prefix: '/burn-blocks' });
104+
await fastify.register(BlockTenureRoutes, { prefix: '/block-tenures' });
103105
await fastify.register(SmartContractRoutesV2, { prefix: '/smart-contracts' });
104106
await fastify.register(MempoolRoutesV2, { prefix: '/mempool' });
105107
await fastify.register(PoxRoutesV2, { prefix: '/pox' });

src/api/routes/v2/block-tenures.ts

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
2+
import { FastifyPluginAsync } from 'fastify';
3+
import { Server } from 'node:http';
4+
import { handleBlockCache } from '../../../../src/api/controllers/cache-controller';
5+
import { getPagingQueryLimit, ResourceType } from '../../../../src/api/pagination';
6+
import { CursorOffsetParam, LimitParam } from '../../../../src/api/schemas/params';
7+
import { BlockListV2ResponseSchema } from '../../../../src/api/schemas/responses/responses';
8+
import { BlockTenureParamsSchema } from './schemas';
9+
import { NotFoundError } from '../../../../src/errors';
10+
import { NakamotoBlock } from '../../../../src/api/schemas/entities/block';
11+
import { parseDbNakamotoBlock } from './helpers';
12+
13+
export const BlockTenureRoutes: FastifyPluginAsync<
14+
Record<never, never>,
15+
Server,
16+
TypeBoxTypeProvider
17+
> = async fastify => {
18+
fastify.get(
19+
'/:tenure_height/blocks',
20+
{
21+
preHandler: handleBlockCache,
22+
schema: {
23+
operationId: 'get_tenure_blocks',
24+
summary: 'Get blocks by tenure',
25+
description: `Retrieves blocks confirmed in a block tenure`,
26+
tags: ['Blocks'],
27+
params: BlockTenureParamsSchema,
28+
querystring: Type.Object({
29+
limit: LimitParam(ResourceType.Block),
30+
offset: CursorOffsetParam({ resource: ResourceType.Block }),
31+
cursor: Type.Optional(Type.String({ description: 'Cursor for pagination' })),
32+
}),
33+
response: {
34+
200: BlockListV2ResponseSchema,
35+
},
36+
},
37+
},
38+
async (req, reply) => {
39+
const offset = req.query.offset ?? 0;
40+
const limit = getPagingQueryLimit(ResourceType.Block, req.query.limit);
41+
const blockQuery = await fastify.db.v2.getBlocks({
42+
offset,
43+
limit,
44+
cursor: req.query.cursor,
45+
tenureHeight: req.params.tenure_height,
46+
});
47+
if (req.query.cursor && !blockQuery.current_cursor) {
48+
throw new NotFoundError('Cursor not found');
49+
}
50+
const blocks: NakamotoBlock[] = blockQuery.results.map(r => parseDbNakamotoBlock(r));
51+
await reply.send({
52+
limit: blockQuery.limit,
53+
offset: blockQuery.offset,
54+
total: blockQuery.total,
55+
next_cursor: blockQuery.next_cursor,
56+
prev_cursor: blockQuery.prev_cursor,
57+
cursor: blockQuery.current_cursor,
58+
results: blocks,
59+
});
60+
}
61+
);
62+
63+
await Promise.resolve();
64+
};

src/api/routes/v2/schemas.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,6 @@ const BurnBlockHashParamSchema = Type.String({
109109
description: 'Burn block hash',
110110
examples: ['0000000000000000000452773967cdd62297137cdaf79950c5e8bb0c62075133'],
111111
});
112-
export const CompiledBurnBlockHashParam = ajv.compile(BurnBlockHashParamSchema);
113112

114113
const BurnBlockHeightParamSchema = Type.Integer({
115114
title: 'Burn block height',
@@ -153,6 +152,13 @@ const TransactionIdParamSchema = Type.String({
153152
examples: ['0xf6bd5f4a7b26184a3466340b2e99fd003b4962c0e382a7e4b6a13df3dd7a91c6'],
154153
});
155154

155+
const TenureHeightParamSchema = Type.Integer({
156+
minimum: 0,
157+
title: 'Block tenure height',
158+
description: 'Block tenure height',
159+
examples: [165453],
160+
});
161+
156162
// ==========================
157163
// Query and path params
158164
// TODO: Migrate these to each endpoint after switching from Express to Fastify
@@ -180,9 +186,6 @@ export type TransactionPaginationQueryParams = Static<
180186
const PoxCyclePaginationQueryParamsSchema = PaginationQueryParamsSchema(PoxCycleLimitParamSchema);
181187
export type PoxCyclePaginationQueryParams = Static<typeof PoxCyclePaginationQueryParamsSchema>;
182188

183-
const PoxSignerPaginationQueryParamsSchema = PaginationQueryParamsSchema(PoxSignerLimitParamSchema);
184-
export type PoxSignerPaginationQueryParams = Static<typeof PoxSignerPaginationQueryParamsSchema>;
185-
186189
export const BlockParamsSchema = Type.Object(
187190
{
188191
height_or_hash: Type.Union([
@@ -205,7 +208,13 @@ export const BurnBlockParamsSchema = Type.Object(
205208
},
206209
{ additionalProperties: false }
207210
);
208-
export type BurnBlockParams = Static<typeof BurnBlockParamsSchema>;
211+
212+
export const BlockTenureParamsSchema = Type.Object(
213+
{
214+
tenure_height: TenureHeightParamSchema,
215+
},
216+
{ additionalProperties: false }
217+
);
209218

210219
export const SmartContractStatusParamsSchema = Type.Object(
211220
{
@@ -228,4 +237,3 @@ export const AddressTransactionParamsSchema = Type.Object(
228237
},
229238
{ additionalProperties: false }
230239
);
231-
export type AddressTransactionParams = Static<typeof AddressTransactionParamsSchema>;

src/datastore/pg-store-v2.ts

+29-27
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,13 @@
11
import { BasePgStoreModule, PgSqlClient, has0xPrefix } from '@hirosystems/api-toolkit';
22
import {
33
BlockLimitParamSchema,
4-
CompiledBurnBlockHashParam,
54
TransactionPaginationQueryParams,
65
TransactionLimitParamSchema,
7-
BlockParams,
8-
BurnBlockParams,
96
BlockPaginationQueryParams,
107
SmartContractStatusParams,
118
AddressParams,
12-
AddressTransactionParams,
139
PoxCyclePaginationQueryParams,
1410
PoxCycleLimitParamSchema,
15-
PoxSignerPaginationQueryParams,
1611
PoxSignerLimitParamSchema,
1712
BlockIdParam,
1813
BlockSignerSignatureLimitParamSchema,
@@ -51,13 +46,6 @@ import {
5146
} from './helpers';
5247
import { SyntheticPoxEventName } from '../pox-helpers';
5348

54-
async function assertAddressExists(sql: PgSqlClient, address: string) {
55-
const addressCheck =
56-
await sql`SELECT principal FROM principal_stx_txs WHERE principal = ${address} LIMIT 1`;
57-
if (addressCheck.count === 0)
58-
throw new InvalidRequestError(`Address not found`, InvalidRequestErrorType.invalid_param);
59-
}
60-
6149
async function assertTxIdExists(sql: PgSqlClient, tx_id: string) {
6250
const txCheck = await sql`SELECT tx_id FROM txs WHERE tx_id = ${tx_id} LIMIT 1`;
6351
if (txCheck.count === 0)
@@ -69,11 +57,15 @@ export class PgStoreV2 extends BasePgStoreModule {
6957
limit: number;
7058
offset?: number;
7159
cursor?: string;
60+
tenureHeight?: number;
7261
}): Promise<DbCursorPaginatedResult<DbBlock>> {
7362
return await this.sqlTransaction(async sql => {
7463
const limit = args.limit;
7564
const offset = args.offset ?? 0;
7665
const cursor = args.cursor ?? null;
66+
const tenureFilter = args.tenureHeight
67+
? sql`AND tenure_height = ${args.tenureHeight}`
68+
: sql``;
7769

7870
const blocksQuery = await sql<
7971
(BlockQueryResult & { total: number; next_block_hash: string; prev_block_hash: string })[]
@@ -82,7 +74,7 @@ export class PgStoreV2 extends BasePgStoreModule {
8274
WITH ordered_blocks AS (
8375
SELECT *, LEAD(block_height, ${offset}) OVER (ORDER BY block_height DESC) offset_block_height
8476
FROM blocks
85-
WHERE canonical = true
77+
WHERE canonical = true ${tenureFilter}
8678
ORDER BY block_height DESC
8779
)
8880
SELECT offset_block_height as block_height
@@ -94,20 +86,22 @@ export class PgStoreV2 extends BasePgStoreModule {
9486
SELECT ${sql(BLOCK_COLUMNS)}
9587
FROM blocks
9688
WHERE canonical = true
97-
AND block_height <= (SELECT block_height FROM cursor_block)
89+
${tenureFilter}
90+
AND block_height <= (SELECT block_height FROM cursor_block)
9891
ORDER BY block_height DESC
9992
LIMIT ${limit}
10093
),
10194
prev_page AS (
10295
SELECT index_block_hash as prev_block_hash
10396
FROM blocks
10497
WHERE canonical = true
105-
AND block_height < (
106-
SELECT block_height
107-
FROM selected_blocks
108-
ORDER BY block_height DESC
109-
LIMIT 1
110-
)
98+
${tenureFilter}
99+
AND block_height < (
100+
SELECT block_height
101+
FROM selected_blocks
102+
ORDER BY block_height DESC
103+
LIMIT 1
104+
)
111105
ORDER BY block_height DESC
112106
OFFSET ${limit - 1}
113107
LIMIT 1
@@ -116,18 +110,26 @@ export class PgStoreV2 extends BasePgStoreModule {
116110
SELECT index_block_hash as next_block_hash
117111
FROM blocks
118112
WHERE canonical = true
119-
AND block_height > (
120-
SELECT block_height
121-
FROM selected_blocks
122-
ORDER BY block_height DESC
123-
LIMIT 1
124-
)
113+
${tenureFilter}
114+
AND block_height > (
115+
SELECT block_height
116+
FROM selected_blocks
117+
ORDER BY block_height DESC
118+
LIMIT 1
119+
)
125120
ORDER BY block_height ASC
126121
OFFSET ${limit - 1}
127122
LIMIT 1
123+
),
124+
block_count AS (
125+
SELECT ${
126+
args.tenureHeight
127+
? sql`(SELECT COUNT(*) FROM blocks WHERE tenure_height = ${args.tenureHeight})::int`
128+
: sql`(SELECT block_count FROM chain_tip)::int`
129+
} AS total
128130
)
129131
SELECT
130-
(SELECT block_count FROM chain_tip)::int AS total,
132+
(SELECT total FROM block_count) AS total,
131133
sb.*,
132134
nb.next_block_hash,
133135
pb.prev_block_hash

0 commit comments

Comments
 (0)