Skip to content
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
6 changes: 1 addition & 5 deletions docs/entities/blocks/nakamoto-block.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@
"burn_block_hash": "0xb154c008df2101023a6d0d54986b3964cee58119eed14f5bed98e15678e18fe2",
"burn_block_height": 654439,
"miner_txid": "0xd7d56070277ccd87b42acf0c91f915dd181f9db4cf878a4e95518bc397c240cc",
"txs": [
"0x4262db117659d1ca9406970c8f44ffd3d8f11f8e18c591d2e3960f4070107754",
"0x383632cd3b5464dffb684082750fcfaddd1f52625bbb9f884ed8f45d2b1f0547",
"0xc99fe597e44b8bd15a50eec660c6e679a7144a5a8553d214b9d5f1406d278c22"
],
"tx_count": 3,
"execution_cost_read_count": 2477,
"execution_cost_read_length": 1659409,
"execution_cost_runtime": 2520952000,
Expand Down
11 changes: 4 additions & 7 deletions docs/entities/blocks/nakamoto-block.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"parent_block_hash",
"parent_index_block_hash",
"txs",
"tx_count",
"burn_block_time",
"burn_block_time_iso",
"burn_block_hash",
Expand Down Expand Up @@ -67,13 +68,9 @@
"type": "string",
"description": "Anchor chain transaction ID"
},
"txs": {
"type": "array",
"description": "List of transactions included in the block",
"items": {
"type": "string",
"description": "Transaction ID"
}
"tx_count": {
"type": "integer",
"description": "Number of transactions included in the block"
},
"execution_cost_read_count": {
"type": "integer",
Expand Down
4 changes: 2 additions & 2 deletions docs/generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1366,9 +1366,9 @@ export interface NakamotoBlock {
*/
miner_txid: string;
/**
* List of transactions included in the block
* Number of transactions included in the block
*/
txs: string[];
tx_count: number;
/**
* Execution cost read count.
*/
Expand Down
29 changes: 29 additions & 0 deletions docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,35 @@ paths:
example:
$ref: ./entities/blocks/nakamoto-block.example.json

/extended/v2/blocks/{height_or_hash}/transactions:
get:
summary: Get block transactions
description: |
Retrieves transactions confirmed in a single block
tags:
- Blocks
operationId: get_block_transactions
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: List of transactions
content:
application/json:
schema:
$ref: ./api/transaction/get-transactions.schema.json
example:
$ref: ./api/transaction/get-transactions.example.json

/extended/v1/block:
get:
summary: Get recent blocks
Expand Down
26 changes: 26 additions & 0 deletions migrations/1702913457527_block-tx-count.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/* eslint-disable camelcase */

exports.shorthands = undefined;

exports.up = pgm => {
pgm.addColumns('blocks', {
tx_count: {
type: 'int',
default: 1,
},
});
pgm.sql(`
UPDATE blocks SET tx_count = (
SELECT COUNT(*)::int
FROM txs
WHERE index_block_hash = blocks.index_block_hash
AND canonical = TRUE
AND microblock_canonical = TRUE
)
`);
pgm.alterColumn('blocks', 'tx_count', { notNull: true });
};

exports.down = pgm => {
pgm.dropColumn('blocks', ['tx_count']);
};
53 changes: 47 additions & 6 deletions src/api/routes/v2/blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,20 @@ import {
setETagCacheHeaders,
} from '../../../api/controllers/cache-controller';
import { asyncHandler } from '../../async-handler';
import { NakamotoBlockListResponse } from 'docs/generated';
import { NakamotoBlockListResponse, TransactionResults } from 'docs/generated';
import {
BlocksQueryParams,
BurnBlockParams,
BlockParams,
CompiledBlocksQueryParams,
CompiledBurnBlockParams,
CompiledBlockParams,
CompiledTransactionPaginationQueryParams,
TransactionPaginationQueryParams,
validRequestQuery,
validRequestParams,
} from './schemas';
import { parseDbNakamotoBlock, validRequestParams, validRequestQuery } from './helpers';
import { parseDbNakamotoBlock } from './helpers';
import { InvalidRequestError } from '../../../errors';
import { parseDbTx } from '../../../api/controllers/db-controller';

export function createV2BlocksRouter(db: PgStore): express.Router {
const router = express.Router();
Expand Down Expand Up @@ -41,8 +47,8 @@ export function createV2BlocksRouter(db: PgStore): express.Router {
'/:height_or_hash',
cacheHandler,
asyncHandler(async (req, res) => {
if (!validRequestParams(req, res, CompiledBurnBlockParams)) return;
const params = req.params as BurnBlockParams;
if (!validRequestParams(req, res, CompiledBlockParams)) return;
const params = req.params as BlockParams;

const block = await db.getV2Block(params);
if (!block) {
Expand All @@ -54,5 +60,40 @@ export function createV2BlocksRouter(db: PgStore): express.Router {
})
);

router.get(
'/:height_or_hash/transactions',
cacheHandler,
asyncHandler(async (req, res) => {
if (
!validRequestParams(req, res, CompiledBlockParams) ||
!validRequestQuery(req, res, CompiledTransactionPaginationQueryParams)
)
return;
const params = req.params as BlockParams;
const query = req.query as TransactionPaginationQueryParams;

try {
const { limit, offset, results, total } = await db.getV2BlockTransactions({
...params,
...query,
});
const response: TransactionResults = {
limit,
offset,
total,
results: results.map(r => parseDbTx(r)),
};
setETagCacheHeaders(res);
res.json(response);
} catch (error) {
if (error instanceof InvalidRequestError) {
res.status(404).json({ errors: error.message });
return;
}
throw error;
}
})
);

return router;
}
16 changes: 9 additions & 7 deletions src/api/routes/v2/burn-blocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { BurnBlockListResponse } from '@stacks/stacks-blockchain-api-types';
import { getETagCacheHandler, setETagCacheHeaders } from '../../controllers/cache-controller';
import { asyncHandler } from '../../async-handler';
import { PgStore } from '../../../datastore/pg-store';
import { parseDbBurnBlock, validRequestParams, validRequestQuery } from './helpers';
import { parseDbBurnBlock } from './helpers';
import {
BlockPaginationQueryParams,
BurnBlockParams,
CompiledBlockPaginationParams,
CompiledBurnBlockParams,
BlockParams,
CompiledBlockPaginationQueryParams,
CompiledBlockParams,
validRequestParams,
validRequestQuery,
} from './schemas';

export function createV2BurnBlocksRouter(db: PgStore): express.Router {
Expand All @@ -19,7 +21,7 @@ export function createV2BurnBlocksRouter(db: PgStore): express.Router {
'/',
cacheHandler,
asyncHandler(async (req, res) => {
if (!validRequestQuery(req, res, CompiledBlockPaginationParams)) return;
if (!validRequestQuery(req, res, CompiledBlockPaginationQueryParams)) return;
const query = req.query as BlockPaginationQueryParams;

const { limit, offset, results, total } = await db.getBurnBlocks(query);
Expand All @@ -38,8 +40,8 @@ export function createV2BurnBlocksRouter(db: PgStore): express.Router {
'/:height_or_hash',
cacheHandler,
asyncHandler(async (req, res) => {
if (!validRequestParams(req, res, CompiledBurnBlockParams)) return;
const params = req.params as BurnBlockParams;
if (!validRequestParams(req, res, CompiledBlockParams)) return;
const params = req.params as BlockParams;

const block = await db.getBurnBlock(params);
if (!block) {
Expand Down
49 changes: 3 additions & 46 deletions src/api/routes/v2/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,8 @@
import { BurnBlock, NakamotoBlock } from 'docs/generated';
import { BlockWithTransactionIds, DbBurnBlock } from '../../../datastore/common';
import { DbBlock, DbBurnBlock } from '../../../datastore/common';
import { unixEpochToIso } from '../../../helpers';
import { TypeCheck } from '@sinclair/typebox/compiler';
import { Request, Response } from 'express';
import { TSchema } from '@sinclair/typebox';

/**
* Validate request query parameters with a TypeBox compiled schema
* @param req - Request
* @param res - Response
* @param compiledType - TypeBox compiled schema
* @returns boolean
*/
export function validRequestQuery(
req: Request,
res: Response,
compiledType: TypeCheck<TSchema>
): boolean {
if (!compiledType.Check(req.query)) {
// TODO: Return a more user-friendly error
res.status(400).json({ errors: [...compiledType.Errors(req.query)] });
return false;
}
return true;
}

/**
* Validate request path parameters with a TypeBox compiled schema
* @param req - Request
* @param res - Response
* @param compiledType - TypeBox compiled schema
* @returns boolean
*/
export function validRequestParams(
req: Request,
res: Response,
compiledType: TypeCheck<TSchema>
): boolean {
if (!compiledType.Check(req.params)) {
// TODO: Return a more user-friendly error
res.status(400).json({ errors: [...compiledType.Errors(req.params)] });
return false;
}
return true;
}

export function parseDbNakamotoBlock(block: BlockWithTransactionIds): NakamotoBlock {
export function parseDbNakamotoBlock(block: DbBlock): NakamotoBlock {
const apiBlock: NakamotoBlock = {
canonical: block.canonical,
height: block.block_height,
Expand All @@ -58,7 +15,7 @@ export function parseDbNakamotoBlock(block: BlockWithTransactionIds): NakamotoBl
burn_block_hash: block.burn_block_hash,
burn_block_height: block.burn_block_height,
miner_txid: block.miner_txid,
txs: [...block.tx_ids],
tx_count: block.tx_count,
execution_cost_read_count: block.execution_cost_read_count,
execution_cost_read_length: block.execution_cost_read_length,
execution_cost_runtime: block.execution_cost_runtime,
Expand Down
Loading