diff --git a/docs/entities/blocks/nakamoto-block.example.json b/docs/entities/blocks/nakamoto-block.example.json index 67da2c54f3..f2cd985407 100644 --- a/docs/entities/blocks/nakamoto-block.example.json +++ b/docs/entities/blocks/nakamoto-block.example.json @@ -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, diff --git a/docs/entities/blocks/nakamoto-block.schema.json b/docs/entities/blocks/nakamoto-block.schema.json index 96395d87e8..41f9a38ced 100644 --- a/docs/entities/blocks/nakamoto-block.schema.json +++ b/docs/entities/blocks/nakamoto-block.schema.json @@ -11,6 +11,7 @@ "parent_block_hash", "parent_index_block_hash", "txs", + "tx_count", "burn_block_time", "burn_block_time_iso", "burn_block_hash", @@ -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", diff --git a/docs/generated.d.ts b/docs/generated.d.ts index 97538fa4c5..933fe0dc8d 100644 --- a/docs/generated.d.ts +++ b/docs/generated.d.ts @@ -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. */ diff --git a/docs/openapi.yaml b/docs/openapi.yaml index d79d3ed633..1a2fadfdb8 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -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 diff --git a/migrations/1702913457527_block-tx-count.js b/migrations/1702913457527_block-tx-count.js new file mode 100644 index 0000000000..3410704e9e --- /dev/null +++ b/migrations/1702913457527_block-tx-count.js @@ -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']); +}; diff --git a/src/api/routes/v2/blocks.ts b/src/api/routes/v2/blocks.ts index 652fc19766..9a78c302e0 100644 --- a/src/api/routes/v2/blocks.ts +++ b/src/api/routes/v2/blocks.ts @@ -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(); @@ -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) { @@ -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; } diff --git a/src/api/routes/v2/burn-blocks.ts b/src/api/routes/v2/burn-blocks.ts index 5b724fdd3a..1f2d5c323e 100644 --- a/src/api/routes/v2/burn-blocks.ts +++ b/src/api/routes/v2/burn-blocks.ts @@ -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 { @@ -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); @@ -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) { diff --git a/src/api/routes/v2/helpers.ts b/src/api/routes/v2/helpers.ts index 413192c9ab..41d0ce2a2e 100644 --- a/src/api/routes/v2/helpers.ts +++ b/src/api/routes/v2/helpers.ts @@ -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 -): 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 -): 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, @@ -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, diff --git a/src/api/routes/v2/schemas.ts b/src/api/routes/v2/schemas.ts index 3a80a9071e..50d3f12d69 100644 --- a/src/api/routes/v2/schemas.ts +++ b/src/api/routes/v2/schemas.ts @@ -1,6 +1,50 @@ import { Type, Static, TSchema } from '@sinclair/typebox'; import { TypeCompiler } from '@sinclair/typebox/compiler'; import { ResourceType, pagingQueryLimits } from '../../../api/pagination'; +import { Request, Response } from 'express'; +import * as Ajv from 'ajv'; + +const ajv = new Ajv({ coerceTypes: true }); + +/** + * Validate request query parameters with a TypeBox compiled schema + * @param req - Request + * @param res - Response + * @param compiledType - Ajv compiled schema + * @returns boolean + */ +export function validRequestQuery( + req: Request, + res: Response, + compiledType: Ajv.ValidateFunction +): boolean { + if (!compiledType(req.query)) { + // TODO: Return a more user-friendly error + res.status(400).json({ errors: compiledType.errors }); + return false; + } + return true; +} + +/** + * Validate request path parameters with a TypeBox compiled schema + * @param req - Request + * @param res - Response + * @param compiledType - Ajv compiled schema + * @returns boolean + */ +export function validRequestParams( + req: Request, + res: Response, + compiledType: Ajv.ValidateFunction +): boolean { + if (!compiledType(req.params)) { + // TODO: Return a more user-friendly error + res.status(400).json({ errors: compiledType.errors }); + return false; + } + return true; +} // ========================== // Parameters @@ -20,21 +64,26 @@ export const BlockLimitParamSchema = Type.Integer({ description: 'Blocks per page', }); +export const TransactionLimitParamSchema = Type.Integer({ + minimum: 1, + maximum: pagingQueryLimits[ResourceType.Tx].maxLimit, + default: pagingQueryLimits[ResourceType.Tx].defaultLimit, + title: 'Transaction limit', + description: 'Transactions per page', +}); + const BurnBlockHashParamSchema = Type.RegExp(/^(0x)?[a-fA-F0-9]{64}$/i, { title: 'Burn block hash', description: 'Burn block hash', examples: ['0000000000000000000452773967cdd62297137cdaf79950c5e8bb0c62075133'], }); -type BurnBlockHashParam = Static; -export const CompiledBurnBlockHashParam = TypeCompiler.Compile(BurnBlockHashParamSchema); +export const CompiledBurnBlockHashParam = ajv.compile(BurnBlockHashParamSchema); const BurnBlockHeightParamSchema = Type.RegExp(/^[0-9]+$/, { title: 'Burn block height', description: 'Burn block height', examples: ['777678'], }); -type BurnBlockHeightParam = Static; -const CompiledBurnBlockHeightParam = TypeCompiler.Compile(BurnBlockHeightParamSchema); // ========================== // Query and path params @@ -52,7 +101,17 @@ const PaginationQueryParamsSchema = (t: T) => const BlockPaginationQueryParamsSchema = PaginationQueryParamsSchema(BlockLimitParamSchema); export type BlockPaginationQueryParams = Static; -export const CompiledBlockPaginationParams = TypeCompiler.Compile(BlockPaginationQueryParamsSchema); +export const CompiledBlockPaginationQueryParams = ajv.compile(BlockPaginationQueryParamsSchema); + +const TransactionPaginationQueryParamsSchema = PaginationQueryParamsSchema( + TransactionLimitParamSchema +); +export type TransactionPaginationQueryParams = Static< + typeof TransactionPaginationQueryParamsSchema +>; +export const CompiledTransactionPaginationQueryParams = ajv.compile( + TransactionPaginationQueryParamsSchema +); const BlocksQueryParamsSchema = Type.Union([ BlockPaginationQueryParamsSchema, @@ -76,9 +135,9 @@ const BlocksQueryParamsSchema = Type.Union([ ), ]); export type BlocksQueryParams = Static; -export const CompiledBlocksQueryParams = TypeCompiler.Compile(BlocksQueryParamsSchema); +export const CompiledBlocksQueryParams = ajv.compile(BlocksQueryParamsSchema); -const BurnBlockParamsSchema = Type.Object( +const BlockParamsSchema = Type.Object( { height_or_hash: Type.Union([ Type.Literal('latest'), @@ -88,5 +147,5 @@ const BurnBlockParamsSchema = Type.Object( }, { additionalProperties: false } ); -export type BurnBlockParams = Static; -export const CompiledBurnBlockParams = TypeCompiler.Compile(BurnBlockParamsSchema); +export type BlockParams = Static; +export const CompiledBlockParams = ajv.compile(BlockParamsSchema); diff --git a/src/datastore/common.ts b/src/datastore/common.ts index 773051bbac..981fa6a7ef 100644 --- a/src/datastore/common.ts +++ b/src/datastore/common.ts @@ -23,6 +23,7 @@ export interface DbBlock { execution_cost_runtime: number; execution_cost_write_count: number; execution_cost_write_length: number; + tx_count: number; } /** An interface representing the microblock data that can be constructed _only_ from the /new_microblocks payload */ @@ -571,18 +572,6 @@ export interface NftEventWithTxMetadata { tx?: DbTx; } -export interface AddressNftEventIdentifier { - sender: string; - recipient: string; - asset_identifier: string; - value: string; - block_height: number; - tx_id: string; - event_index: number; - tx_index: number; - asset_event_type_id: number; -} - export interface DataStoreBlockUpdateData { block: DbBlock; microblocks: DbMicroblock[]; @@ -806,6 +795,7 @@ export interface BlockQueryResult { execution_cost_runtime: string; execution_cost_write_count: string; execution_cost_write_length: string; + tx_count: number; } export interface MicroblockQueryResult { @@ -1037,10 +1027,6 @@ export type DbPaginatedResult = { results: T[]; }; -export type BlockWithTransactionIds = DbBlock & { - tx_ids: string[]; -}; - export interface BlocksWithMetadata { results: { block: DbBlock; @@ -1174,6 +1160,7 @@ export interface BlockInsertValues { execution_cost_runtime: number; execution_cost_write_count: number; execution_cost_write_length: number; + tx_count: number; } export interface MicroblockInsertValues { diff --git a/src/datastore/helpers.ts b/src/datastore/helpers.ts index af92983f71..e35cabe9ee 100644 --- a/src/datastore/helpers.ts +++ b/src/datastore/helpers.ts @@ -183,6 +183,7 @@ export const BLOCK_COLUMNS = [ 'execution_cost_runtime', 'execution_cost_write_count', 'execution_cost_write_length', + 'tx_count', ]; export const MICROBLOCK_COLUMNS = [ @@ -464,6 +465,7 @@ export function parseBlockQueryResult(row: BlockQueryResult): DbBlock { execution_cost_runtime: Number.parseInt(row.execution_cost_runtime), execution_cost_write_count: Number.parseInt(row.execution_cost_write_count), execution_cost_write_length: Number.parseInt(row.execution_cost_write_length), + tx_count: row.tx_count, }; return block; } diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index 9a3539a4c2..22ed88617a 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -19,10 +19,8 @@ import { } from '../helpers'; import { PgStoreEventEmitter } from './pg-store-event-emitter'; import { - AddressNftEventIdentifier, BlockIdentifier, BlockQueryResult, - BlockWithTransactionIds, BlocksWithMetadata, ContractTxQueryResult, DbAssetEventTypeId, @@ -73,6 +71,7 @@ import { PoxSyntheticEventTable, DbPoxStacker, DbPoxSyntheticEvent, + TxQueryResult, } from './common'; import { abiColumn, @@ -108,9 +107,12 @@ import { BlockLimitParamSchema, BlockPaginationQueryParams, BlocksQueryParams, - BurnBlockParams, + BlockParams, + TransactionPaginationQueryParams, + TransactionLimitParamSchema, CompiledBurnBlockHashParam, } from '../api/routes/v2/schemas'; +import { InvalidRequestError, InvalidRequestErrorType } from '../errors'; export const MIGRATIONS_DIR = path.join(REPO_DIR, 'migrations'); @@ -389,7 +391,7 @@ export class PgStore extends BasePgStore { async getCurrentBlockInternal(sql: PgSqlClient): Promise> { const result = await sql` - SELECT ${sql(BLOCK_COLUMNS)} + SELECT ${sql(BLOCK_COLUMNS.map(c => `b.${c}`))} FROM blocks b INNER JOIN chain_tip t USING (index_block_hash, block_hash, block_height, burn_block_height) LIMIT 1 @@ -436,12 +438,12 @@ export class PgStore extends BasePgStore { }); } - async getBurnBlock(args: BurnBlockParams): Promise { + async getBurnBlock(args: BlockParams): Promise { return await this.sqlTransaction(async sql => { const filter = args.height_or_hash === 'latest' ? sql`burn_block_hash = (SELECT burn_block_hash FROM blocks WHERE canonical = TRUE ORDER BY block_height DESC LIMIT 1)` - : CompiledBurnBlockHashParam.Check(args.height_or_hash) + : CompiledBurnBlockHashParam(args.height_or_hash) ? sql`burn_block_hash = ${args.height_or_hash}` : sql`burn_block_height = ${args.height_or_hash}`; const blockQuery = await sql` @@ -573,9 +575,9 @@ export class PgStore extends BasePgStore { /** * Returns Block information with transaction IDs - * @returns Paginated `BlockWithTransactionIds` array + * @returns Paginated `DbBlock` array */ - async getV2Blocks(args: BlocksQueryParams): Promise> { + async getV2Blocks(args: BlocksQueryParams): Promise> { return await this.sqlTransaction(async sql => { const limit = args.limit ?? BlockLimitParamSchema.default; const offset = args.offset ?? 0; @@ -597,7 +599,7 @@ export class PgStore extends BasePgStore { : undefined; // Obtain blocks and transaction counts in the same query. - const blocksQuery = await sql<(BlockQueryResult & { tx_ids: string; total: number })[]>` + const blocksQuery = await sql<(BlockQueryResult & { total: number })[]>` WITH block_count AS ( ${ 'burn_block_hash' in args @@ -609,13 +611,6 @@ export class PgStore extends BasePgStore { ) 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, (SELECT count FROM block_count)::int AS total FROM blocks WHERE canonical = true @@ -637,10 +632,7 @@ export class PgStore extends BasePgStore { results: [], total: 0, }; - const blocks = blocksQuery.map(b => ({ - ...parseBlockQueryResult(b), - tx_ids: b.tx_ids ? b.tx_ids.split(',') : [], - })); + const blocks = blocksQuery.map(b => parseBlockQueryResult(b)); return { limit, offset, @@ -650,39 +642,77 @@ export class PgStore extends BasePgStore { }); } - async getV2Block(args: BurnBlockParams): Promise { + async getV2Block(args: BlockParams): 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) + : CompiledBurnBlockHashParam(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 + const blockQuery = await sql` + SELECT ${sql(BLOCK_COLUMNS)} FROM blocks WHERE canonical = true AND ${filter} LIMIT 1 `; - if (blockQuery.count > 0) + if (blockQuery.count > 0) return parseBlockQueryResult(blockQuery[0]); + }); + } + + async getV2BlockTransactions( + args: BlockParams & TransactionPaginationQueryParams + ): Promise> { + return await this.sqlTransaction(async sql => { + const limit = args.limit ?? TransactionLimitParamSchema.default; + const offset = args.offset ?? 0; + 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(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 blockCheck = await sql`SELECT index_block_hash FROM blocks WHERE ${filter} LIMIT 1`; + if (blockCheck.count === 0) + throw new InvalidRequestError(`Block not found`, InvalidRequestErrorType.invalid_param); + const txsQuery = await sql<(TxQueryResult & { total: number })[]>` + WITH tx_count AS ( + SELECT tx_count AS total FROM blocks WHERE canonical = TRUE AND ${filter} + ) + SELECT ${sql(TX_COLUMNS)}, (SELECT total FROM tx_count)::int AS total + FROM txs + WHERE canonical = true + AND microblock_canonical = true + AND ${filter} + ORDER BY microblock_sequence ASC, tx_index ASC + LIMIT ${limit} + OFFSET ${offset} + `; + if (txsQuery.count === 0) return { - ...parseBlockQueryResult(blockQuery[0]), - tx_ids: blockQuery[0].tx_ids ? blockQuery[0].tx_ids.split(',') : [], + limit, + offset, + results: [], + total: 0, }; + return { + limit, + offset, + results: txsQuery.map(t => parseTxQueryResult(t)), + total: txsQuery[0].total, + }; }); } + /** + * @deprecated Only used in tests + */ async getBlockTxs(indexBlockHash: string) { const result = await this.sql<{ tx_id: string; tx_index: number }[]>` SELECT tx_id, tx_index diff --git a/src/datastore/pg-write-store.ts b/src/datastore/pg-write-store.ts index 3da53808d5..1eac2ac2fc 100644 --- a/src/datastore/pg-write-store.ts +++ b/src/datastore/pg-write-store.ts @@ -444,6 +444,7 @@ export class PgWriteStore extends PgStore { execution_cost_runtime: block.execution_cost_runtime, execution_cost_write_count: block.execution_cost_write_count, execution_cost_write_length: block.execution_cost_write_length, + tx_count: block.tx_count, }; const result = await sql` INSERT INTO blocks ${sql(values)} @@ -2791,6 +2792,7 @@ export class PgWriteStore extends PgStore { execution_cost_runtime: block.execution_cost_runtime, execution_cost_write_count: block.execution_cost_write_count, execution_cost_write_length: block.execution_cost_write_length, + tx_count: block.tx_count, })); await sql` INSERT INTO blocks ${sql(values)} diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index 7f123f7a29..56b6b648d9 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -259,6 +259,7 @@ async function handleBlockMessage( execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: msg.transactions.length, }; logger.debug(`Received block ${msg.block_hash} (${msg.block_height}) from node`, dbBlock); @@ -1064,6 +1065,7 @@ export function parseNewBlockMessage(chainId: ChainID, msg: CoreNodeBlockMessage execution_cost_runtime: totalCost.execution_cost_runtime, execution_cost_write_count: totalCost.execution_cost_write_count, execution_cost_write_length: totalCost.execution_cost_write_length, + tx_count: msg.transactions.length, }; const dbMinerRewards: DbMinerReward[] = []; diff --git a/src/test-utils/test-builders.ts b/src/test-utils/test-builders.ts index 98f2db2a63..0fd51fc2ed 100644 --- a/src/test-utils/test-builders.ts +++ b/src/test-utils/test-builders.ts @@ -122,6 +122,7 @@ function testBlock(args?: TestBlockArgs): DbBlock { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; } @@ -743,7 +744,9 @@ export class TestBlockBuilder { } build(): DataStoreBlockUpdateData { - return this.data; + const data = this.data; + data.block.tx_count = this.txIndex + 1; + return data; } } diff --git a/src/tests/address-tests.ts b/src/tests/address-tests.ts index 00db2d577a..64b197c841 100644 --- a/src/tests/address-tests.ts +++ b/src/tests/address-tests.ts @@ -89,6 +89,7 @@ describe('address tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; let indexIdIndex = 0; const createStxTx = ( @@ -867,6 +868,7 @@ describe('address tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; let indexIdIndex = 0; @@ -2038,6 +2040,7 @@ describe('address tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const txBuilder = await makeContractCall({ contractAddress: 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y', diff --git a/src/tests/block-tests.ts b/src/tests/block-tests.ts index 3bef070e7c..44616f58d8 100644 --- a/src/tests/block-tests.ts +++ b/src/tests/block-tests.ts @@ -83,6 +83,7 @@ describe('block tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, block); const tx: DbTxRaw = { @@ -519,6 +520,7 @@ describe('block tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const dbTx1: DbTxRaw = { ...dbBlock, @@ -673,7 +675,7 @@ describe('block tests', () => { miner_txid: '0x4321', parent_block_hash: '0x0004', parent_index_block_hash: '0x0004', - txs: ['0x0005'], + tx_count: 1, }; let fetch = await supertest(api.server).get( `/extended/v2/blocks?burn_block_hash=00000000000000000001e2ee7f0c6bd5361b5e7afd76156ca7d6f524ee5ca3d8` @@ -708,7 +710,7 @@ describe('block tests', () => { miner_txid: '0x4321', parent_block_hash: '0x0007', parent_index_block_hash: '0x0007', - txs: ['0x0018'], + tx_count: 1, }; fetch = await supertest(api.server).get(`/extended/v2/blocks?burn_block_hash=latest`); json = JSON.parse(fetch.text); @@ -770,7 +772,7 @@ describe('block tests', () => { miner_txid: '0x4321', parent_block_hash: '0x0000000000000000000000000000000000000000000000000000000000000004', parent_index_block_hash: '0x0000000000000000000000000000000000000000000000000000000000000114', - txs: ['0x0005'], + tx_count: 1, }; let fetch = await supertest(api.server).get(`/extended/v2/blocks/latest`); let json = JSON.parse(fetch.text); diff --git a/src/tests/cache-control-tests.ts b/src/tests/cache-control-tests.ts index e27211ac87..12cb6fb14c 100644 --- a/src/tests/cache-control-tests.ts +++ b/src/tests/cache-control-tests.ts @@ -95,6 +95,7 @@ describe('cache-control tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTxRaw = { tx_id: '0x1234', diff --git a/src/tests/datastore-tests.ts b/src/tests/datastore-tests.ts index 34804796e0..ce2617c73e 100644 --- a/src/tests/datastore-tests.ts +++ b/src/tests/datastore-tests.ts @@ -273,6 +273,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTxRaw = { tx_id: '0x1234', @@ -436,6 +437,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTxRaw = { tx_id: '0x1234', @@ -605,6 +607,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, block); const blockQuery = await db.getBlock({ hash: block.block_hash }); @@ -670,6 +673,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; let indexIdIndex = 0; @@ -929,6 +933,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const txs1 = [ createStxTx('addrA', 'addrB', 100, dbBlock1), @@ -1001,6 +1006,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1: DbTxRaw = { tx_id: '0x1234', @@ -1966,6 +1972,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTx = { tx_id: '0x1234', @@ -2045,6 +2052,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTx = { tx_id: '0x421234', @@ -2129,6 +2137,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTx = { tx_id: '0x421234', @@ -2221,6 +2230,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTxRaw = { tx_id: '0x421234', @@ -2355,6 +2365,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTx = { tx_id: '0x421234', @@ -2439,6 +2450,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTx = { tx_id: '0x421234', @@ -2522,6 +2534,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTx = { tx_id: '0x421234', @@ -2604,6 +2617,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, dbBlock); @@ -2673,6 +2687,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1: DbTx = { tx_id: '0x421234', @@ -3042,6 +3057,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block2: DbBlock = { block_hash: '0x22', @@ -3061,6 +3077,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block3: DbBlock = { block_hash: '0x33', @@ -3080,6 +3097,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block3B: DbBlock = { ...block3, @@ -3105,6 +3123,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block4: DbBlock = { block_hash: '0x44', @@ -3124,6 +3143,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block5: DbBlock = { block_hash: '0x55', @@ -3143,6 +3163,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block6: DbBlock = { block_hash: '0x66', @@ -3162,6 +3183,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1Mempool: DbMempoolTxRaw = { @@ -3349,6 +3371,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block2: DbBlock = { block_hash: '0x22', @@ -3368,6 +3391,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block3: DbBlock = { block_hash: '0x33', @@ -3387,6 +3411,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block3B: DbBlock = { ...block3, @@ -3412,6 +3437,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const minerReward1: DbMinerReward = { @@ -3540,6 +3566,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const reorgResult = await db.handleReorg(client, block5, 0); @@ -3611,6 +3638,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const block2: DbBlock = { block_hash: '0x22', @@ -3630,6 +3658,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const minerReward1: DbMinerReward = { @@ -3903,6 +3932,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.update({ block: block3, microblocks: [], minerRewards: [], txs: [] }); @@ -3924,6 +3954,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx3: DbTxRaw = { tx_id: '0x03', @@ -4129,6 +4160,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.update({ block: block3b, microblocks: [], minerRewards: [], txs: [] }); const blockQuery2 = await db.getBlock({ hash: block3b.block_hash }); @@ -4166,6 +4198,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.update({ block: block4b, microblocks: [], minerRewards: [], txs: [] }); @@ -4262,6 +4295,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1: DbTxRaw = { tx_id: '0x421234', @@ -4344,6 +4378,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1: DbTxRaw = { tx_id: '0x421234', @@ -4425,6 +4460,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1: DbTxRaw = { tx_id: '0x421234', @@ -4577,6 +4613,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.update({ block: dbBlock, @@ -4636,6 +4673,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.update({ block: dbBlock, @@ -4696,6 +4734,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.update({ block: dbBlock, @@ -4756,6 +4795,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, block); const blockQuery = await db.getBlock({ hash: block.block_hash }); @@ -4859,6 +4899,7 @@ describe('postgres datastore', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, block); const blockQuery = await db.getBlock({ hash: block.block_hash }); diff --git a/src/tests/mempool-tests.ts b/src/tests/mempool-tests.ts index af8805cbfb..fd33072c2d 100644 --- a/src/tests/mempool-tests.ts +++ b/src/tests/mempool-tests.ts @@ -500,6 +500,7 @@ describe('mempool tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const dbTx1: DbTxRaw = { ...mempoolTx1, @@ -1190,6 +1191,7 @@ describe('mempool tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, dbBlock); const senderAddress = 'SP25YGP221F01S9SSCGN114MKDAK9VRK8P3KXGEMB'; @@ -1262,6 +1264,7 @@ describe('mempool tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, dbBlock); const senderAddress = 'SP25YGP221F01S9SSCGN114MKDAK9VRK8P3KXGEMB'; @@ -1480,6 +1483,7 @@ describe('mempool tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const dbBlock2: DbBlock = { block_hash: '0x2123', @@ -1499,6 +1503,7 @@ describe('mempool tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const mempoolTx: DbMempoolTxRaw = { tx_id: txId, diff --git a/src/tests/microblock-tests.ts b/src/tests/microblock-tests.ts index 0ed9e74264..2f6bfe176d 100644 --- a/src/tests/microblock-tests.ts +++ b/src/tests/microblock-tests.ts @@ -282,6 +282,7 @@ describe('microblock tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1: DbTxRaw = { diff --git a/src/tests/other-tests.ts b/src/tests/other-tests.ts index a4a7f9a8a3..d64bf877de 100644 --- a/src/tests/other-tests.ts +++ b/src/tests/other-tests.ts @@ -60,6 +60,7 @@ describe('other tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTxRaw = { tx_id: '0x1234', diff --git a/src/tests/search-tests.ts b/src/tests/search-tests.ts index ea1d5d0b3c..5135630497 100644 --- a/src/tests/search-tests.ts +++ b/src/tests/search-tests.ts @@ -60,6 +60,7 @@ describe('search tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, block); const tx: DbTxRaw = { @@ -267,6 +268,7 @@ describe('search tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx: DbTxRaw = { @@ -597,6 +599,7 @@ describe('search tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.updateBlock(client, block); @@ -1033,6 +1036,7 @@ describe('search tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const stxTx1: DbTxRaw = { diff --git a/src/tests/smart-contract-tests.ts b/src/tests/smart-contract-tests.ts index 4329c827a7..d3b32161af 100644 --- a/src/tests/smart-contract-tests.ts +++ b/src/tests/smart-contract-tests.ts @@ -60,6 +60,7 @@ describe('smart contract tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1: DbTxRaw = { tx_id: '0x421234', @@ -208,6 +209,7 @@ describe('smart contract tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const txId1 = '0x421234'; const smartContract1: DbSmartContract = { @@ -315,6 +317,7 @@ describe('smart contract tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const txId1 = '0x421234'; const smartContract1: DbSmartContract = { @@ -420,6 +423,7 @@ describe('smart contract tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; const tx1: DbTxRaw = { tx_id: '0x421235', diff --git a/src/tests/tx-tests.ts b/src/tests/tx-tests.ts index 445196d51c..4677ae66d6 100644 --- a/src/tests/tx-tests.ts +++ b/src/tests/tx-tests.ts @@ -145,6 +145,7 @@ describe('tx tests', () => { burn_block_hash: '0x0000000000000000000342c6f7e9313ffa6f0a92618edaf86351ca265aee1c7a', burn_block_height: 1, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 1210, execution_cost_read_length: 1919542, @@ -328,6 +329,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -483,6 +485,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -621,6 +624,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -759,6 +763,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -949,6 +954,7 @@ describe('tx tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; await db.update({ block: dbBlock, @@ -1135,6 +1141,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -1333,6 +1340,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -1578,6 +1586,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -1723,6 +1732,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -1868,6 +1878,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -1983,6 +1994,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -2634,6 +2646,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -2879,6 +2892,7 @@ describe('tx tests', () => { burn_block_hash: '0x0000000000000000000342c6f7e9313ffa6f0a92618edaf86351ca265aee1c7a', burn_block_height: 1, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 1210, execution_cost_read_length: 1919542, @@ -3208,6 +3222,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, @@ -3283,6 +3298,110 @@ describe('tx tests', () => { expect(result1.body.results[0].index_block_hash).toBe('0xdeadbeef'); }); + test('fetch transactions from v2 block', async () => { + await db.update( + new TestBlockBuilder({ + block_hash: '0x00000000000000000001e2ee7f0c6bd5361b5e7afd76156ca7d6f524ee5ca3d8', + index_block_hash: '0xdeadbeef', + parent_index_block_hash: '0x00', + parent_block_hash: '0xff0011', + parent_microblock_hash: '', + parent_microblock_sequence: 0, + block_height: 1, + burn_block_time: 94869286, + burn_block_hash: '0x1234', + burn_block_height: 123, + miner_txid: '0x4321', + canonical: true, + }) + .addTx({ + tx_id: '0x1234', + tx_index: 0, + nonce: 0, + type_id: DbTxTypeId.Coinbase, + status: 1, + raw_result: '0x0100000000000000000000000000000001', // u1 + canonical: true, + microblock_canonical: true, + microblock_sequence: I32_MAX, + microblock_hash: '', + fee_rate: 1234n, + sender_address: 'sender-addr', + }) + .addTx({ + tx_id: '0x1235', + tx_index: 1, + nonce: 0, + type_id: DbTxTypeId.Coinbase, + status: 1, + raw_result: '0x0100000000000000000000000000000001', // u1 + canonical: true, + microblock_canonical: true, + microblock_sequence: I32_MAX, + microblock_hash: '', + fee_rate: 1234n, + sender_address: 'sender-addr', + }) + .build() + ); + let result = await supertest(api.server).get( + `/extended/v2/blocks/0x00000000000000000001e2ee7f0c6bd5361b5e7afd76156ca7d6f524ee5ca3d8/transactions?limit=20&offset=0` + ); + expect(result.status).toBe(200); + expect(result.type).toBe('application/json'); + let json = JSON.parse(result.text); + expect(json.total).toBe(2); + expect(json.results[0]).toStrictEqual({ + anchor_mode: 'any', + block_hash: '0x00000000000000000001e2ee7f0c6bd5361b5e7afd76156ca7d6f524ee5ca3d8', + block_height: 1, + burn_block_time: 94869286, + burn_block_time_iso: '1973-01-03T00:34:46.000Z', + canonical: true, + coinbase_payload: { + alt_recipient: null, + data: '0x6869', + }, + event_count: 0, + events: [], + execution_cost_read_count: 0, + execution_cost_read_length: 0, + execution_cost_runtime: 0, + execution_cost_write_count: 0, + execution_cost_write_length: 0, + fee_rate: '1234', + is_unanchored: false, + microblock_canonical: true, + microblock_hash: '0x', + microblock_sequence: 2147483647, + nonce: 0, + parent_block_hash: '0x123456', + parent_burn_block_time: 94869286, + parent_burn_block_time_iso: '1973-01-03T00:34:46.000Z', + post_condition_mode: 'allow', + post_conditions: [], + sender_address: 'sender-addr', + sponsored: false, + tx_id: '0x1234', + tx_index: 0, + tx_result: { + hex: '0x0100000000000000000000000000000001', + repr: 'u1', + }, + tx_status: 'success', + tx_type: 'coinbase', + }); + + // Try a non-existent block + result = await supertest(api.server).get( + `/extended/v2/blocks/0x00000000000000000001e2ee7f0c6bd5361b5e7afd76156ca7d6f524ee999999/transactions?limit=20&offset=0` + ); + expect(result.status).toBe(404); + expect(result.type).toBe('application/json'); + json = JSON.parse(result.text); + expect(json.errors).toBe('Block not found'); + }); + test('fetch transactions from block', async () => { const not_updated_tx_id = '0x1111'; const tx_not_found = { @@ -3300,6 +3419,7 @@ describe('tx tests', () => { burn_block_hash: '0x1234', burn_block_height: 123, miner_txid: '0x4321', + tx_count: 1, canonical: true, execution_cost_read_count: 0, execution_cost_read_length: 0, diff --git a/src/tests/v2-proxy-tests.ts b/src/tests/v2-proxy-tests.ts index da5aad053e..5d2a32e0bf 100644 --- a/src/tests/v2-proxy-tests.ts +++ b/src/tests/v2-proxy-tests.ts @@ -74,6 +74,7 @@ describe('v2-proxy tests', () => { execution_cost_runtime: 0, execution_cost_write_count: 0, execution_cost_write_length: 0, + tx_count: 1, }; // Ensure db has a block so that current block height queries return a found result