Skip to content

feat: add /extended/v2/block-tenures/:height/blocks endpoint #2285

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions migrations/1747318945115_blocks-tenure-height-index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable camelcase */

exports.shorthands = undefined;

exports.up = pgm => {
pgm.createIndex('blocks', ['tenure_height', { name: 'block_height', sort: 'DESC' }]);
};

exports.down = pgm => {
pgm.dropIndex('blocks', ['tenure_height', { name: 'block_height', sort: 'DESC' }]);
};
2 changes: 2 additions & 0 deletions src/api/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import FastifyCors from '@fastify/cors';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import * as promClient from 'prom-client';
import DeprecationPlugin from './deprecation-plugin';
import { BlockTenureRoutes } from './routes/v2/block-tenures';

export interface ApiServer {
fastifyApp: FastifyInstance;
Expand Down Expand Up @@ -100,6 +101,7 @@ export const StacksApiRoutes: FastifyPluginAsync<
async fastify => {
await fastify.register(BlockRoutesV2, { prefix: '/blocks' });
await fastify.register(BurnBlockRoutesV2, { prefix: '/burn-blocks' });
await fastify.register(BlockTenureRoutes, { prefix: '/block-tenures' });
await fastify.register(SmartContractRoutesV2, { prefix: '/smart-contracts' });
await fastify.register(MempoolRoutesV2, { prefix: '/mempool' });
await fastify.register(PoxRoutesV2, { prefix: '/pox' });
Expand Down
64 changes: 64 additions & 0 deletions src/api/routes/v2/block-tenures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { Type, TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { FastifyPluginAsync } from 'fastify';
import { Server } from 'node:http';
import { handleBlockCache } from '../../../../src/api/controllers/cache-controller';
import { getPagingQueryLimit, ResourceType } from '../../../../src/api/pagination';
import { CursorOffsetParam, LimitParam } from '../../../../src/api/schemas/params';
import { BlockListV2ResponseSchema } from '../../../../src/api/schemas/responses/responses';
import { BlockTenureParamsSchema } from './schemas';
import { NotFoundError } from '../../../../src/errors';
import { NakamotoBlock } from '../../../../src/api/schemas/entities/block';
import { parseDbNakamotoBlock } from './helpers';

export const BlockTenureRoutes: FastifyPluginAsync<
Record<never, never>,
Server,
TypeBoxTypeProvider
> = async fastify => {
fastify.get(
'/:tenure_height/blocks',
{
preHandler: handleBlockCache,
schema: {
operationId: 'get_tenure_blocks',
summary: 'Get blocks by tenure',
description: `Retrieves blocks confirmed in a block tenure`,
tags: ['Blocks'],
params: BlockTenureParamsSchema,
querystring: Type.Object({
limit: LimitParam(ResourceType.Block),
offset: CursorOffsetParam({ resource: ResourceType.Block }),
cursor: Type.Optional(Type.String({ description: 'Cursor for pagination' })),
}),
response: {
200: BlockListV2ResponseSchema,
},
},
},
async (req, reply) => {
const offset = req.query.offset ?? 0;
const limit = getPagingQueryLimit(ResourceType.Block, req.query.limit);
const blockQuery = await fastify.db.v2.getBlocks({
offset,
limit,
cursor: req.query.cursor,
tenureHeight: req.params.tenure_height,
});
if (req.query.cursor && !blockQuery.current_cursor) {
throw new NotFoundError('Cursor not found');
}
const blocks: NakamotoBlock[] = blockQuery.results.map(r => parseDbNakamotoBlock(r));
await reply.send({
limit: blockQuery.limit,
offset: blockQuery.offset,
total: blockQuery.total,
next_cursor: blockQuery.next_cursor,
prev_cursor: blockQuery.prev_cursor,
cursor: blockQuery.current_cursor,
results: blocks,
});
}
);

await Promise.resolve();
};
20 changes: 14 additions & 6 deletions src/api/routes/v2/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ const BurnBlockHashParamSchema = Type.String({
description: 'Burn block hash',
examples: ['0000000000000000000452773967cdd62297137cdaf79950c5e8bb0c62075133'],
});
export const CompiledBurnBlockHashParam = ajv.compile(BurnBlockHashParamSchema);

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

const TenureHeightParamSchema = Type.Integer({
minimum: 0,
title: 'Block tenure height',
description: 'Block tenure height',
examples: [165453],
});

// ==========================
// Query and path params
// TODO: Migrate these to each endpoint after switching from Express to Fastify
Expand Down Expand Up @@ -180,9 +186,6 @@ export type TransactionPaginationQueryParams = Static<
const PoxCyclePaginationQueryParamsSchema = PaginationQueryParamsSchema(PoxCycleLimitParamSchema);
export type PoxCyclePaginationQueryParams = Static<typeof PoxCyclePaginationQueryParamsSchema>;

const PoxSignerPaginationQueryParamsSchema = PaginationQueryParamsSchema(PoxSignerLimitParamSchema);
export type PoxSignerPaginationQueryParams = Static<typeof PoxSignerPaginationQueryParamsSchema>;

export const BlockParamsSchema = Type.Object(
{
height_or_hash: Type.Union([
Expand All @@ -205,7 +208,13 @@ export const BurnBlockParamsSchema = Type.Object(
},
{ additionalProperties: false }
);
export type BurnBlockParams = Static<typeof BurnBlockParamsSchema>;

export const BlockTenureParamsSchema = Type.Object(
{
tenure_height: TenureHeightParamSchema,
},
{ additionalProperties: false }
);

export const SmartContractStatusParamsSchema = Type.Object(
{
Expand All @@ -228,4 +237,3 @@ export const AddressTransactionParamsSchema = Type.Object(
},
{ additionalProperties: false }
);
export type AddressTransactionParams = Static<typeof AddressTransactionParamsSchema>;
56 changes: 29 additions & 27 deletions src/datastore/pg-store-v2.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
import { BasePgStoreModule, PgSqlClient, has0xPrefix } from '@hirosystems/api-toolkit';
import {
BlockLimitParamSchema,
CompiledBurnBlockHashParam,
TransactionPaginationQueryParams,
TransactionLimitParamSchema,
BlockParams,
BurnBlockParams,
BlockPaginationQueryParams,
SmartContractStatusParams,
AddressParams,
AddressTransactionParams,
PoxCyclePaginationQueryParams,
PoxCycleLimitParamSchema,
PoxSignerPaginationQueryParams,
PoxSignerLimitParamSchema,
BlockIdParam,
BlockSignerSignatureLimitParamSchema,
Expand Down Expand Up @@ -51,13 +46,6 @@ import {
} from './helpers';
import { SyntheticPoxEventName } from '../pox-helpers';

async function assertAddressExists(sql: PgSqlClient, address: string) {
const addressCheck =
await sql`SELECT principal FROM principal_stx_txs WHERE principal = ${address} LIMIT 1`;
if (addressCheck.count === 0)
throw new InvalidRequestError(`Address not found`, InvalidRequestErrorType.invalid_param);
}

async function assertTxIdExists(sql: PgSqlClient, tx_id: string) {
const txCheck = await sql`SELECT tx_id FROM txs WHERE tx_id = ${tx_id} LIMIT 1`;
if (txCheck.count === 0)
Expand All @@ -69,11 +57,15 @@ export class PgStoreV2 extends BasePgStoreModule {
limit: number;
offset?: number;
cursor?: string;
tenureHeight?: number;
}): Promise<DbCursorPaginatedResult<DbBlock>> {
return await this.sqlTransaction(async sql => {
const limit = args.limit;
const offset = args.offset ?? 0;
const cursor = args.cursor ?? null;
const tenureFilter = args.tenureHeight
? sql`AND tenure_height = ${args.tenureHeight}`
: sql``;

const blocksQuery = await sql<
(BlockQueryResult & { total: number; next_block_hash: string; prev_block_hash: string })[]
Expand All @@ -82,7 +74,7 @@ export class PgStoreV2 extends BasePgStoreModule {
WITH ordered_blocks AS (
SELECT *, LEAD(block_height, ${offset}) OVER (ORDER BY block_height DESC) offset_block_height
FROM blocks
WHERE canonical = true
WHERE canonical = true ${tenureFilter}
ORDER BY block_height DESC
)
SELECT offset_block_height as block_height
Expand All @@ -94,20 +86,22 @@ export class PgStoreV2 extends BasePgStoreModule {
SELECT ${sql(BLOCK_COLUMNS)}
FROM blocks
WHERE canonical = true
AND block_height <= (SELECT block_height FROM cursor_block)
${tenureFilter}
AND block_height <= (SELECT block_height FROM cursor_block)
ORDER BY block_height DESC
LIMIT ${limit}
),
prev_page AS (
SELECT index_block_hash as prev_block_hash
FROM blocks
WHERE canonical = true
AND block_height < (
SELECT block_height
FROM selected_blocks
ORDER BY block_height DESC
LIMIT 1
)
${tenureFilter}
AND block_height < (
SELECT block_height
FROM selected_blocks
ORDER BY block_height DESC
LIMIT 1
)
ORDER BY block_height DESC
OFFSET ${limit - 1}
LIMIT 1
Expand All @@ -116,18 +110,26 @@ export class PgStoreV2 extends BasePgStoreModule {
SELECT index_block_hash as next_block_hash
FROM blocks
WHERE canonical = true
AND block_height > (
SELECT block_height
FROM selected_blocks
ORDER BY block_height DESC
LIMIT 1
)
${tenureFilter}
AND block_height > (
SELECT block_height
FROM selected_blocks
ORDER BY block_height DESC
LIMIT 1
)
ORDER BY block_height ASC
OFFSET ${limit - 1}
LIMIT 1
),
block_count AS (
SELECT ${
args.tenureHeight
? sql`(SELECT COUNT(*) FROM blocks WHERE tenure_height = ${args.tenureHeight})::int`
: sql`(SELECT block_count FROM chain_tip)::int`
} AS total
)
SELECT
(SELECT block_count FROM chain_tip)::int AS total,
(SELECT total FROM block_count) AS total,
sb.*,
nb.next_block_hash,
pb.prev_block_hash
Expand Down
Loading