diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19a6db61a6..39d80ecf6f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -413,6 +413,7 @@ jobs: parallel: true test-subnets: + if: false runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/docs/openapi.yaml b/docs/openapi.yaml index d1ef93fbac..e15cdb10b9 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -219,6 +219,71 @@ paths: items: type: string enum: [coinbase, token_transfer, smart_contract, contract_call, poison_microblock, tenure_change] + - name: from_address + in: query + description: Option to filter results by sender address + required: false + schema: + type: string + - name: to_address + in: query + description: Option to filter results by recipient address + required: false + schema: + type: string + - name: sort_by + in: query + description: Option to sort results by block height, timestamp, or fee + required: false + schema: + type: string + enum: [block_height, burn_block_time, fee] + example: burn_block_time + default: block_height + - name: start_time + in: query + description: Filter by transactions after this timestamp (unix timestamp in seconds) + required: false + schema: + type: integer + example: 1704067200 + - name: end_time + in: query + description: Filter by transactions before this timestamp (unix timestamp in seconds) + required: false + schema: + type: integer + example: 1706745599 + - name: contract_id + in: query + description: Filter by contract call transactions involving this contract ID + required: false + schema: + type: string + example: "SP000000000000000000002Q6VF78.pox-4" + - name: function_name + in: query + description: Filter by contract call transactions involving this function name + required: false + schema: + type: string + example: "delegate-stx" + - name: nonce + in: query + description: Filter by transactions with this nonce + required: false + schema: + type: integer + example: 123 + - name: order + in: query + description: Option to sort results in ascending or descending order + required: false + schema: + type: string + enum: [asc, desc] + example: desc + default: desc - name: unanchored in: query description: Include transaction data from unanchored (i.e. unconfirmed) microblocks diff --git a/migrations/1718632097776_tx-sort-indexes.js b/migrations/1718632097776_tx-sort-indexes.js new file mode 100644 index 0000000000..d5e8adb9f5 --- /dev/null +++ b/migrations/1718632097776_tx-sort-indexes.js @@ -0,0 +1,11 @@ +/** @param { import("node-pg-migrate").MigrationBuilder } pgm */ +exports.up = pgm => { + pgm.createIndex('txs', 'burn_block_time'); + pgm.createIndex('txs', 'fee_rate'); +}; + +/** @param { import("node-pg-migrate").MigrationBuilder } pgm */ +exports.down = pgm => { + pgm.dropIndex('txs', 'burn_block_time'); + pgm.dropIndex('txs', 'fee_rate'); +}; diff --git a/migrations/1718887498565_tx-contract-call-indexes.js b/migrations/1718887498565_tx-contract-call-indexes.js new file mode 100644 index 0000000000..e0aead3fb7 --- /dev/null +++ b/migrations/1718887498565_tx-contract-call-indexes.js @@ -0,0 +1,9 @@ +/** @param { import("node-pg-migrate").MigrationBuilder } pgm */ +exports.up = pgm => { + pgm.createIndex('txs', 'contract_call_function_name'); +}; + +/** @param { import("node-pg-migrate").MigrationBuilder } pgm */ +exports.down = pgm => { + pgm.dropIndex('txs', 'contract_call_function_name'); +}; diff --git a/migrations/1718887498565_tx-nonce-index.js b/migrations/1718887498565_tx-nonce-index.js new file mode 100644 index 0000000000..ae57d49c80 --- /dev/null +++ b/migrations/1718887498565_tx-nonce-index.js @@ -0,0 +1,9 @@ +/** @param { import("node-pg-migrate").MigrationBuilder } pgm */ +exports.up = pgm => { + pgm.createIndex('txs', 'nonce'); +}; + +/** @param { import("node-pg-migrate").MigrationBuilder } pgm */ +exports.down = pgm => { + pgm.dropIndex('txs', 'nonce'); +}; diff --git a/package-lock.json b/package-lock.json index 06ae5fa80a..cf9e1342d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "GPL-3.0", "dependencies": { "@apidevtools/json-schema-ref-parser": "9.0.9", - "@hirosystems/api-toolkit": "1.5.0", + "@hirosystems/api-toolkit": "1.6.2", "@promster/express": "6.0.0", "@promster/server": "6.0.6", "@promster/types": "3.2.3", @@ -1865,13 +1865,14 @@ } }, "node_modules/@hirosystems/api-toolkit": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@hirosystems/api-toolkit/-/api-toolkit-1.5.0.tgz", - "integrity": "sha512-f7rL2Bct+tW5gtYEZwCFQYQnkEIgGH+yoBYe807c+/gYItfWa9bPdY8KAFo+5AD1TbvP1bECrUClhK2TCCc1tA==", + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@hirosystems/api-toolkit/-/api-toolkit-1.6.2.tgz", + "integrity": "sha512-soL9IoRVADCm06x1sK5JHccvdAIhs4wk385lzlH/nIH3dIM7qJTQwC4gv5dCoSB4O9o0hK98sgB4xebjikPTGg==", "dependencies": { "@fastify/cors": "^8.0.0", "@fastify/swagger": "^8.3.1", "@fastify/type-provider-typebox": "^3.2.0", + "@sinclair/typebox": "^0.28.20", "fastify": "^4.3.0", "fastify-metrics": "^10.2.0", "node-pg-migrate": "^6.2.2", @@ -1885,6 +1886,11 @@ "node": ">=18" } }, + "node_modules/@hirosystems/api-toolkit/node_modules/@sinclair/typebox": { + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.28.20.tgz", + "integrity": "sha512-QCF3BGfacwD+3CKhGsMeixnwOmX4AWgm61nKkNdRStyLVu0mpVFYlDSY8gVBOOED1oSwzbJauIWl/+REj8K5+w==" + }, "node_modules/@hirosystems/api-toolkit/node_modules/@types/pg": { "version": "8.10.7", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.7.tgz", diff --git a/package.json b/package.json index 6cae3f9395..7169701442 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ }, "dependencies": { "@apidevtools/json-schema-ref-parser": "9.0.9", - "@hirosystems/api-toolkit": "1.5.0", + "@hirosystems/api-toolkit": "1.6.2", "@promster/express": "6.0.0", "@promster/server": "6.0.6", "@promster/types": "3.2.3", diff --git a/src/api/routes/tx.ts b/src/api/routes/tx.ts index 73eae43152..811a5ba96a 100644 --- a/src/api/routes/tx.ts +++ b/src/api/routes/tx.ts @@ -65,12 +65,121 @@ export function createTxRouter(db: PgStore): express.Router { txTypeFilter = []; } + let order: 'asc' | 'desc' | undefined; + if (req.query.order) { + if ( + typeof req.query.order === 'string' && + (req.query.order === 'asc' || req.query.order === 'desc') + ) { + order = req.query.order; + } else { + throw new InvalidRequestError( + `The "order" query parameter must be a 'desc' or 'asc'`, + InvalidRequestErrorType.invalid_param + ); + } + } + + let fromAddress: string | undefined; + if (typeof req.query.from_address === 'string') { + if (!isValidC32Address(req.query.from_address)) { + throw new InvalidRequestError( + `Invalid query parameter for "from_address": "${req.query.from_address}" is not a valid STX address`, + InvalidRequestErrorType.invalid_param + ); + } + fromAddress = req.query.from_address; + } + + let toAddress: string | undefined; + if (typeof req.query.to_address === 'string') { + if (!isValidPrincipal(req.query.to_address)) { + throw new InvalidRequestError( + `Invalid query parameter for "to_address": "${req.query.to_address}" is not a valid STX address`, + InvalidRequestErrorType.invalid_param + ); + } + toAddress = req.query.to_address; + } + + let startTime: number | undefined; + if (typeof req.query.start_time === 'string') { + if (!/^\d{10}$/.test(req.query.start_time)) { + throw new InvalidRequestError( + `Invalid query parameter for "start_time": "${req.query.start_time}" is not a valid timestamp`, + InvalidRequestErrorType.invalid_param + ); + } + startTime = parseInt(req.query.start_time); + } + + let endTime: number | undefined; + if (typeof req.query.end_time === 'string') { + if (!/^\d{10}$/.test(req.query.end_time)) { + throw new InvalidRequestError( + `Invalid query parameter for "end_time": "${req.query.end_time}" is not a valid timestamp`, + InvalidRequestErrorType.invalid_param + ); + } + endTime = parseInt(req.query.end_time); + } + + let contractId: string | undefined; + if (typeof req.query.contract_id === 'string') { + if (!isValidPrincipal(req.query.contract_id)) { + throw new InvalidRequestError( + `Invalid query parameter for "contract_id": "${req.query.contract_id}" is not a valid principal`, + InvalidRequestErrorType.invalid_param + ); + } + contractId = req.query.contract_id; + } + + let functionName: string | undefined; + if (typeof req.query.function_name === 'string') { + functionName = req.query.function_name; + } + + let nonce: number | undefined; + if (typeof req.query.nonce === 'string') { + if (!/^\d{1,10}$/.test(req.query.nonce)) { + throw new InvalidRequestError( + `Invalid query parameter for "nonce": "${req.query.nonce}" is not a valid nonce`, + InvalidRequestErrorType.invalid_param + ); + } + nonce = parseInt(req.query.nonce); + } + + let sortBy: 'block_height' | 'burn_block_time' | 'fee' | undefined; + if (req.query.sort_by) { + if ( + typeof req.query.sort_by === 'string' && + ['block_height', 'burn_block_time', 'fee'].includes(req.query.sort_by) + ) { + sortBy = req.query.sort_by as typeof sortBy; + } else { + throw new InvalidRequestError( + `The "sort_by" query parameter must be 'block_height', 'burn_block_time', or 'fee'`, + InvalidRequestErrorType.invalid_param + ); + } + } const includeUnanchored = isUnanchoredRequest(req, res, next); const { results: txResults, total } = await db.getTxList({ offset, limit, txTypeFilter, includeUnanchored, + fromAddress, + toAddress, + startTime, + endTime, + contractId, + functionName, + nonce, + order, + sortBy, }); const results = txResults.map(tx => parseDbTx(tx)); const response: TransactionResults = { limit, offset, total, results }; diff --git a/src/datastore/helpers.ts b/src/datastore/helpers.ts index 105814404d..1a8566aefa 100644 --- a/src/datastore/helpers.ts +++ b/src/datastore/helpers.ts @@ -273,29 +273,18 @@ export function prefixedCols(columns: string[], prefix: string): string[] { return columns.map(c => `${prefix}.${c}`); } -/** - * Concatenates column names to use on a query. Necessary when one or more of those columns is complex enough - * so that postgres.js can't figure out how to list it (e.g. abi column, aggregates, partitions, etc.). - * @param sql - SQL client - * @param columns - list of columns - * @returns raw SQL column list string - */ -export function unsafeCols(sql: PgSqlClient, columns: string[]): postgres.PendingQuery { - return sql.unsafe(columns.join(', ')); -} - /** * Shorthand function that returns a column query to retrieve the smart contract abi when querying transactions * that may be of type `contract_call`. Usually used alongside `TX_COLUMNS` or `MEMPOOL_TX_COLUMNS`. * @param tableName - Name of the table that will determine the transaction type. Defaults to `txs`. * @returns `string` - abi column select statement portion */ -export function abiColumn(tableName: string = 'txs'): string { - return ` - CASE WHEN ${tableName}.type_id = ${DbTxTypeId.ContractCall} THEN ( +export function abiColumn(sql: PgSqlClient, tableName: string = 'txs'): postgres.Fragment { + return sql` + CASE WHEN ${sql(tableName)}.type_id = ${DbTxTypeId.ContractCall} THEN ( SELECT abi FROM smart_contracts - WHERE smart_contracts.contract_id = ${tableName}.contract_call_contract_id + WHERE smart_contracts.contract_id = ${sql(tableName)}.contract_call_contract_id ORDER BY abi != 'null' DESC, canonical DESC, microblock_canonical DESC, block_height DESC LIMIT 1 ) END as abi diff --git a/src/datastore/pg-store.ts b/src/datastore/pg-store.ts index e335c7c038..5f1838e834 100644 --- a/src/datastore/pg-store.ts +++ b/src/datastore/pg-store.ts @@ -88,7 +88,6 @@ import { POX_SYNTHETIC_EVENT_COLUMNS, prefixedCols, TX_COLUMNS, - unsafeCols, validateZonefileHash, } from './helpers'; import { PgNotifier } from './pg-notifier'; @@ -103,6 +102,7 @@ import { import * as path from 'path'; import { PgStoreV2 } from './pg-store-v2'; import { MempoolOrderByParam, OrderParam } from '../api/query-helpers'; +import { Fragment } from 'postgres'; export const MIGRATIONS_DIR = path.join(REPO_DIR, 'migrations'); @@ -240,7 +240,7 @@ export class PgStore extends BasePgStore { const microblock_tx_count: Record = {}; if (metadata?.txs) { const txQuery = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} FROM txs WHERE index_block_hash = ${block.result.index_block_hash} AND canonical = true AND microblock_canonical = true @@ -528,7 +528,7 @@ export class PgStore extends BasePgStore { throw new Error(`Could not find block by hash ${blockHash}`); } const result = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} FROM txs WHERE index_block_hash = ${blockQuery.result.index_block_hash} AND canonical = true AND microblock_canonical = true @@ -613,7 +613,7 @@ export class PgStore extends BasePgStore { const { block_height } = await this.getChainTip(sql); const unanchoredBlockHeight = block_height + 1; const query = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} FROM txs WHERE canonical = true AND microblock_canonical = true AND block_height = ${unanchoredBlockHeight} ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC @@ -823,7 +823,7 @@ export class PgStore extends BasePgStore { AND index_block_hash = ${blockQuery.result.index_block_hash} `; const result = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} FROM txs WHERE canonical = true AND microblock_canonical = true AND index_block_hash = ${blockQuery.result.index_block_hash} @@ -977,7 +977,7 @@ export class PgStore extends BasePgStore { } return await this.sqlTransaction(async sql => { const result = await sql` - SELECT ${unsafeCols(sql, [...MEMPOOL_TX_COLUMNS, abiColumn('mempool_txs')])} + SELECT ${sql(MEMPOOL_TX_COLUMNS)}, ${abiColumn(sql, 'mempool_txs')} FROM mempool_txs WHERE tx_id IN ${sql(args.txIds)} `; @@ -996,7 +996,7 @@ export class PgStore extends BasePgStore { }): Promise> { return await this.sqlTransaction(async sql => { const result = await sql` - SELECT ${unsafeCols(sql, [...MEMPOOL_TX_COLUMNS, abiColumn('mempool_txs')])} + SELECT ${sql(MEMPOOL_TX_COLUMNS)}, ${abiColumn(sql, 'mempool_txs')} FROM mempool_txs WHERE tx_id = ${txId} `; @@ -1048,11 +1048,9 @@ export class PgStore extends BasePgStore { DbTxStatus.DroppedProblematic, ]; const resultQuery = await sql<(MempoolTxQueryResult & { count: number })[]>` - SELECT ${unsafeCols(sql, [ - ...prefixedCols(MEMPOOL_TX_COLUMNS, 'mempool'), - abiColumn('mempool'), - '(COUNT(*) OVER())::INTEGER AS count', - ])} + SELECT ${sql(prefixedCols(MEMPOOL_TX_COLUMNS, 'mempool'))}, + ${abiColumn(sql, 'mempool')}, + (COUNT(*) OVER())::INTEGER AS count FROM ( SELECT * FROM mempool_txs @@ -1323,7 +1321,7 @@ export class PgStore extends BasePgStore { orderBy == 'fee' ? sql`fee_rate` : orderBy == 'size' ? sql`tx_size` : sql`receipt_time`; const orderSql = order == 'asc' ? sql`ASC` : sql`DESC`; const resultQuery = await sql<(MempoolTxQueryResult & { count: number })[]>` - SELECT ${unsafeCols(sql, [...MEMPOOL_TX_COLUMNS, abiColumn('mempool_txs')])}, ${count} + SELECT ${sql(MEMPOOL_TX_COLUMNS)}, ${abiColumn(sql, 'mempool_txs')}, ${count} FROM mempool_txs WHERE ${ address @@ -1386,7 +1384,7 @@ export class PgStore extends BasePgStore { return await this.sqlTransaction(async sql => { const maxBlockHeight = await this.getMaxBlockHeight(sql, { includeUnanchored }); const result = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} FROM txs WHERE tx_id = ${txId} AND block_height <= ${maxBlockHeight} ORDER BY canonical DESC, microblock_canonical DESC, block_height DESC @@ -1418,47 +1416,113 @@ export class PgStore extends BasePgStore { offset, txTypeFilter, includeUnanchored, + fromAddress, + toAddress, + startTime, + endTime, + contractId, + functionName, + nonce, + order, + sortBy, }: { limit: number; offset: number; txTypeFilter: TransactionType[]; includeUnanchored: boolean; + fromAddress?: string; + toAddress?: string; + startTime?: number; + endTime?: number; + contractId?: string; + functionName?: string; + nonce?: number; + order?: 'desc' | 'asc'; + sortBy?: 'block_height' | 'burn_block_time' | 'fee'; }): Promise<{ results: DbTx[]; total: number }> { - let totalQuery: { count: number }[]; - let resultQuery: ContractTxQueryResult[]; return await this.sqlTransaction(async sql => { const maxHeight = await this.getMaxBlockHeight(sql, { includeUnanchored }); - if (txTypeFilter.length === 0) { - totalQuery = await sql<{ count: number }[]>` - SELECT ${includeUnanchored ? sql('tx_count_unanchored') : sql('tx_count')} AS count - FROM chain_tip - `; - resultQuery = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} - FROM txs - WHERE canonical = true AND microblock_canonical = true AND block_height <= ${maxHeight} - ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC - LIMIT ${limit} - OFFSET ${offset} - `; - } else { - const txTypeIds = txTypeFilter.flatMap(t => getTxTypeId(t)); - totalQuery = await sql<{ count: number }[]>` - SELECT COUNT(*)::integer - FROM txs - WHERE canonical = true AND microblock_canonical = true - AND type_id IN ${sql(txTypeIds)} AND block_height <= ${maxHeight} - `; - resultQuery = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} - FROM txs - WHERE canonical = true AND microblock_canonical = true - AND type_id IN ${sql(txTypeIds)} AND block_height <= ${maxHeight} - ORDER BY block_height DESC, microblock_sequence DESC, tx_index DESC - LIMIT ${limit} - OFFSET ${offset} - `; + const orderSql = order === 'asc' ? sql`ASC` : sql`DESC`; + + let orderBySql: Fragment; + switch (sortBy) { + case undefined: + case 'block_height': + orderBySql = sql`ORDER BY block_height ${orderSql}, microblock_sequence ${orderSql}, tx_index ${orderSql}`; + break; + case 'burn_block_time': + orderBySql = sql`ORDER BY burn_block_time ${orderSql}, block_height ${orderSql}, microblock_sequence ${orderSql}, tx_index ${orderSql}`; + break; + case 'fee': + orderBySql = sql`ORDER BY fee_rate ${orderSql}, block_height ${orderSql}, microblock_sequence ${orderSql}, tx_index ${orderSql}`; + break; + default: + throw new Error(`Invalid sortBy param: ${sortBy}`); } + + const txTypeFilterSql = + txTypeFilter.length > 0 + ? sql`AND type_id IN ${sql(txTypeFilter.flatMap(t => getTxTypeId(t)))}` + : sql``; + const fromAddressFilterSql = fromAddress ? sql`AND sender_address = ${fromAddress}` : sql``; + const toAddressFilterSql = toAddress + ? sql`AND token_transfer_recipient_address = ${toAddress}` + : sql``; + const startTimeFilterSql = startTime ? sql`AND burn_block_time >= ${startTime}` : sql``; + const endTimeFilterSql = endTime ? sql`AND burn_block_time <= ${endTime}` : sql``; + const contractIdFilterSql = contractId + ? sql`AND contract_call_contract_id = ${contractId}` + : sql``; + const contractFuncFilterSql = functionName + ? sql`AND contract_call_function_name = ${functionName}` + : sql``; + const nonceFilterSql = nonce ? sql`AND nonce = ${nonce}` : sql``; + const noFilters = + txTypeFilter.length === 0 && + !fromAddress && + !toAddress && + !startTime && + !endTime && + !contractId && + !functionName && + !nonce; + + const totalQuery: { count: number }[] = noFilters + ? await sql<{ count: number }[]>` + SELECT ${includeUnanchored ? sql('tx_count_unanchored') : sql('tx_count')} AS count + FROM chain_tip + ` + : await sql<{ count: number }[]>` + SELECT COUNT(*)::integer AS count + FROM txs + WHERE canonical = true AND microblock_canonical = true AND block_height <= ${maxHeight} + ${txTypeFilterSql} + ${fromAddressFilterSql} + ${toAddressFilterSql} + ${startTimeFilterSql} + ${endTimeFilterSql} + ${contractIdFilterSql} + ${contractFuncFilterSql} + ${nonceFilterSql} + `; + + const resultQuery: ContractTxQueryResult[] = await sql` + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} + FROM txs + WHERE canonical = true AND microblock_canonical = true AND block_height <= ${maxHeight} + ${txTypeFilterSql} + ${fromAddressFilterSql} + ${toAddressFilterSql} + ${startTimeFilterSql} + ${endTimeFilterSql} + ${contractIdFilterSql} + ${contractFuncFilterSql} + ${nonceFilterSql} + ${orderBySql} + LIMIT ${limit} + OFFSET ${offset} + `; + const parsed = resultQuery.map(r => parseTxQueryResult(r)); return { results: parsed, total: totalQuery[0].count }; }); @@ -1474,11 +1538,13 @@ export class PgStore extends BasePgStore { }): Promise<{ results: DbEvent[] }> { return await this.sqlTransaction(async sql => { if (args.txs.length === 0) return { results: [] }; - // TODO: This hack has to be done because postgres.js can't figure out how to interpolate - // these `bytea` VALUES comparisons yet. - const transactionValues = args.txs - .map(tx => `('\\x${tx.txId.slice(2)}'::bytea, '\\x${tx.indexBlockHash.slice(2)}'::bytea)`) - .join(', '); + + const transactionValues = args.txs.map(tx => [ + `\\x${tx.txId.slice(2)}`, + `\\x${tx.indexBlockHash.slice(2)}`, + ]); + const txValuesSql = sql(transactionValues.map(tx => sql`(${tx[0]}::bytea, ${tx[1]}::bytea)`)); + const eventIndexStart = args.offset; const eventIndexEnd = args.offset + args.limit - 1; const stxLockResults = await sql< @@ -1497,7 +1563,7 @@ export class PgStore extends BasePgStore { SELECT event_index, tx_id, tx_index, block_height, canonical, locked_amount, unlock_height, locked_address, contract_name FROM stx_lock_events - WHERE (tx_id, index_block_hash) IN (VALUES ${sql.unsafe(transactionValues)}) + WHERE (tx_id, index_block_hash) IN ${txValuesSql} AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; const stxResults = await sql< @@ -1517,7 +1583,7 @@ export class PgStore extends BasePgStore { SELECT event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, amount, memo FROM stx_events - WHERE (tx_id, index_block_hash) IN (VALUES ${sql.unsafe(transactionValues)}) + WHERE (tx_id, index_block_hash) IN ${txValuesSql} AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; const ftResults = await sql< @@ -1537,7 +1603,7 @@ export class PgStore extends BasePgStore { SELECT event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, asset_identifier, amount FROM ft_events - WHERE (tx_id, index_block_hash) IN (VALUES ${sql.unsafe(transactionValues)}) + WHERE (tx_id, index_block_hash) IN ${txValuesSql} AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; const nftResults = await sql< @@ -1557,7 +1623,7 @@ export class PgStore extends BasePgStore { SELECT event_index, tx_id, tx_index, block_height, canonical, asset_event_type_id, sender, recipient, asset_identifier, value FROM nft_events - WHERE (tx_id, index_block_hash) = ANY(VALUES ${sql.unsafe(transactionValues)}) + WHERE (tx_id, index_block_hash) IN ${txValuesSql} AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; const logResults = await sql< @@ -1575,7 +1641,7 @@ export class PgStore extends BasePgStore { SELECT event_index, tx_id, tx_index, block_height, canonical, contract_identifier, topic, value FROM contract_logs - WHERE (tx_id, index_block_hash) IN (VALUES ${sql.unsafe(transactionValues)}) + WHERE (tx_id, index_block_hash) IN ${txValuesSql} AND microblock_canonical = true AND event_index BETWEEN ${eventIndexStart} AND ${eventIndexEnd} `; return { @@ -2745,7 +2811,9 @@ export class PgStore extends BasePgStore { LIMIT ${args.limit} OFFSET ${args.offset} ) - SELECT ${unsafeCols(this.sql, [...TX_COLUMNS, abiColumn(), 'count'])} + SELECT ${this.sql(TX_COLUMNS)}, + ${abiColumn(this.sql)}, + count FROM stx_txs INNER JOIN txs USING (tx_id, index_block_hash, microblock_hash) `; @@ -2814,7 +2882,7 @@ export class PgStore extends BasePgStore { events.sender as event_sender, events.recipient as event_recipient, events.memo as event_memo, - ${this.sql.unsafe(abiColumn('transactions'))} + ${abiColumn(this.sql, 'transactions')} FROM transactions LEFT JOIN events ON transactions.tx_id = events.tx_id AND transactions.tx_id = ${tx_id} @@ -2914,7 +2982,7 @@ export class PgStore extends BasePgStore { ) SELECT transactions.*, - ${this.sql.unsafe(abiColumn('transactions'))}, + ${abiColumn(this.sql, 'transactions')}, events.event_index as event_index, events.event_type_id as event_type, events.amount as event_amount, @@ -3039,7 +3107,7 @@ export class PgStore extends BasePgStore { // TODO(mb): add support for searching for microblock by hash return await this.sqlTransaction(async sql => { const txQuery = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} FROM txs WHERE tx_id = ${hash} LIMIT 1 `; if (txQuery.length > 0) { @@ -3054,7 +3122,7 @@ export class PgStore extends BasePgStore { }; } const txMempoolQuery = await sql` - SELECT ${unsafeCols(sql, [...MEMPOOL_TX_COLUMNS, abiColumn('mempool_txs')])} + SELECT ${sql(MEMPOOL_TX_COLUMNS)}, ${abiColumn(sql, 'mempool_txs')} FROM mempool_txs WHERE pruned = false AND tx_id = ${hash} LIMIT 1 `; if (txMempoolQuery.length > 0) { @@ -3099,7 +3167,7 @@ export class PgStore extends BasePgStore { return await this.sqlTransaction(async sql => { if (isContract) { const contractMempoolTxResult = await sql` - SELECT ${unsafeCols(sql, [...MEMPOOL_TX_COLUMNS, abiColumn('mempool_txs')])} + SELECT ${sql(MEMPOOL_TX_COLUMNS)}, ${abiColumn(sql, 'mempool_txs')} FROM mempool_txs WHERE pruned = false AND smart_contract_contract_id = ${principal} LIMIT 1 `; if (contractMempoolTxResult.length > 0) { @@ -3114,7 +3182,7 @@ export class PgStore extends BasePgStore { }; } const contractTxResult = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} FROM txs WHERE smart_contract_contract_id = ${principal} ORDER BY canonical DESC, microblock_canonical DESC, block_height DESC @@ -3259,13 +3327,12 @@ export class PgStore extends BasePgStore { ${ args.includeTxMetadata ? this.sql` - SELECT ${unsafeCols(this.sql, [ + SELECT ${this.sql([ 'nft.asset_identifier', 'nft.value', ...prefixedCols(TX_COLUMNS, 'txs'), - abiColumn(), 'nft.count', - ])} + ])}, ${abiColumn(this.sql)} FROM nft INNER JOIN txs USING (tx_id) WHERE txs.canonical = TRUE AND txs.microblock_canonical = TRUE @@ -3301,7 +3368,7 @@ export class PgStore extends BasePgStore { includeTxMetadata: boolean; }): Promise<{ results: NftEventWithTxMetadata[]; total: number }> { const columns = args.includeTxMetadata - ? unsafeCols(this.sql, [ + ? this.sql`${this.sql([ 'asset_identifier', 'value', 'event_index', @@ -3309,8 +3376,7 @@ export class PgStore extends BasePgStore { 'sender', 'recipient', ...prefixedCols(TX_COLUMNS, 'txs'), - abiColumn(), - ]) + ])}, ${abiColumn(this.sql)}` : this.sql`nft.*`; const nftTxResults = await this.sql<(DbNftEvent & ContractTxQueryResult & { count: number })[]>` SELECT ${columns}, (COUNT(*) OVER())::INTEGER AS count @@ -3362,7 +3428,7 @@ export class PgStore extends BasePgStore { includeTxMetadata: boolean; }): Promise<{ results: NftEventWithTxMetadata[]; total: number }> { const columns = args.includeTxMetadata - ? unsafeCols(this.sql, [ + ? this.sql`${this.sql([ 'asset_identifier', 'value', 'event_index', @@ -3370,8 +3436,7 @@ export class PgStore extends BasePgStore { 'sender', 'recipient', ...prefixedCols(TX_COLUMNS, 'txs'), - abiColumn(), - ]) + ])}, ${abiColumn(this.sql)}` : this.sql`nft.*`; const nftTxResults = await this.sql<(DbNftEvent & ContractTxQueryResult & { count: number })[]>` SELECT ${columns}, (COUNT(*) OVER())::INTEGER AS count @@ -3442,7 +3507,7 @@ export class PgStore extends BasePgStore { return await this.sqlTransaction(async sql => { const maxBlockHeight = await this.getMaxBlockHeight(sql, { includeUnanchored }); const result = await sql` - SELECT ${unsafeCols(sql, [...TX_COLUMNS, abiColumn()])} + SELECT ${sql(TX_COLUMNS)}, ${abiColumn(sql)} FROM txs WHERE tx_id IN ${sql(txIds)} AND block_height <= ${maxBlockHeight} diff --git a/src/event-stream/event-server.ts b/src/event-stream/event-server.ts index 22fc2cd08d..5bbe0bfdfe 100644 --- a/src/event-stream/event-server.ts +++ b/src/event-stream/event-server.ts @@ -480,6 +480,8 @@ function parseDataStoreTxEventData( return dbTx; }); + const poxEventLogs: Map = new Map(); + for (const event of events) { if (!event.committed) { logger.debug(`Ignoring uncommitted tx event from tx ${event.txid}`); @@ -548,6 +550,7 @@ function parseDataStoreTxEventData( ...dbEvent, ...poxEventData, }; + poxEventLogs.set(dbPoxEvent, entry); switch (contractName) { case POX_2_CONTRACT_NAME: { dbTx.pox2Events.push(dbPoxEvent); @@ -723,9 +726,6 @@ function parseDataStoreTxEventData( tx.nftEvents, tx.stxEvents, tx.stxLockEvents, - tx.pox2Events, - tx.pox3Events, - tx.pox4Events, ] .flat() .sort((a, b) => a.event_index - b.event_index); @@ -733,6 +733,13 @@ function parseDataStoreTxEventData( for (let i = 0; i < sortedEvents.length; i++) { sortedEvents[i].event_index = i; } + for (const poxEvent of [tx.pox2Events, tx.pox3Events, tx.pox4Events].flat()) { + const associatedLogEvent = poxEventLogs.get(poxEvent); + if (!associatedLogEvent) { + throw new Error(`Missing associated contract log event for pox event ${poxEvent.tx_id}`); + } + poxEvent.event_index = associatedLogEvent.event_index; + } } return dbData; diff --git a/src/test-utils/test-builders.ts b/src/test-utils/test-builders.ts index 0018eba505..fe91ac6bb5 100644 --- a/src/test-utils/test-builders.ts +++ b/src/test-utils/test-builders.ts @@ -256,6 +256,20 @@ function testTx(args?: TestTxArgs): DataStoreTxEventData { pox3Events: [], pox4Events: [], }; + if ( + data.tx.type_id === DbTxTypeId.SmartContract || + data.tx.type_id === DbTxTypeId.VersionedSmartContract + ) { + data.smartContracts.push({ + tx_id: data.tx.tx_id, + canonical: data.tx.canonical, + contract_id: data.tx.smart_contract_contract_id as string, + block_height: data.tx.block_height, + clarity_version: data.tx.smart_contract_clarity_version as number, + source_code: data.tx.smart_contract_source_code as string, + abi: data.tx.abi as string, + }); + } return data; } diff --git a/src/tests/other-tests.ts b/src/tests/other-tests.ts index 28e76e1a90..e1cb2b6025 100644 --- a/src/tests/other-tests.ts +++ b/src/tests/other-tests.ts @@ -247,10 +247,10 @@ describe('other tests', () => { const block_hash = '0xd10ccecfd7ac9e5f8a10de0532fac028559b31a6ff494d82147f6297fb66313'; const principal_addr = 'S.hello-world'; const odd_tx_error = { - error: `Hex string is an odd number of digits: ${tx_id}`, + error: `Hex string is an odd number of digits`, }; const odd_block_error = { - error: `Hex string is an odd number of digits: ${block_hash}`, + error: `Hex string is an odd number of digits`, }; const metadata_error = { error: `Unexpected value for 'include_metadata' parameter: "bac"` }; const principal_error = { error: 'invalid STX address "S.hello-world"' }; diff --git a/src/tests/smart-contract-tests.ts b/src/tests/smart-contract-tests.ts index 1d0941642f..d8b8faa4e1 100644 --- a/src/tests/smart-contract-tests.ts +++ b/src/tests/smart-contract-tests.ts @@ -1740,12 +1740,8 @@ describe('smart contract tests', () => { type_id: DbTxTypeId.SmartContract, smart_contract_contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1', smart_contract_source_code: '(some-contract-src)', - }) - .addTxSmartContract({ - tx_id: '0x1234', - block_height: 1, - contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-1', - contract_source: '(some-contract-src)', + smart_contract_clarity_version: 1, + abi: JSON.stringify({ some: 'abi' }), }) .build(); await db.update(block1); @@ -1759,12 +1755,8 @@ describe('smart contract tests', () => { type_id: DbTxTypeId.SmartContract, smart_contract_contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2', smart_contract_source_code: '(some-contract-src)', - }) - .addTxSmartContract({ - tx_id: '0x1222', - block_height: 2, - contract_id: 'SP3K8BC0PPEVCV7NZ6QSRWPQ2JE9E5B6N3PA0KBR9.contract-2', - contract_source: '(some-contract-src)', + smart_contract_clarity_version: 1, + abi: JSON.stringify({ some: 'abi' }), }) .build(); await db.update(block2); diff --git a/src/tests/synthetic-stx-txs-tests.ts b/src/tests/synthetic-stx-txs-tests.ts index cd9b7dd278..c13270bd96 100644 --- a/src/tests/synthetic-stx-txs-tests.ts +++ b/src/tests/synthetic-stx-txs-tests.ts @@ -4,6 +4,7 @@ import * as path from 'path'; import { DecodedTxResult, TxPayloadTypeID } from 'stacks-encoding-native-js'; import { CoreNodeBlockMessage } from '../event-stream/core-node-message'; import { parseMessageTransaction } from '../event-stream/reader'; +import { parseNewBlockMessage } from '../event-stream/event-server'; // Test processing of the psuedo-Stacks transactions, i.e. the ones that // originate on the Bitcoin chain, and have a `raw_tx == '0x00'. @@ -194,6 +195,34 @@ describe('synthetic stx txs', () => { expect(parsed.parsed_tx).toEqual(expect.objectContaining(expected)); }); + test('test synthetic tx stx lock 3', () => { + const file = + 'synthetic-tx-payloads/stx_lock-1994-0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc.json'; + const txid = file.split('-').slice(-1)[0].split('.')[0]; + const payloadStr = fs.readFileSync(path.join(__dirname, file), { encoding: 'utf8' }); + const blockMsg = JSON.parse(payloadStr) as CoreNodeBlockMessage; + const txMsg = blockMsg.transactions.find(t => t.txid === txid); + if (!txMsg) { + throw new Error(`Cound not find tx ${txid}`); + } + const parsed = parseNewBlockMessage(ChainID.Mainnet, blockMsg); + if (!parsed) { + throw new Error(`Failed to parse ${txid}`); + } + // Ensure real contract event indexes are contiguous + const events = [parsed.txs[0].contractLogEvents, parsed.txs[0].stxLockEvents] + .flat() + .sort((a, b) => a.event_index - b.event_index); + expect(events).toHaveLength(13); + for (let i = 0; i < events.length; i++) { + expect(events[i].event_index).toEqual(i); + } + // Ensure synthetic pox event indexes are in expected range + for (const poxEvent of parsed.txs[0].pox4Events) { + expect(poxEvent.event_index).toBeLessThan(events.length); + } + }); + test('test synthetic tx stx lock 2', () => { // Testing a newer tx from mainnet (at block 51451) const file = diff --git a/src/tests/synthetic-tx-payloads/stx_lock-1994-0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc.json b/src/tests/synthetic-tx-payloads/stx_lock-1994-0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc.json new file mode 100644 index 0000000000..8af2e36c61 --- /dev/null +++ b/src/tests/synthetic-tx-payloads/stx_lock-1994-0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc.json @@ -0,0 +1,2357 @@ +{ + "block_hash": "0x5d53911701b66adcdda018001ebe1a7d54acd4f9a8d15d1f57cf4b2058b78f95", + "miner_txid": "0x22f30c8696f15dda1bf32cd1f5d0a44e242cf19f1ccc0613b8566cfdf79b5cfe", + "reward_set": null, + "block_height": 1, + "cycle_number": null, + "anchored_cost": { + "runtime": 402683495, + "read_count": 10185, + "read_length": 25959775, + "write_count": 700, + "write_length": 70075 + }, + "signer_bitvec": null, + "burn_block_hash": "0x00000000000000000002f2e8c7fd60addc520b09467f618667a841e2ceafa6cb", + "burn_block_time": 1716238792, + "index_block_hash": "0x80e73cd9a61876db38f4e02828241076d0a16670fc11646abb74d634804cc8d9", + "burn_block_height": 844350, + "parent_block_hash": "0x042cc20f6105fb9e742c4c2369a05999ee766bfa21fe66e465ebb2de75c3a36d", + "parent_microblock": "0x0000000000000000000000000000000000000000000000000000000000000000", + "pox_v1_unlock_height": 781552, + "pox_v2_unlock_height": 787652, + "pox_v3_unlock_height": 840361, + "matured_miner_rewards": [ + { + "recipient": "SP11M3BVG5TGNWQ1FFE5EJM86TMV08YF3EK0JSMQ8", + "miner_address": "SP11M3BVG5TGNWQ1FFE5EJM86TMV08YF3EK0JSMQ8", + "coinbase_amount": "1000000000", + "tx_fees_anchored": "9511437", + "from_stacks_block_hash": "0x040516e4ae9bbbaf22c4b4e716f359bd9a7b90b29f47c17e8d0df24af8242e20", + "from_index_consensus_hash": "0xb94814bef09b858177fe471b917273a63d899c25f0eb0f6210a07733e8c0ea0e", + "tx_fees_streamed_produced": "0", + "tx_fees_streamed_confirmed": "0" + }, + { + "recipient": "SP28YSGNKBZM8VE1ZQF8A3ANG9MZ7Q5893XASMGWM", + "miner_address": "SP28YSGNKBZM8VE1ZQF8A3ANG9MZ7Q5893XASMGWM", + "coinbase_amount": "0", + "tx_fees_anchored": "0", + "from_stacks_block_hash": "0x040516e4ae9bbbaf22c4b4e716f359bd9a7b90b29f47c17e8d0df24af8242e20", + "from_index_consensus_hash": "0xb94814bef09b858177fe471b917273a63d899c25f0eb0f6210a07733e8c0ea0e", + "tx_fees_streamed_produced": "0", + "tx_fees_streamed_confirmed": "0" + } + ], + "parent_burn_block_hash": "0x00000000000000000002dcef0ad9a999961f87665cbda4fc728fd8ba2b63c419", + "parent_index_block_hash": "0x6245f25fbfbdc9bc1e48e10c091930f6a25b3b77eb483e8b61b1c18a848f8599", + "parent_burn_block_height": 844349, + "confirmed_microblocks_cost": { + "runtime": 0, + "read_count": 0, + "read_length": 0, + "write_count": 0, + "write_length": 0 + }, + "parent_microblock_sequence": 0, + "parent_burn_block_timestamp": 1716237637, + "transactions": [ + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "raw_tx": "0x0000000001040020a586c3f6c204b8429f5f2f50c384c101057ee9000000000000000e00000000000186a00000ed9a37094eabba548ba149245e4f1224c8194b3bff2084b5987d96d27707273e61691671ed12f06d80b8ebbf0706de458370bd2e3a9682a58db7db8a66ce9a9b0101000000000214e75dff23072877a139ecf7b7f899aa714b7fe97f207075626c69632d706f6f6c732d73747261746567792d6d616e616765722d76320d66756e642d7374726174656779000000010b0000000f0100000000000000000000027f59b6d40a010000000000000000000000e8d4a510000100000000000000000000003a35294400010000000000000000000000000000000001000000000000000000000002540be400010000000000000000000000000000000001000000000000000000000000000000000100000000000000000000000000000000010000000000000000000000000000000001000000000000000000000000000000000100000000000000000000027f59b6d40a01000000000000000000000000000000000100000000000000000000003a35294400010000000000000000000000174876e80001000000000000000000000002540be400", + "status": "success", + "tx_index": 40, + "raw_result": "0x070100000000000000000000000000000000", + "burnchain_op": null, + "contract_abi": null, + "execution_cost": { + "runtime": 6560983, + "read_count": 596, + "read_length": 4230411, + "write_count": 38, + "write_length": 3669 + }, + "microblock_hash": null, + "microblock_sequence": null, + "microblock_parent_hash": null + } + ], + "events": [ + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 105, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "delegate-to": { + "Principal": { + "Contract": { + "name": "pox4-fast-pool-v3", + "issuer": [ + 22, + [ + 131, + 237, + 102, + 134, + 3, + 21, + 227, + 52, + 1, + 11, + 191, + 183, + 110, + 179, + 238, + 248, + 135, + 239, + 238, + 10 + ] + ] + } + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + } + }, + "type_signature": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 114, + 101, + 118, + 111, + 107, + 101, + 45, + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 2745989256201 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "fastpool-v2-member1", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 19 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000030b64656c65676174652d746f061683ed66860315e334010bbfb76eb3eef887efee0a11706f78342d666173742d706f6f6c2d76330c656e642d6379636c652d6964090e73746172742d6379636c652d69640100000000000000000000000000000055066c6f636b65640100000000000000000000027f59b6d409046e616d650d000000137265766f6b652d64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f1366617374706f6f6c2d76322d6d656d62657231", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 111, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "delegate-to": { + "Principal": { + "Standard": [ + 22, + [ + 59, + 188, + 101, + 209, + 18, + 231, + 158, + 169, + 210, + 0, + 30, + 21, + 248, + 166, + 21, + 136, + 86, + 149, + 216, + 182 + ] + ] + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + } + }, + "type_signature": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 114, + 101, + 118, + 111, + 107, + 101, + 45, + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 249999000000 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "xverse-v2-member3", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 19 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000030b64656c65676174652d746f05163bbc65d112e79ea9d2001e15f8a615885695d8b60c656e642d6379636c652d6964090e73746172742d6379636c652d69640100000000000000000000000000000055066c6f636b65640100000000000000000000003a351a01c0046e616d650d000000137265766f6b652d64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f117876657273652d76322d6d656d62657233", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 115, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "delegate-to": { + "Principal": { + "Standard": [ + 22, + [ + 59, + 188, + 101, + 209, + 18, + 231, + 158, + 169, + 210, + 0, + 30, + 21, + 248, + 166, + 21, + 136, + 86, + 149, + 216, + 182 + ] + ] + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + } + }, + "type_signature": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 114, + 101, + 118, + 111, + 107, + 101, + 45, + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 9999000000 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "xverse-v2-member5", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 19 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000030b64656c65676174652d746f05163bbc65d112e79ea9d2001e15f8a615885695d8b60c656e642d6379636c652d6964090e73746172742d6379636c652d69640100000000000000000000000000000055066c6f636b65640100000000000000000000000253fca1c0046e616d650d000000137265766f6b652d64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f117876657273652d76322d6d656d62657235", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 112, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "pox-addr": { + "Optional": { + "data": null + } + }, + "amount-ustx": { + "UInt": 250000000000 + }, + "delegate-to": { + "Principal": { + "Standard": [ + 22, + [ + 59, + 188, + 101, + 209, + 18, + 231, + 158, + 169, + 210, + 0, + 30, + 21, + 248, + 166, + 21, + 136, + 86, + 149, + 216, + 182 + ] + ] + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + }, + "unlock-burn-height": { + "Optional": { + "data": null + } + } + }, + "type_signature": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 249999000000 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "xverse-v2-member3", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 12 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000060b616d6f756e742d757374780100000000000000000000003a352944000b64656c65676174652d746f05163bbc65d112e79ea9d2001e15f8a615885695d8b60c656e642d6379636c652d69640908706f782d61646472090e73746172742d6379636c652d6964010000000000000000000000000000005512756e6c6f636b2d6275726e2d68656967687409066c6f636b65640100000000000000000000003a351a01c0046e616d650d0000000c64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f117876657273652d76322d6d656d62657233", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 114, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "pox-addr": { + "Optional": { + "data": null + } + }, + "amount-ustx": { + "UInt": 100000000000 + }, + "delegate-to": { + "Principal": { + "Standard": [ + 22, + [ + 59, + 188, + 101, + 209, + 18, + 231, + 158, + 169, + 210, + 0, + 30, + 21, + 248, + 166, + 21, + 136, + 86, + 149, + 216, + 182 + ] + ] + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + }, + "unlock-burn-height": { + "Optional": { + "data": null + } + } + }, + "type_signature": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 99999000000 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "xverse-v2-member4", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 12 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000060b616d6f756e742d75737478010000000000000000000000174876e8000b64656c65676174652d746f05163bbc65d112e79ea9d2001e15f8a615885695d8b60c656e642d6379636c652d69640908706f782d61646472090e73746172742d6379636c652d6964010000000000000000000000000000005512756e6c6f636b2d6275726e2d68656967687409066c6f636b6564010000000000000000000000174867a5c0046e616d650d0000000c64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f117876657273652d76322d6d656d62657234", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 109, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "delegate-to": { + "Principal": { + "Standard": [ + 22, + [ + 59, + 188, + 101, + 209, + 18, + 231, + 158, + 169, + 210, + 0, + 30, + 21, + 248, + 166, + 21, + 136, + 86, + 149, + 216, + 182 + ] + ] + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + } + }, + "type_signature": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 114, + 101, + 118, + 111, + 107, + 101, + 45, + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 2745989256201 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "xverse-v2-member1", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 19 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000030b64656c65676174652d746f05163bbc65d112e79ea9d2001e15f8a615885695d8b60c656e642d6379636c652d6964090e73746172742d6379636c652d69640100000000000000000000000000000055066c6f636b65640100000000000000000000027f59b6d409046e616d650d000000137265766f6b652d64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f117876657273652d76322d6d656d62657231", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 106, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "pox-addr": { + "Optional": { + "data": null + } + }, + "amount-ustx": { + "UInt": 2745989256202 + }, + "delegate-to": { + "Principal": { + "Contract": { + "name": "pox4-fast-pool-v3", + "issuer": [ + 22, + [ + 131, + 237, + 102, + 134, + 3, + 21, + 227, + 52, + 1, + 11, + 191, + 183, + 110, + 179, + 238, + 248, + 135, + 239, + 238, + 10 + ] + ] + } + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + }, + "unlock-burn-height": { + "Optional": { + "data": null + } + } + }, + "type_signature": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 2745989256201 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "fastpool-v2-member1", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 12 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000060b616d6f756e742d757374780100000000000000000000027f59b6d40a0b64656c65676174652d746f061683ed66860315e334010bbfb76eb3eef887efee0a11706f78342d666173742d706f6f6c2d76330c656e642d6379636c652d69640908706f782d61646472090e73746172742d6379636c652d6964010000000000000000000000000000005512756e6c6f636b2d6275726e2d68656967687409066c6f636b65640100000000000000000000027f59b6d409046e616d650d0000000c64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f1366617374706f6f6c2d76322d6d656d62657231", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 116, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "pox-addr": { + "Optional": { + "data": null + } + }, + "amount-ustx": { + "UInt": 10000000000 + }, + "delegate-to": { + "Principal": { + "Standard": [ + 22, + [ + 59, + 188, + 101, + 209, + 18, + 231, + 158, + 169, + 210, + 0, + 30, + 21, + 248, + 166, + 21, + 136, + 86, + 149, + 216, + 182 + ] + ] + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + }, + "unlock-burn-height": { + "Optional": { + "data": null + } + } + }, + "type_signature": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 9999000000 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "xverse-v2-member5", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 12 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000060b616d6f756e742d7573747801000000000000000000000002540be4000b64656c65676174652d746f05163bbc65d112e79ea9d2001e15f8a615885695d8b60c656e642d6379636c652d69640908706f782d61646472090e73746172742d6379636c652d6964010000000000000000000000000000005512756e6c6f636b2d6275726e2d68656967687409066c6f636b65640100000000000000000000000253fca1c0046e616d650d0000000c64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f117876657273652d76322d6d656d62657235", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 113, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "delegate-to": { + "Principal": { + "Standard": [ + 22, + [ + 59, + 188, + 101, + 209, + 18, + 231, + 158, + 169, + 210, + 0, + 30, + 21, + 248, + 166, + 21, + 136, + 86, + 149, + 216, + 182 + ] + ] + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + } + }, + "type_signature": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 114, + 101, + 118, + 111, + 107, + 101, + 45, + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 99999000000 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "xverse-v2-member4", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType" + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 19 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000030b64656c65676174652d746f05163bbc65d112e79ea9d2001e15f8a615885695d8b60c656e642d6379636c652d6964090e73746172742d6379636c652d69640100000000000000000000000000000055066c6f636b6564010000000000000000000000174867a5c0046e616d650d000000137265766f6b652d64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f117876657273652d76322d6d656d62657234", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 107, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "stacker": { + "Principal": { + "Contract": { + "name": "fastpool-v2-member1", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "pox-addr": { + "Tuple": { + "data_map": { + "version": { + "Sequence": { + "Buffer": { + "data": [ + 4 + ] + } + } + }, + "hashbytes": { + "Sequence": { + "Buffer": { + "data": [ + 131, + 237, + 102, + 134, + 3, + 21, + 227, + 52, + 1, + 11, + 191, + 183, + 110, + 179, + 238, + 248, + 135, + 239, + 238, + 10 + ] + } + } + } + }, + "type_signature": { + "type_map": { + "version": { + "SequenceType": { + "BufferType": 1 + } + }, + "hashbytes": { + "SequenceType": { + "BufferType": 20 + } + } + } + } + } + }, + "delegator": { + "Principal": { + "Contract": { + "name": "pox4-fast-pool-v3", + "issuer": [ + 22, + [ + 131, + 237, + 102, + 134, + 3, + 21, + 227, + 52, + 1, + 11, + 191, + 183, + 110, + 179, + 238, + 248, + 135, + 239, + 238, + 10 + ] + ] + } + } + }, + "end-cycle-id": { + "Optional": { + "data": { + "UInt": 86 + } + } + }, + "extend-count": { + "UInt": 1 + }, + "start-cycle-id": { + "UInt": 85 + }, + "unlock-burn-height": { + "UInt": 846650 + } + }, + "type_signature": { + "type_map": { + "stacker": "PrincipalType", + "pox-addr": { + "TupleType": { + "type_map": { + "version": { + "SequenceType": { + "BufferType": 1 + } + }, + "hashbytes": { + "SequenceType": { + "BufferType": 20 + } + } + } + } + }, + "delegator": "PrincipalType", + "end-cycle-id": { + "OptionalType": "UIntType" + }, + "extend-count": "UIntType", + "start-cycle-id": "UIntType", + "unlock-burn-height": "UIntType" + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 97, + 99, + 107, + 45, + 101, + 120, + 116, + 101, + 110, + 100 + ] + } + } + } + }, + "locked": { + "UInt": 2745989256201 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "fastpool-v2-member1", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "stacker": "PrincipalType", + "pox-addr": { + "TupleType": { + "type_map": { + "version": { + "SequenceType": { + "BufferType": 1 + } + }, + "hashbytes": { + "SequenceType": { + "BufferType": 20 + } + } + } + } + }, + "delegator": "PrincipalType", + "end-cycle-id": { + "OptionalType": "UIntType" + }, + "extend-count": "UIntType", + "start-cycle-id": "UIntType", + "unlock-burn-height": "UIntType" + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 21 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000070964656c656761746f72061683ed66860315e334010bbfb76eb3eef887efee0a11706f78342d666173742d706f6f6c2d76330c656e642d6379636c652d69640a01000000000000000000000000000000560c657874656e642d636f756e74010000000000000000000000000000000108706f782d616464720c0000000209686173686279746573020000001483ed66860315e334010bbfb76eb3eef887efee0a0776657273696f6e02000000010407737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f1366617374706f6f6c2d76322d6d656d626572310e73746172742d6379636c652d6964010000000000000000000000000000005512756e6c6f636b2d6275726e2d68656967687401000000000000000000000000000ceb3a066c6f636b65640100000000000000000000027f59b6d409046e616d650d0000001564656c65676174652d737461636b2d657874656e6407737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f1366617374706f6f6c2d76322d6d656d62657231", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 110, + "contract_event": { + "topic": "print", + "value": { + "Response": { + "data": { + "Tuple": { + "data_map": { + "data": { + "Tuple": { + "data_map": { + "pox-addr": { + "Optional": { + "data": null + } + }, + "amount-ustx": { + "UInt": 2745989256202 + }, + "delegate-to": { + "Principal": { + "Standard": [ + 22, + [ + 59, + 188, + 101, + 209, + 18, + 231, + 158, + 169, + 210, + 0, + 30, + 21, + 248, + 166, + 21, + 136, + 86, + 149, + 216, + 182 + ] + ] + } + }, + "end-cycle-id": { + "Optional": { + "data": null + } + }, + "start-cycle-id": { + "UInt": 85 + }, + "unlock-burn-height": { + "Optional": { + "data": null + } + } + }, + "type_signature": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + } + }, + "name": { + "Sequence": { + "String": { + "ASCII": { + "data": [ + 100, + 101, + 108, + 101, + 103, + 97, + 116, + 101, + 45, + 115, + 116, + 120 + ] + } + } + } + }, + "locked": { + "UInt": 2745989256201 + }, + "balance": { + "UInt": 1000000 + }, + "stacker": { + "Principal": { + "Contract": { + "name": "xverse-v2-member1", + "issuer": [ + 20, + [ + 231, + 93, + 255, + 35, + 7, + 40, + 119, + 161, + 57, + 236, + 247, + 183, + 248, + 153, + 170, + 113, + 75, + 127, + 233, + 127 + ] + ] + } + } + }, + "burnchain-unlock-height": { + "UInt": 844550 + } + }, + "type_signature": { + "type_map": { + "data": { + "TupleType": { + "type_map": { + "pox-addr": { + "OptionalType": "NoType" + }, + "amount-ustx": "UIntType", + "delegate-to": "PrincipalType", + "end-cycle-id": { + "OptionalType": "NoType" + }, + "start-cycle-id": "UIntType", + "unlock-burn-height": { + "OptionalType": "NoType" + } + } + } + }, + "name": { + "SequenceType": { + "StringType": { + "ASCII": 12 + } + } + }, + "locked": "UIntType", + "balance": "UIntType", + "stacker": "PrincipalType", + "burnchain-unlock-height": "UIntType" + } + } + } + }, + "committed": true + } + }, + "raw_value": "0x070c000000060762616c616e636501000000000000000000000000000f4240176275726e636861696e2d756e6c6f636b2d68656967687401000000000000000000000000000ce30604646174610c000000060b616d6f756e742d757374780100000000000000000000027f59b6d40a0b64656c65676174652d746f05163bbc65d112e79ea9d2001e15f8a615885695d8b60c656e642d6379636c652d69640908706f782d61646472090e73746172742d6379636c652d6964010000000000000000000000000000005512756e6c6f636b2d6275726e2d68656967687409066c6f636b65640100000000000000000000027f59b6d409046e616d650d0000000c64656c65676174652d73747807737461636b65720614e75dff23072877a139ecf7b7f899aa714b7fe97f117876657273652d76322d6d656d62657231", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "stx_lock_event", + "committed": true, + "event_index": 108, + "stx_lock_event": { + "locked_amount": "2745989256201", + "unlock_height": "846650", + "locked_address": "SM3KNVZS30WM7F89SXKVVFY4SN9RMPZZ9FX929N0V.fastpool-v2-member1", + "contract_identifier": "SP000000000000000000002Q6VF78.pox-4" + } + }, + { + "txid": "0xd45e090ac442380cf50655e3d1c904c355a501d6dffa3b5e4799083062469dbc", + "type": "contract_event", + "committed": true, + "event_index": 117, + "contract_event": { + "topic": "print", + "value": { + "Sequence": { + "List": { + "data": [ + { + "Response": { + "data": { + "UInt": 0 + }, + "committed": true + } + }, + { + "Response": { + "data": { + "UInt": 603 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 603 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 34 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 603 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 18000 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 18000 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 18000 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 18000 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 18000 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 0 + }, + "committed": true + } + }, + { + "Response": { + "data": { + "UInt": 34 + }, + "committed": false + } + }, + { + "Response": { + "data": { + "UInt": 0 + }, + "committed": true + } + }, + { + "Response": { + "data": { + "UInt": 0 + }, + "committed": true + } + }, + { + "Response": { + "data": { + "UInt": 0 + }, + "committed": true + } + } + ], + "type_signature": { + "max_len": 15, + "entry_type": { + "ResponseType": [ + "UIntType", + "UIntType" + ] + } + } + } + } + }, + "raw_value": "0x0b0000000f07010000000000000000000000000000000008010000000000000000000000000000025b08010000000000000000000000000000025b08010000000000000000000000000000002208010000000000000000000000000000025b080100000000000000000000000000004650080100000000000000000000000000004650080100000000000000000000000000004650080100000000000000000000000000004650080100000000000000000000000000004650070100000000000000000000000000000000080100000000000000000000000000000022070100000000000000000000000000000000070100000000000000000000000000000000070100000000000000000000000000000000", + "contract_identifier": "SM3KNVZS30WM7F89SXKVVFY4SN9RMPZZ9FX929N0V.public-pools-strategy-v2" + } + } + ] +} diff --git a/src/tests/tx-tests.ts b/src/tests/tx-tests.ts index 4fcc7ef98c..095da598f6 100644 --- a/src/tests/tx-tests.ts +++ b/src/tests/tx-tests.ts @@ -18,6 +18,7 @@ import { publicKeyToAddress, AddressVersion, bufferCV, + stringAsciiCV, } from '@stacks/transactions'; import { createClarityValueArray } from '../stacks-encoding-helpers'; import { decodeTransaction, TxPayloadVersionedSmartContract } from 'stacks-encoding-native-js'; @@ -1938,6 +1939,614 @@ describe('tx tests', () => { expect(JSON.parse(fetchTx.text)).toEqual(expectedResp); }); + test('tx list - order by', async () => { + const block1 = new TestBlockBuilder({ block_height: 1, index_block_hash: '0x01' }) + .addTx({ + tx_id: '0x1234', + fee_rate: 1n, + burn_block_time: 2, + }) + .build(); + + await db.update(block1); + + const block2 = new TestBlockBuilder({ + block_height: 2, + index_block_hash: '0x02', + parent_block_hash: block1.block.block_hash, + parent_index_block_hash: block1.block.index_block_hash, + }) + .addTx({ + tx_id: '0x2234', + fee_rate: 3n, + burn_block_time: 1, + }) + .build(); + await db.update(block2); + + const block3 = new TestBlockBuilder({ + block_height: 3, + index_block_hash: '0x03', + parent_block_hash: block2.block.block_hash, + parent_index_block_hash: block2.block.index_block_hash, + }) + .addTx({ + tx_id: '0x3234', + fee_rate: 2n, + burn_block_time: 3, + }) + .build(); + await db.update(block3); + + const txsReqAsc = await supertest(api.server).get(`/extended/v1/tx?order=asc`); + expect(txsReqAsc.status).toBe(200); + expect(txsReqAsc.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReqDesc = await supertest(api.server).get(`/extended/v1/tx?order=desc`); + expect(txsReqDesc.status).toBe(200); + expect(txsReqDesc.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReqTimeDesc = await supertest(api.server).get( + `/extended/v1/tx?sort_by=burn_block_time&order=desc` + ); + expect(txsReqTimeDesc.status).toBe(200); + expect(txsReqTimeDesc.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReqTimeAsc = await supertest(api.server).get( + `/extended/v1/tx?sort_by=burn_block_time&order=asc` + ); + expect(txsReqTimeAsc.status).toBe(200); + expect(txsReqTimeAsc.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReqFeeDesc = await supertest(api.server).get(`/extended/v1/tx?sort_by=fee&order=desc`); + expect(txsReqFeeDesc.status).toBe(200); + expect(txsReqFeeDesc.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReqFeeAsc = await supertest(api.server).get(`/extended/v1/tx?sort_by=fee&order=asc`); + expect(txsReqFeeAsc.status).toBe(200); + expect(txsReqFeeAsc.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + ], + }) + ); + }); + + test('tx list - filter by to/from address', async () => { + const fromAddress = 'ST1HB1T8WRNBYB0Y3T7WXZS38NKKPTBR3EG9EPJKR'; + const toAddress = 'STRYYQQ9M8KAF4NS7WNZQYY59X93XEKR31JP64CP'; + const differentAddress = 'STF9B75ADQAVXQHNEQ6KGHXTG7JP305J2GRWF3A2'; + + const block1 = new TestBlockBuilder({ block_height: 1, index_block_hash: '0x01' }) + .addTx({ + tx_id: '0x0001', + sender_address: fromAddress, + token_transfer_recipient_address: toAddress, + }) + .build(); + await db.update(block1); + + const block2 = new TestBlockBuilder({ + block_height: 2, + index_block_hash: '0x02', + parent_block_hash: block1.block.block_hash, + parent_index_block_hash: block1.block.index_block_hash, + }) + .addTx({ + tx_id: '0x0002', + sender_address: fromAddress, + token_transfer_recipient_address: toAddress, + }) + .addTx({ + tx_id: '0x0003', + sender_address: fromAddress, + token_transfer_recipient_address: toAddress, + }) + .addTx({ + tx_id: '0x0004', + sender_address: fromAddress, + token_transfer_recipient_address: differentAddress, + }) + .addTx({ + tx_id: '0x0005', + sender_address: differentAddress, + token_transfer_recipient_address: toAddress, + }) + .build(); + await db.update(block2); + + const txsReqFrom = await supertest(api.server).get( + `/extended/v1/tx?from_address=${fromAddress}` + ); + expect(txsReqFrom.status).toBe(200); + expect(txsReqFrom.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[2].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[1].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReqTo = await supertest(api.server).get(`/extended/v1/tx?to_address=${toAddress}`); + expect(txsReqTo.status).toBe(200); + expect(txsReqTo.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[3].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[1].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReqFromTo = await supertest(api.server).get( + `/extended/v1/tx?from_address=${fromAddress}&to_address=${toAddress}` + ); + expect(txsReqFromTo.status).toBe(200); + expect(txsReqFromTo.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[1].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + ], + }) + ); + }); + + test('tx list - filter by timestamp', async () => { + const block1 = new TestBlockBuilder({ + block_height: 1, + index_block_hash: '0x01', + burn_block_time: 1710000000, + }) + .addTx({ + tx_id: '0x1234', + fee_rate: 1n, + }) + .build(); + + await db.update(block1); + + const block2 = new TestBlockBuilder({ + block_height: 2, + index_block_hash: '0x02', + parent_block_hash: block1.block.block_hash, + parent_index_block_hash: block1.block.index_block_hash, + burn_block_time: 1720000000, + }) + .addTx({ + tx_id: '0x2234', + fee_rate: 3n, + }) + .build(); + await db.update(block2); + + const block3 = new TestBlockBuilder({ + block_height: 3, + index_block_hash: '0x03', + parent_block_hash: block2.block.block_hash, + parent_index_block_hash: block2.block.index_block_hash, + burn_block_time: 1730000000, + }) + .addTx({ + tx_id: '0x3234', + fee_rate: 2n, + }) + .build(); + await db.update(block3); + + const txsReq1 = await supertest(api.server).get( + `/extended/v1/tx?start_time=${block1.block.burn_block_time}&end_time=${block3.block.burn_block_time}` + ); + expect(txsReq1.status).toBe(200); + expect(txsReq1.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReq2 = await supertest(api.server).get( + `/extended/v1/tx?start_time=${block2.block.burn_block_time}&end_time=${ + block3.block.burn_block_time - 1 + }` + ); + expect(txsReq2.status).toBe(200); + expect(txsReq2.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReq3 = await supertest(api.server).get( + `/extended/v1/tx?start_time=${block1.block.burn_block_time + 1}` + ); + expect(txsReq3.status).toBe(200); + expect(txsReq3.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReq4 = await supertest(api.server).get( + `/extended/v1/tx?end_time=${block3.block.burn_block_time - 1}` + ); + expect(txsReq4.status).toBe(200); + expect(txsReq4.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReq5 = await supertest(api.server).get( + `/extended/v1/tx?start_time=${block3.block.burn_block_time + 1}` + ); + expect(txsReq5.status).toBe(200); + expect(txsReq5.body).toEqual( + expect.objectContaining({ + results: [], + }) + ); + }); + + test('tx list - filter by contract id/name', async () => { + const testContractAddr = 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y.hello-world'; + const testContractFnName = 'test-contract-fn'; + const testContractFnName2 = 'test-contract-fn-2'; + const contractJsonAbi = { + maps: [], + functions: [ + { + args: [ + { type: 'uint128', name: 'amount' }, + { type: 'string-ascii', name: 'desc' }, + ], + name: testContractFnName, + access: 'public', + outputs: { + type: { + response: { + ok: 'uint128', + error: 'none', + }, + }, + }, + }, + { + args: [ + { type: 'uint128', name: 'amount' }, + { type: 'string-ascii', name: 'desc' }, + ], + name: testContractFnName2, + access: 'public', + outputs: { + type: { + response: { + ok: 'uint128', + error: 'none', + }, + }, + }, + }, + ], + variables: [], + fungible_tokens: [], + non_fungible_tokens: [], + }; + const block1 = new TestBlockBuilder({ + block_height: 1, + index_block_hash: '0x01', + burn_block_time: 1710000000, + }) + .addTx({ + tx_id: '0x0001', + fee_rate: 1n, + type_id: DbTxTypeId.SmartContract, + smart_contract_contract_id: testContractAddr, + smart_contract_clarity_version: 1, + smart_contract_source_code: '(some-contract-src)', + abi: JSON.stringify(contractJsonAbi), + }) + .build(); + + await db.update(block1); + + const block2 = new TestBlockBuilder({ + block_height: 2, + index_block_hash: '0x02', + parent_block_hash: block1.block.block_hash, + parent_index_block_hash: block1.block.index_block_hash, + burn_block_time: 1720000000, + }) + .addTx({ + tx_id: '0x1234', + fee_rate: 1n, + type_id: DbTxTypeId.ContractCall, + contract_call_contract_id: testContractAddr, + contract_call_function_name: testContractFnName, + contract_call_function_args: bufferToHex( + createClarityValueArray(uintCV(123456), stringAsciiCV('hello')) + ), + }) + .build(); + await db.update(block2); + + const block3 = new TestBlockBuilder({ + block_height: 3, + index_block_hash: '0x03', + parent_block_hash: block2.block.block_hash, + parent_index_block_hash: block2.block.index_block_hash, + burn_block_time: 1730000000, + }) + .addTx({ + tx_id: '0x2234', + type_id: DbTxTypeId.ContractCall, + contract_call_contract_id: testContractAddr, + contract_call_function_name: testContractFnName2, + contract_call_function_args: bufferToHex( + createClarityValueArray(uintCV(123456), stringAsciiCV('hello')) + ), + }) + .build(); + await db.update(block3); + + const txsReq1 = await supertest(api.server).get( + `/extended/v1/tx?contract_id=${testContractAddr}&function_name=${testContractFnName}` + ); + expect(txsReq1.status).toBe(200); + expect(txsReq1.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReq2 = await supertest(api.server).get( + `/extended/v1/tx?contract_id=${testContractAddr}` + ); + expect(txsReq2.status).toBe(200); + expect(txsReq2.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReq3 = await supertest(api.server).get( + `/extended/v1/tx?function_name=${testContractFnName2}` + ); + expect(txsReq3.status).toBe(200); + expect(txsReq3.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block3.txs[0].tx.tx_id, + }), + ], + }) + ); + }); + + test('tx list - filter by nonce', async () => { + const testSendertAddr = 'ST27W5M8BRKA7C5MZE2R1S1F4XTPHFWFRNHA9M04Y'; + const block1 = new TestBlockBuilder({ + block_height: 1, + index_block_hash: '0x01', + burn_block_time: 1710000000, + }) + .addTx({ + tx_id: '0x1234', + fee_rate: 1n, + sender_address: testSendertAddr, + nonce: 1, + }) + .build(); + + await db.update(block1); + + const block2 = new TestBlockBuilder({ + block_height: 2, + index_block_hash: '0x02', + parent_block_hash: block1.block.block_hash, + parent_index_block_hash: block1.block.index_block_hash, + burn_block_time: 1720000000, + }) + .addTx({ + tx_id: '0x2234', + fee_rate: 3n, + sender_address: testSendertAddr, + nonce: 2, + }) + .build(); + await db.update(block2); + + const block3 = new TestBlockBuilder({ + block_height: 3, + index_block_hash: '0x03', + parent_block_hash: block2.block.block_hash, + parent_index_block_hash: block2.block.index_block_hash, + burn_block_time: 1730000000, + }) + .addTx({ + tx_id: '0x3234', + fee_rate: 2n, + sender_address: testSendertAddr, + nonce: 3, + }) + .build(); + await db.update(block3); + + const txsReq1 = await supertest(api.server).get( + `/extended/v1/tx?from_address=${testSendertAddr}&nonce=${1}` + ); + expect(txsReq1.status).toBe(200); + expect(txsReq1.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block1.txs[0].tx.tx_id, + }), + ], + }) + ); + + const txsReq2 = await supertest(api.server).get( + `/extended/v1/tx?from_address=${testSendertAddr}&nonce=${2}` + ); + expect(txsReq2.status).toBe(200); + expect(txsReq2.body).toEqual( + expect.objectContaining({ + results: [ + expect.objectContaining({ + tx_id: block2.txs[0].tx.tx_id, + }), + ], + }) + ); + }); + test('fetch raw tx', async () => { const block: DbBlock = { block_hash: '0x1234',