diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 0725a2c8..00ed7c2d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -720,6 +720,35 @@ paths: $ref: ./api/blocks/get-nakamoto-blocks.schema.json example: $ref: ./api/blocks/get-nakamoto-blocks.example.json + + /extended/v2/blocks/{height_or_hash}: + get: + summary: Get block + description: | + Retrieves a single block + tags: + - Blocks + operationId: get_block + parameters: + - name: height_or_hash + in: path + description: filter by block height, hash, index block hash or the constant `latest` to filter for the most recent block + required: true + schema: + oneOf: + - type: integer + example: 42000 + - type: string + example: "0x4839a8b01cfb39ffcc0d07d3db31e848d5adf5279d529ed5062300b9f353ff79" + responses: + 200: + description: Block + content: + application/json: + schema: + $ref: ./entities/blocks/nakamoto-block.schema.json + example: + $ref: ./entities/blocks/nakamoto-block.example.json /extended/v1/block: get: @@ -769,8 +798,12 @@ paths: type: string example: "0x4839a8b01cfb39ffcc0d07d3db31e848d5adf5279d529ed5062300b9f353ff79" get: + deprecated: true summary: Get block by hash - description: Retrieves block details of a specific block for a given chain height. You can use the hash from your latest block ('get_block_list' API) to get your block details. + description: | + **NOTE:** This endpoint is deprecated in favor of [Get block](#operation/get_block). + + Retrieves block details of a specific block for a given chain height. You can use the hash from your latest block ('get_block_list' API) to get your block details. tags: - Blocks operationId: get_block_by_hash @@ -799,8 +832,12 @@ paths: type: number example: 10000 get: + deprecated: true summary: Get block by height - description: Retrieves block details of a specific block at a given block height + description: | + **NOTE:** This endpoint is deprecated in favor of [Get block](#operation/get_block). + + Retrieves block details of a specific block at a given block height tags: - Blocks operationId: get_block_by_height diff --git a/src/api/routes/v2/blocks.ts b/src/api/routes/v2/blocks.ts index ea1f5d3a..652fc197 100644 --- a/src/api/routes/v2/blocks.ts +++ b/src/api/routes/v2/blocks.ts @@ -6,8 +6,13 @@ import { } from '../../../api/controllers/cache-controller'; import { asyncHandler } from '../../async-handler'; import { NakamotoBlockListResponse } from 'docs/generated'; -import { BlockLimitParamSchema, BlocksQueryParams, CompiledBlocksQueryParams } from './schemas'; -import { parseDbNakamotoBlock, validRequestQuery } from './helpers'; +import { + BlocksQueryParams, + BurnBlockParams, + CompiledBlocksQueryParams, + CompiledBurnBlockParams, +} from './schemas'; +import { parseDbNakamotoBlock, validRequestParams, validRequestQuery } from './helpers'; export function createV2BlocksRouter(db: PgStore): express.Router { const router = express.Router(); @@ -31,5 +36,23 @@ export function createV2BlocksRouter(db: PgStore): express.Router { res.json(response); }) ); + + router.get( + '/:height_or_hash', + cacheHandler, + asyncHandler(async (req, res) => { + if (!validRequestParams(req, res, CompiledBurnBlockParams)) return; + const params = req.params as BurnBlockParams; + + const block = await db.getV2Block(params); + if (!block) { + res.status(404).json({ errors: 'Not found' }); + return; + } + setETagCacheHeaders(res); + res.json(parseDbNakamotoBlock(block)); + }) + ); + return router; } diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 22d4d136..eabc8d8b 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -465,6 +465,7 @@ export class PgStore extends BasePgStore { /** * Returns Block information with metadata, including accepted and streamed microblocks hash * @returns `BlocksWithMetadata` object including list of Blocks with metadata and total count. + * @deprecated use `getV2Blocks` */ async getBlocksWithMetadata({ limit, @@ -596,14 +597,7 @@ export class PgStore extends BasePgStore { : undefined; // Obtain blocks and transaction counts in the same query. - const blocksQuery = await sql< - (BlockQueryResult & { - tx_ids: string; - microblocks_accepted: string; - microblocks_streamed: string; - total: number; - })[] - >` + const blocksQuery = await sql<(BlockQueryResult & { tx_ids: string; total: number })[]>` WITH block_count AS ( ${ 'burn_block_hash' in args @@ -656,6 +650,39 @@ export class PgStore extends BasePgStore { }); } + async getV2Block(args: BurnBlockParams): Promise { + return await this.sqlTransaction(async sql => { + const filter = + args.height_or_hash === 'latest' + ? sql`index_block_hash = (SELECT index_block_hash FROM blocks WHERE canonical = TRUE ORDER BY block_height DESC LIMIT 1)` + : CompiledBurnBlockHashParam.Check(args.height_or_hash) + ? sql`( + block_hash = ${normalizeHashString(args.height_or_hash)} + OR index_block_hash = ${normalizeHashString(args.height_or_hash)} + )` + : sql`block_height = ${args.height_or_hash}`; + const blockQuery = await sql<(BlockQueryResult & { tx_ids: string })[]>` + SELECT + ${sql(BLOCK_COLUMNS)}, + ( + SELECT STRING_AGG(tx_id,',') + FROM txs + WHERE index_block_hash = blocks.index_block_hash + AND canonical = true + AND microblock_canonical = true + ) AS tx_ids + FROM blocks + WHERE canonical = true AND ${filter} + LIMIT 1 + `; + if (blockQuery.count > 0) + return { + ...parseBlockQueryResult(blockQuery[0]), + tx_ids: blockQuery[0].tx_ids ? blockQuery[0].tx_ids.split(',') : [], + }; + }); + } + async getBlockTxs(indexBlockHash: string) { const result = await this.sql<{ tx_id: string; tx_index: number }[]>` SELECT tx_id, tx_index diff --git a/src/tests/block-tests.ts b/src/tests/block-tests.ts index 5fd9a65a..3bef070e 100644 --- a/src/tests/block-tests.ts +++ b/src/tests/block-tests.ts @@ -731,4 +731,72 @@ describe('block tests', () => { fetch = await supertest(api.server).get(`/extended/v2/blocks?burn_block_hash=testvalue`); expect(fetch.status).toBe(400); }); + + test('blocks v2 retrieved by hash or height', async () => { + for (let i = 1; i < 6; i++) { + const block = new TestBlockBuilder({ + block_height: i, + block_hash: `0x000000000000000000000000000000000000000000000000000000000000000${i}`, + index_block_hash: `0x000000000000000000000000000000000000000000000000000000000000011${i}`, + parent_index_block_hash: `0x000000000000000000000000000000000000000000000000000000000000011${ + i - 1 + }`, + parent_block_hash: `0x000000000000000000000000000000000000000000000000000000000000000${ + i - 1 + }`, + burn_block_height: 700000, + burn_block_hash: '0x00000000000000000001e2ee7f0c6bd5361b5e7afd76156ca7d6f524ee5ca3d8', + }) + .addTx({ tx_id: `0x000${i}` }) + .build(); + await db.update(block); + } + + // Get latest + const block5 = { + burn_block_hash: '0x00000000000000000001e2ee7f0c6bd5361b5e7afd76156ca7d6f524ee5ca3d8', + burn_block_height: 700000, + burn_block_time: 94869286, + burn_block_time_iso: '1973-01-03T00:34:46.000Z', + canonical: true, + execution_cost_read_count: 0, + execution_cost_read_length: 0, + execution_cost_runtime: 0, + execution_cost_write_count: 0, + execution_cost_write_length: 0, + hash: '0x0000000000000000000000000000000000000000000000000000000000000005', + height: 5, + index_block_hash: '0x0000000000000000000000000000000000000000000000000000000000000115', + miner_txid: '0x4321', + parent_block_hash: '0x0000000000000000000000000000000000000000000000000000000000000004', + parent_index_block_hash: '0x0000000000000000000000000000000000000000000000000000000000000114', + txs: ['0x0005'], + }; + let fetch = await supertest(api.server).get(`/extended/v2/blocks/latest`); + let json = JSON.parse(fetch.text); + expect(fetch.status).toBe(200); + expect(json).toStrictEqual(block5); + + // Get by height + fetch = await supertest(api.server).get(`/extended/v2/blocks/5`); + json = JSON.parse(fetch.text); + expect(fetch.status).toBe(200); + expect(json).toStrictEqual(block5); + + // Get by hash + fetch = await supertest(api.server).get( + `/extended/v2/blocks/0x0000000000000000000000000000000000000000000000000000000000000005` + ); + json = JSON.parse(fetch.text); + expect(fetch.status).toBe(200); + expect(json).toStrictEqual(block5); + + // Get by index block hash + fetch = await supertest(api.server).get( + `/extended/v2/blocks/0x0000000000000000000000000000000000000000000000000000000000000115` + ); + json = JSON.parse(fetch.text); + expect(fetch.status).toBe(200); + expect(json).toStrictEqual(block5); + }); });